Skip to main content

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