Skip to main content

freya_code_editor/
editor_ui.rs

1use std::borrow::Cow;
2
3use freya_components::{
4    get_theme_or_default,
5    scrollviews::{
6        ScrollConfig,
7        ScrollController,
8        VirtualScrollView,
9        use_scroll_controller,
10    },
11};
12use freya_core::prelude::*;
13use freya_edit::EditableEvent;
14use torin::size::Size;
15
16use crate::{
17    editor_data::CodeEditorData,
18    editor_line::{
19        EditorGutterLineUI,
20        EditorLineUI,
21    },
22    editor_theme::{
23        EditorTheme,
24        EditorThemePartial,
25        EditorThemePreference,
26    },
27};
28
29#[derive(PartialEq, Clone)]
30pub struct CodeEditor {
31    editor: Writable<CodeEditorData>,
32    font_size: f32,
33    line_height: f32,
34    read_only: bool,
35    gutter: bool,
36    show_whitespace: bool,
37    font_family: Cow<'static, str>,
38    a11y_id: AccessibilityId,
39    a11y_auto_focus: bool,
40    cursor_mode: CursorMode,
41    scroll_controller: Option<ScrollController>,
42    pub(crate) theme: Option<EditorThemePartial>,
43    on_pre_key_down: Callback<Event<KeyboardEventData>, bool>,
44}
45
46impl CodeEditor {
47    /// Creates a new editor UI component with the given writable data.
48    ///
49    /// Default values are applied for font size and line height.
50    pub fn new(editor: impl Into<Writable<CodeEditorData>>, a11y_id: AccessibilityId) -> Self {
51        Self {
52            editor: editor.into(),
53            font_size: 14.0,
54            line_height: 1.4,
55            read_only: false,
56            gutter: true,
57            show_whitespace: true,
58            font_family: Cow::Borrowed("Jetbrains Mono"),
59            a11y_id,
60            a11y_auto_focus: false,
61            cursor_mode: CursorMode::Expanded,
62            scroll_controller: None,
63            theme: None,
64            on_pre_key_down: Callback::new(|e: Event<KeyboardEventData>| {
65                e.stop_propagation();
66                if let Key::Named(NamedKey::Tab) = &e.key {
67                    e.prevent_default();
68                }
69                true
70            }),
71        }
72    }
73
74    pub fn font_size(mut self, size: f32) -> Self {
75        self.font_size = size;
76        self
77    }
78
79    /// Sets the line height multiplier (relative to font size).
80    pub fn line_height(mut self, height: f32) -> Self {
81        self.line_height = height;
82        self
83    }
84
85    /// Sets whether the editor is read-only.
86    pub fn read_only(mut self, read_only: bool) -> Self {
87        self.read_only = read_only;
88        self
89    }
90
91    /// Sets whether the gutter (line numbers) is visible.
92    pub fn gutter(mut self, gutter: bool) -> Self {
93        self.gutter = gutter;
94        self
95    }
96
97    /// Sets whether leading whitespace characters are rendered visually.
98    pub fn show_whitespace(mut self, show_whitespace: bool) -> Self {
99        self.show_whitespace = show_whitespace;
100        self
101    }
102
103    /// Sets the font family used in the editor. Defaults to `"Jetbrains Mono"`.
104    pub fn font_family(mut self, font_family: impl Into<Cow<'static, str>>) -> Self {
105        self.font_family = font_family.into();
106        self
107    }
108
109    /// Set the [`CursorMode`]. Defaults to [`CursorMode::Expanded`].
110    pub fn cursor_mode(mut self, cursor_mode: impl Into<CursorMode>) -> Self {
111        self.cursor_mode = cursor_mode.into();
112        self
113    }
114
115    /// Sets the [`ScrollController`] driving the scroll position, instead of an internal one.
116    pub fn scroll_controller(
117        mut self,
118        scroll_controller: impl Into<Option<ScrollController>>,
119    ) -> Self {
120        self.scroll_controller = scroll_controller.into();
121        self
122    }
123
124    /// Sets whether the editor automatically receives focus.
125    pub fn a11y_auto_focus(mut self, a11y_auto_focus: bool) -> Self {
126        self.a11y_auto_focus = a11y_auto_focus;
127        self
128    }
129
130    /// Sets a pre-handler called for each key event. Return `true` to let the editor process it,
131    /// `false` to skip. The callback may call `stop_propagation()` / `prevent_default()` directly.
132    pub fn on_pre_key_down(
133        mut self,
134        on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
135    ) -> Self {
136        self.on_pre_key_down = on_pre_key_down.into();
137        self
138    }
139}
140
141impl Component for CodeEditor {
142    fn render(&self) -> impl IntoElement {
143        let CodeEditor {
144            editor,
145            font_size,
146            line_height,
147            read_only,
148            gutter,
149            show_whitespace,
150            font_family,
151            a11y_id,
152            a11y_auto_focus,
153            cursor_mode,
154            scroll_controller,
155            theme,
156            on_pre_key_down,
157        } = self.clone();
158
159        let theme = get_theme_or_default!(&theme, EditorThemePreference, "code_editor", || {
160            EditorTheme::light().into()
161        });
162
163        let editor_data = editor.read_unchecked();
164
165        let scroll_controller =
166            scroll_controller.unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
167        let line_height = (font_size * line_height).floor();
168        let lines_len = editor_data.metrics.syntax_blocks.len();
169
170        let on_key_up = {
171            let mut editor = editor.clone();
172            let font_family = font_family.clone();
173            move |e: Event<KeyboardEventData>| {
174                editor.write_if(|mut editor| {
175                    editor.process(
176                        font_size,
177                        &font_family,
178                        EditableEvent::KeyUp { key: &e.key },
179                    )
180                });
181            }
182        };
183
184        let on_key_down = {
185            let mut editor = editor.clone();
186            let mut scroll_controller = scroll_controller;
187            let font_family = font_family.clone();
188            move |e: Event<KeyboardEventData>| {
189                const LINES_JUMP_ALT: usize = 5;
190                const LINES_JUMP_CONTROL: usize = 3;
191
192                let key = e.key.clone();
193                let modifiers = e.modifiers;
194
195                if !on_pre_key_down.call(e) {
196                    return;
197                }
198
199                editor.write_if(|mut editor| {
200                    let lines_jump = (line_height * LINES_JUMP_ALT as f32).ceil() as i32;
201                    let content_height = lines_len as f32 * line_height;
202                    let viewport_height = editor.viewport.height;
203                    let min_scroll = -(content_height - viewport_height).max(0.) as i32;
204                    let max_scroll = 0;
205                    let (_, current_scroll) = scroll_controller.into();
206
207                    let events = match &key {
208                        Key::Named(NamedKey::ArrowUp) if modifiers.contains(Modifiers::ALT) => {
209                            let jump = (current_scroll + lines_jump).clamp(min_scroll, max_scroll);
210                            scroll_controller.scroll_to_y(jump);
211                            (0..LINES_JUMP_ALT)
212                                .map(|_| EditableEvent::KeyDown {
213                                    key: &key,
214                                    modifiers,
215                                    editor_line: None,
216                                    holder: None,
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_scroll, max_scroll);
222                            scroll_controller.scroll_to_y(jump);
223                            (0..LINES_JUMP_ALT)
224                                .map(|_| EditableEvent::KeyDown {
225                                    key: &key,
226                                    modifiers,
227                                    editor_line: None,
228                                    holder: None,
229                                })
230                                .collect::<Vec<EditableEvent>>()
231                        }
232                        Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
233                            if modifiers.contains(Modifiers::CONTROL) =>
234                        {
235                            (0..LINES_JUMP_CONTROL)
236                                .map(|_| EditableEvent::KeyDown {
237                                    key: &key,
238                                    modifiers,
239                                    editor_line: None,
240                                    holder: None,
241                                })
242                                .collect::<Vec<EditableEvent>>()
243                        }
244                        _ => vec![EditableEvent::KeyDown {
245                            key: &key,
246                            modifiers,
247                            editor_line: None,
248                            holder: None,
249                        }],
250                    };
251
252                    let mut changed = false;
253
254                    for event in events {
255                        changed |= editor.process(font_size, &font_family, event);
256                    }
257
258                    if changed {
259                        editor.scroll_to_cursor(scroll_controller, line_height);
260                    }
261
262                    changed
263                });
264            }
265        };
266
267        let on_global_pointer_press = {
268            let mut editor = editor.clone();
269            let font_family = font_family.clone();
270            move |_: Event<PointerEventData>| {
271                editor.write_if(|mut editor| {
272                    editor.process(font_size, &font_family, EditableEvent::Release)
273                });
274            }
275        };
276
277        let gutter_editor = editor.clone();
278        let gutter_theme = theme.clone();
279
280        let on_sized = {
281            let mut editor = editor.clone();
282            move |e: Event<SizedEventData>| {
283                editor.write_if(|mut editor| {
284                    let changed = editor.viewport != e.area.size;
285                    editor.viewport = e.area.size;
286                    changed
287                });
288            }
289        };
290
291        rect()
292            .a11y_auto_focus(a11y_auto_focus)
293            .a11y_focusable(true)
294            .a11y_id(a11y_id)
295            .a11y_role(AccessibilityRole::TextInput)
296            .expanded()
297            .background(theme.background)
298            .maybe(!read_only, |el| {
299                el.on_key_down(on_key_down).on_key_up(on_key_up)
300            })
301            .on_global_pointer_press(on_global_pointer_press)
302            .child(
303                rect()
304                    .horizontal()
305                    .maybe_child(gutter.then(|| {
306                        let gutter_width = font_size * 5.0;
307                        VirtualScrollView::new(move |item, _| {
308                            EditorGutterLineUI {
309                                editor: gutter_editor.clone(),
310                                font_size,
311                                line_height,
312                                line_index: item.index,
313                                theme: gutter_theme.clone(),
314                            }
315                            .into()
316                        })
317                        .width(Size::px(gutter_width))
318                        .show_scrollbar(false)
319                        .scroll_controller(scroll_controller)
320                        .length(lines_len)
321                        .item_size(line_height)
322                    }))
323                    .child(
324                        VirtualScrollView::new(move |item, _| {
325                            EditorLineUI {
326                                editor: editor.clone(),
327                                font_size,
328                                line_height,
329                                line_index: item.index,
330                                read_only,
331                                show_whitespace,
332                                font_family: font_family.clone(),
333                                cursor_mode,
334                                theme: theme.clone(),
335                                a11y_id,
336                                scroll_controller,
337                            }
338                            .into()
339                        })
340                        .expanded()
341                        .scroll_controller(scroll_controller)
342                        .on_sized(on_sized)
343                        .length(lines_len)
344                        .item_size(line_height),
345                    ),
346            )
347    }
348}