freya_code_editor/
editor_ui.rs

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