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        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    /// Creates a new editor UI component with the given writable data.
41    ///
42    /// Default values are applied for font size and line height.
43    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    /// Sets the line height multiplier (relative to font size).
71    pub fn line_height(mut self, height: f32) -> Self {
72        self.line_height = height;
73        self
74    }
75
76    /// Sets whether the editor is read-only.
77    pub fn read_only(mut self, read_only: bool) -> Self {
78        self.read_only = read_only;
79        self
80    }
81
82    /// Sets whether the gutter (line numbers) is visible.
83    pub fn gutter(mut self, gutter: bool) -> Self {
84        self.gutter = gutter;
85        self
86    }
87
88    /// Sets whether leading whitespace characters are rendered visually.
89    pub fn show_whitespace(mut self, show_whitespace: bool) -> Self {
90        self.show_whitespace = show_whitespace;
91        self
92    }
93
94    /// Sets the font family used in the editor. Defaults to `"Jetbrains Mono"`.
95    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    /// Sets whether the editor automatically receives focus.
101    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    /// Sets a pre-handler called for each key event. Return `true` to let the editor process it,
107    /// `false` to skip. The callback may call `stop_propagation()` / `prevent_default()` directly.
108    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; // TODO, this should be the height of the viewport
207                    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                                    editor_line: None,
218                                    holder: None,
219                                })
220                                .collect::<Vec<EditableEvent>>()
221                        }
222                        Key::Named(NamedKey::ArrowDown) if modifiers.contains(Modifiers::ALT) => {
223                            let jump = (current_scroll - lines_jump).clamp(min_height, max_height);
224                            editor.scrolls.1 = jump;
225                            (0..LINES_JUMP_ALT)
226                                .map(|_| EditableEvent::KeyDown {
227                                    key: &key,
228                                    modifiers,
229                                    editor_line: None,
230                                    holder: None,
231                                })
232                                .collect::<Vec<EditableEvent>>()
233                        }
234                        Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
235                            if modifiers.contains(Modifiers::CONTROL) =>
236                        {
237                            (0..LINES_JUMP_CONTROL)
238                                .map(|_| EditableEvent::KeyDown {
239                                    key: &key,
240                                    modifiers,
241                                    editor_line: None,
242                                    holder: None,
243                                })
244                                .collect::<Vec<EditableEvent>>()
245                        }
246                        _ => vec![EditableEvent::KeyDown {
247                            key: &key,
248                            modifiers,
249                            editor_line: None,
250                            holder: None,
251                        }],
252                    };
253
254                    let mut changed = false;
255
256                    for event in events {
257                        changed |= editor.process(font_size, &font_family, event);
258                    }
259
260                    changed
261                });
262            }
263        };
264
265        let on_global_pointer_press = {
266            let mut editor = editor.clone();
267            let font_family = font_family.clone();
268            move |_: Event<PointerEventData>| {
269                editor.write_if(|mut editor_editor| {
270                    editor_editor.process(font_size, &font_family, EditableEvent::Release)
271                });
272            }
273        };
274
275        rect()
276            .a11y_auto_focus(a11y_auto_focus)
277            .a11y_focusable(true)
278            .a11y_id(a11y_id)
279            .a11y_role(AccessibilityRole::TextInput)
280            .expanded()
281            .background(theme.background)
282            .maybe(!read_only, |el| {
283                el.on_key_down(on_key_down).on_key_up(on_key_up)
284            })
285            .on_global_pointer_press(on_global_pointer_press)
286            .child(
287                VirtualScrollView::new(move |item, _| {
288                    EditorLineUI {
289                        editor: editor.clone(),
290                        font_size,
291                        line_height,
292                        line_index: item.index,
293                        read_only,
294                        gutter,
295                        show_whitespace,
296                        font_family: font_family.clone(),
297                        theme: theme.clone(),
298                        a11y_id,
299                    }
300                    .into()
301                })
302                .scroll_controller(scroll_controller)
303                .length(lines_len)
304                .item_size(line_height),
305            )
306    }
307}