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                                })
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}