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