1use std::borrow::Cow;
2
3use freya_components::{
4 get_theme_or_default,
5 scrollviews::{
6 ScrollController,
7 ScrollEvent,
8 VirtualScrollView,
9 },
10};
11use freya_core::prelude::*;
12use freya_edit::EditableEvent;
13
14use crate::{
15 editor_data::CodeEditorData,
16 editor_line::EditorLineUI,
17 editor_theme::{
18 EditorTheme,
19 EditorThemePartial,
20 EditorThemePreference,
21 },
22};
23
24#[derive(PartialEq, Clone)]
25pub struct CodeEditor {
26 editor: Writable<CodeEditorData>,
27 font_size: f32,
28 line_height: f32,
29 read_only: bool,
30 gutter: bool,
31 show_whitespace: bool,
32 font_family: Cow<'static, str>,
33 a11y_id: AccessibilityId,
34 a11y_auto_focus: bool,
35 pub(crate) theme: Option<EditorThemePartial>,
36 on_pre_key_down: Callback<Event<KeyboardEventData>, bool>,
37}
38
39impl CodeEditor {
40 pub fn new(editor: impl Into<Writable<CodeEditorData>>, a11y_id: AccessibilityId) -> Self {
44 Self {
45 editor: editor.into(),
46 font_size: 14.0,
47 line_height: 1.4,
48 read_only: false,
49 gutter: true,
50 show_whitespace: true,
51 font_family: Cow::Borrowed("Jetbrains Mono"),
52 a11y_id,
53 a11y_auto_focus: false,
54 theme: None,
55 on_pre_key_down: Callback::new(|e: Event<KeyboardEventData>| {
56 e.stop_propagation();
57 if let Key::Named(NamedKey::Tab) = &e.key {
58 e.prevent_default();
59 }
60 true
61 }),
62 }
63 }
64
65 pub fn font_size(mut self, size: f32) -> Self {
66 self.font_size = size;
67 self
68 }
69
70 pub fn line_height(mut self, height: f32) -> Self {
72 self.line_height = height;
73 self
74 }
75
76 pub fn read_only(mut self, read_only: bool) -> Self {
78 self.read_only = read_only;
79 self
80 }
81
82 pub fn gutter(mut self, gutter: bool) -> Self {
84 self.gutter = gutter;
85 self
86 }
87
88 pub fn show_whitespace(mut self, show_whitespace: bool) -> Self {
90 self.show_whitespace = show_whitespace;
91 self
92 }
93
94 pub fn font_family(mut self, font_family: impl Into<Cow<'static, str>>) -> Self {
96 self.font_family = font_family.into();
97 self
98 }
99
100 pub fn a11y_auto_focus(mut self, a11y_auto_focus: bool) -> Self {
102 self.a11y_auto_focus = a11y_auto_focus;
103 self
104 }
105
106 pub fn on_pre_key_down(
109 mut self,
110 on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
111 ) -> Self {
112 self.on_pre_key_down = on_pre_key_down.into();
113 self
114 }
115}
116
117impl Component for CodeEditor {
118 fn render(&self) -> impl IntoElement {
119 let CodeEditor {
120 editor,
121 font_size,
122 line_height,
123 read_only,
124 gutter,
125 show_whitespace,
126 font_family,
127 a11y_id,
128 a11y_auto_focus,
129 theme,
130 on_pre_key_down,
131 } = self.clone();
132
133 let theme = get_theme_or_default!(&theme, EditorThemePreference, "code_editor", || {
134 EditorTheme::light().into()
135 });
136
137 let editor_data = editor.read();
138
139 let scroll_controller = use_hook(|| {
140 let notifier = State::create(());
141 let requests = State::create(vec![]);
142 ScrollController::managed(
143 notifier,
144 requests,
145 State::create(Callback::new({
146 let mut editor = editor.clone();
147 move |ev| {
148 editor.write_if(|mut editor| {
149 let current = editor.scrolls;
150 match ev {
151 ScrollEvent::X(x) => {
152 editor.scrolls.0 = x;
153 }
154 ScrollEvent::Y(y) => {
155 editor.scrolls.1 = y;
156 }
157 }
158 current != editor.scrolls
159 })
160 }
161 })),
162 State::create(Callback::new({
163 let editor = editor.clone();
164 move |_| {
165 let editor = editor.read();
166 editor.scrolls
167 }
168 })),
169 )
170 });
171
172 let line_height = (font_size * line_height).floor();
173 let lines_len = editor_data.metrics.syntax_blocks.len();
174
175 let on_key_up = {
176 let mut editor = editor.clone();
177 let font_family = font_family.clone();
178 move |e: Event<KeyboardEventData>| {
179 editor.write_if(|mut editor| {
180 editor.process(
181 font_size,
182 &font_family,
183 EditableEvent::KeyUp { key: &e.key },
184 )
185 });
186 }
187 };
188
189 let on_key_down = {
190 let mut editor = editor.clone();
191 let font_family = font_family.clone();
192 move |e: Event<KeyboardEventData>| {
193 const LINES_JUMP_ALT: usize = 5;
194 const LINES_JUMP_CONTROL: usize = 3;
195
196 let key = e.key.clone();
197 let modifiers = e.modifiers;
198
199 if !on_pre_key_down.call(e) {
200 return;
201 }
202
203 editor.write_if(|mut editor| {
204 let lines_jump = (line_height * LINES_JUMP_ALT as f32).ceil() as i32;
205 let min_height = -(lines_len as f32 * line_height) as i32;
206 let max_height = 0; let current_scroll = editor.scrolls.1;
208
209 let events = match &key {
210 Key::Named(NamedKey::ArrowUp) if modifiers.contains(Modifiers::ALT) => {
211 let jump = (current_scroll + lines_jump).clamp(min_height, max_height);
212 editor.scrolls.1 = jump;
213 (0..LINES_JUMP_ALT)
214 .map(|_| EditableEvent::KeyDown {
215 key: &key,
216 modifiers,
217 })
218 .collect::<Vec<EditableEvent>>()
219 }
220 Key::Named(NamedKey::ArrowDown) if modifiers.contains(Modifiers::ALT) => {
221 let jump = (current_scroll - lines_jump).clamp(min_height, max_height);
222 editor.scrolls.1 = jump;
223 (0..LINES_JUMP_ALT)
224 .map(|_| EditableEvent::KeyDown {
225 key: &key,
226 modifiers,
227 })
228 .collect::<Vec<EditableEvent>>()
229 }
230 Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
231 if modifiers.contains(Modifiers::CONTROL) =>
232 {
233 (0..LINES_JUMP_CONTROL)
234 .map(|_| EditableEvent::KeyDown {
235 key: &key,
236 modifiers,
237 })
238 .collect::<Vec<EditableEvent>>()
239 }
240 _ => vec![EditableEvent::KeyDown {
241 key: &key,
242 modifiers,
243 }],
244 };
245
246 let mut changed = false;
247
248 for event in events {
249 changed |= editor.process(font_size, &font_family, event);
250 }
251
252 changed
253 });
254 }
255 };
256
257 let on_global_pointer_press = {
258 let mut editor = editor.clone();
259 let font_family = font_family.clone();
260 move |_: Event<PointerEventData>| {
261 editor.write_if(|mut editor_editor| {
262 editor_editor.process(font_size, &font_family, EditableEvent::Release)
263 });
264 }
265 };
266
267 rect()
268 .a11y_auto_focus(a11y_auto_focus)
269 .a11y_focusable(true)
270 .a11y_id(a11y_id)
271 .a11y_role(AccessibilityRole::TextInput)
272 .expanded()
273 .background(theme.background)
274 .maybe(!read_only, |el| {
275 el.on_key_down(on_key_down).on_key_up(on_key_up)
276 })
277 .on_global_pointer_press(on_global_pointer_press)
278 .child(
279 VirtualScrollView::new(move |line_index, _| {
280 EditorLineUI {
281 editor: editor.clone(),
282 font_size,
283 line_height,
284 line_index,
285 read_only,
286 gutter,
287 show_whitespace,
288 font_family: font_family.clone(),
289 theme: theme.clone(),
290 a11y_id,
291 }
292 .into()
293 })
294 .scroll_controller(scroll_controller)
295 .length(lines_len)
296 .item_size(line_height),
297 )
298 }
299}