freya_code_editor/
editor_ui.rs1use 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 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 pub fn line_height(mut self, height: f32) -> Self {
54 self.line_height = height;
55 self
56 }
57
58 pub fn read_only(mut self, read_only: bool) -> Self {
60 self.read_only = read_only;
61 self
62 }
63
64 pub fn gutter(mut self, gutter: bool) -> Self {
66 self.gutter = gutter;
67 self
68 }
69
70 pub fn show_whitespace(mut self, show_whitespace: bool) -> Self {
72 self.show_whitespace = show_whitespace;
73 self
74 }
75
76 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; 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}