Skip to main content

freya_code_editor/
editor_ui.rs

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