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