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 scroll_controller = use_hook(|| {
119            let notifier = State::create(());
120            let requests = State::create(vec![]);
121            ScrollController::managed(
122                notifier,
123                requests,
124                State::create(Callback::new({
125                    let mut editor = editor.clone();
126                    move |ev| {
127                        editor.write_if(|mut editor| {
128                            let current = editor.scrolls;
129                            match ev {
130                                ScrollEvent::X(x) => {
131                                    editor.scrolls.0 = x;
132                                }
133                                ScrollEvent::Y(y) => {
134                                    editor.scrolls.1 = y;
135                                }
136                            }
137                            current != editor.scrolls
138                        })
139                    }
140                })),
141                State::create(Callback::new({
142                    let editor = editor.clone();
143                    move |_| {
144                        let editor = editor.read();
145                        editor.scrolls
146                    }
147                })),
148            )
149        });
150
151        let line_height = (font_size * line_height).floor();
152        let lines_len = editor_data.metrics.syntax_blocks.len();
153
154        let on_pointer_down = move |e: Event<PointerEventData>| {
155            e.prevent_default();
156            e.stop_propagation();
157            a11y_id.request_focus();
158        };
159
160        let on_key_up = {
161            let mut editor = editor.clone();
162            let font_family = font_family.clone();
163            move |e: Event<KeyboardEventData>| {
164                editor.write_if(|mut editor| {
165                    editor.process(
166                        font_size,
167                        &font_family,
168                        EditableEvent::KeyUp { key: &e.key },
169                    )
170                });
171            }
172        };
173
174        let on_key_down = {
175            let mut editor = editor.clone();
176            let font_family = font_family.clone();
177            move |e: Event<KeyboardEventData>| {
178                e.stop_propagation();
179
180                if let Key::Named(NamedKey::Tab) = &e.key {
181                    e.prevent_default();
182                }
183
184                const LINES_JUMP_ALT: usize = 5;
185                const LINES_JUMP_CONTROL: usize = 3;
186
187                editor.write_if(|mut editor| {
188                    let lines_jump = (line_height * LINES_JUMP_ALT as f32).ceil() as i32;
189                    let min_height = -(lines_len as f32 * line_height) as i32;
190                    let max_height = 0; // TODO, this should be the height of the viewport
191                    let current_scroll = editor.scrolls.1;
192
193                    let events = match &e.key {
194                        Key::Named(NamedKey::ArrowUp) if e.modifiers.contains(Modifiers::ALT) => {
195                            let jump = (current_scroll + lines_jump).clamp(min_height, max_height);
196                            editor.scrolls.1 = jump;
197                            (0..LINES_JUMP_ALT)
198                                .map(|_| EditableEvent::KeyDown {
199                                    key: &e.key,
200                                    modifiers: e.modifiers,
201                                })
202                                .collect::<Vec<EditableEvent>>()
203                        }
204                        Key::Named(NamedKey::ArrowDown) if e.modifiers.contains(Modifiers::ALT) => {
205                            let jump = (current_scroll - lines_jump).clamp(min_height, max_height);
206                            editor.scrolls.1 = jump;
207                            (0..LINES_JUMP_ALT)
208                                .map(|_| EditableEvent::KeyDown {
209                                    key: &e.key,
210                                    modifiers: e.modifiers,
211                                })
212                                .collect::<Vec<EditableEvent>>()
213                        }
214                        Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
215                            if e.modifiers.contains(Modifiers::CONTROL) =>
216                        {
217                            (0..LINES_JUMP_CONTROL)
218                                .map(|_| EditableEvent::KeyDown {
219                                    key: &e.key,
220                                    modifiers: e.modifiers,
221                                })
222                                .collect::<Vec<EditableEvent>>()
223                        }
224                        _ if e.code == Code::Escape
225                            || e.modifiers.contains(Modifiers::ALT)
226                            || (e.modifiers.contains(Modifiers::CONTROL)
227                                && e.code == Code::KeyS) =>
228                        {
229                            Vec::new()
230                        }
231                        _ => {
232                            vec![EditableEvent::KeyDown {
233                                key: &e.key,
234                                modifiers: e.modifiers,
235                            }]
236                        }
237                    };
238
239                    let mut changed = false;
240
241                    for event in events {
242                        changed |= editor.process(font_size, &font_family, event);
243                    }
244
245                    changed
246                });
247            }
248        };
249
250        let on_global_pointer_press = {
251            let mut editor = editor.clone();
252            let font_family = font_family.clone();
253            move |_: Event<PointerEventData>| {
254                editor.write_if(|mut editor_editor| {
255                    editor_editor.process(font_size, &font_family, EditableEvent::Release)
256                });
257            }
258        };
259
260        rect()
261            .a11y_auto_focus(a11y_auto_focus)
262            .a11y_focusable(true)
263            .a11y_id(a11y_id)
264            .a11y_role(AccessibilityRole::TextInput)
265            .expanded()
266            .background(theme.read().background)
267            .maybe(!read_only, |el| {
268                el.on_key_down(on_key_down).on_key_up(on_key_up)
269            })
270            .on_global_pointer_press(on_global_pointer_press)
271            .on_pointer_down(on_pointer_down)
272            .child(
273                VirtualScrollView::new(move |line_index, _| {
274                    EditorLineUI {
275                        editor: editor.clone(),
276                        font_size,
277                        line_height,
278                        line_index,
279                        read_only,
280                        gutter,
281                        show_whitespace,
282                        font_family: font_family.clone(),
283                        theme: theme.clone(),
284                    }
285                    .into()
286                })
287                .scroll_controller(scroll_controller)
288                .length(lines_len)
289                .item_size(line_height),
290            )
291    }
292}