1use std::borrow::Cow;
2
3use freya_components::{
4 get_theme_or_default,
5 scrollviews::{
6 ScrollConfig,
7 ScrollController,
8 VirtualScrollView,
9 use_scroll_controller,
10 },
11};
12use freya_core::prelude::*;
13use freya_edit::EditableEvent;
14use torin::size::Size;
15
16use crate::{
17 editor_data::CodeEditorData,
18 editor_line::{
19 EditorGutterLineUI,
20 EditorLineUI,
21 },
22 editor_theme::{
23 EditorTheme,
24 EditorThemePartial,
25 EditorThemePreference,
26 },
27};
28
29#[derive(PartialEq, Clone)]
30pub struct CodeEditor {
31 editor: Writable<CodeEditorData>,
32 font_size: f32,
33 line_height: f32,
34 read_only: bool,
35 gutter: bool,
36 show_whitespace: bool,
37 font_family: Cow<'static, str>,
38 a11y_id: AccessibilityId,
39 a11y_auto_focus: bool,
40 cursor_mode: CursorMode,
41 scroll_controller: Option<ScrollController>,
42 pub(crate) theme: Option<EditorThemePartial>,
43 on_pre_key_down: Callback<Event<KeyboardEventData>, bool>,
44}
45
46impl CodeEditor {
47 pub fn new(editor: impl Into<Writable<CodeEditorData>>, a11y_id: AccessibilityId) -> Self {
51 Self {
52 editor: editor.into(),
53 font_size: 14.0,
54 line_height: 1.4,
55 read_only: false,
56 gutter: true,
57 show_whitespace: true,
58 font_family: Cow::Borrowed("Jetbrains Mono"),
59 a11y_id,
60 a11y_auto_focus: false,
61 cursor_mode: CursorMode::Expanded,
62 scroll_controller: None,
63 theme: None,
64 on_pre_key_down: Callback::new(|e: Event<KeyboardEventData>| {
65 e.stop_propagation();
66 if let Key::Named(NamedKey::Tab) = &e.key {
67 e.prevent_default();
68 }
69 true
70 }),
71 }
72 }
73
74 pub fn font_size(mut self, size: f32) -> Self {
75 self.font_size = size;
76 self
77 }
78
79 pub fn line_height(mut self, height: f32) -> Self {
81 self.line_height = height;
82 self
83 }
84
85 pub fn read_only(mut self, read_only: bool) -> Self {
87 self.read_only = read_only;
88 self
89 }
90
91 pub fn gutter(mut self, gutter: bool) -> Self {
93 self.gutter = gutter;
94 self
95 }
96
97 pub fn show_whitespace(mut self, show_whitespace: bool) -> Self {
99 self.show_whitespace = show_whitespace;
100 self
101 }
102
103 pub fn font_family(mut self, font_family: impl Into<Cow<'static, str>>) -> Self {
105 self.font_family = font_family.into();
106 self
107 }
108
109 pub fn cursor_mode(mut self, cursor_mode: impl Into<CursorMode>) -> Self {
111 self.cursor_mode = cursor_mode.into();
112 self
113 }
114
115 pub fn scroll_controller(
117 mut self,
118 scroll_controller: impl Into<Option<ScrollController>>,
119 ) -> Self {
120 self.scroll_controller = scroll_controller.into();
121 self
122 }
123
124 pub fn a11y_auto_focus(mut self, a11y_auto_focus: bool) -> Self {
126 self.a11y_auto_focus = a11y_auto_focus;
127 self
128 }
129
130 pub fn on_pre_key_down(
133 mut self,
134 on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
135 ) -> Self {
136 self.on_pre_key_down = on_pre_key_down.into();
137 self
138 }
139}
140
141impl Component for CodeEditor {
142 fn render(&self) -> impl IntoElement {
143 let CodeEditor {
144 editor,
145 font_size,
146 line_height,
147 read_only,
148 gutter,
149 show_whitespace,
150 font_family,
151 a11y_id,
152 a11y_auto_focus,
153 cursor_mode,
154 scroll_controller,
155 theme,
156 on_pre_key_down,
157 } = self.clone();
158
159 let theme = get_theme_or_default!(&theme, EditorThemePreference, "code_editor", || {
160 EditorTheme::light().into()
161 });
162
163 let editor_data = editor.read_unchecked();
164
165 let scroll_controller =
166 scroll_controller.unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
167 let line_height = (font_size * line_height).floor();
168 let lines_len = editor_data.metrics.syntax_blocks.len();
169
170 let on_key_up = {
171 let mut editor = editor.clone();
172 let font_family = font_family.clone();
173 move |e: Event<KeyboardEventData>| {
174 editor.write_if(|mut editor| {
175 editor.process(
176 font_size,
177 &font_family,
178 EditableEvent::KeyUp { key: &e.key },
179 )
180 });
181 }
182 };
183
184 let on_key_down = {
185 let mut editor = editor.clone();
186 let mut scroll_controller = scroll_controller;
187 let font_family = font_family.clone();
188 move |e: Event<KeyboardEventData>| {
189 const LINES_JUMP_ALT: usize = 5;
190 const LINES_JUMP_CONTROL: usize = 3;
191
192 let key = e.key.clone();
193 let modifiers = e.modifiers;
194
195 if !on_pre_key_down.call(e) {
196 return;
197 }
198
199 editor.write_if(|mut editor| {
200 let lines_jump = (line_height * LINES_JUMP_ALT as f32).ceil() as i32;
201 let content_height = lines_len as f32 * line_height;
202 let viewport_height = editor.viewport.height;
203 let min_scroll = -(content_height - viewport_height).max(0.) as i32;
204 let max_scroll = 0;
205 let (_, current_scroll) = scroll_controller.into();
206
207 let events = match &key {
208 Key::Named(NamedKey::ArrowUp) if modifiers.contains(Modifiers::ALT) => {
209 let jump = (current_scroll + lines_jump).clamp(min_scroll, max_scroll);
210 scroll_controller.scroll_to_y(jump);
211 (0..LINES_JUMP_ALT)
212 .map(|_| EditableEvent::KeyDown {
213 key: &key,
214 modifiers,
215 editor_line: None,
216 holder: None,
217 })
218 .collect::<Vec<EditableEvent>>()
219 }
220 Key::Named(NamedKey::ArrowDown) if modifiers.contains(Modifiers::ALT) => {
221 let jump = (current_scroll - lines_jump).clamp(min_scroll, max_scroll);
222 scroll_controller.scroll_to_y(jump);
223 (0..LINES_JUMP_ALT)
224 .map(|_| EditableEvent::KeyDown {
225 key: &key,
226 modifiers,
227 editor_line: None,
228 holder: None,
229 })
230 .collect::<Vec<EditableEvent>>()
231 }
232 Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
233 if modifiers.contains(Modifiers::CONTROL) =>
234 {
235 (0..LINES_JUMP_CONTROL)
236 .map(|_| EditableEvent::KeyDown {
237 key: &key,
238 modifiers,
239 editor_line: None,
240 holder: None,
241 })
242 .collect::<Vec<EditableEvent>>()
243 }
244 _ => vec![EditableEvent::KeyDown {
245 key: &key,
246 modifiers,
247 editor_line: None,
248 holder: None,
249 }],
250 };
251
252 let mut changed = false;
253
254 for event in events {
255 changed |= editor.process(font_size, &font_family, event);
256 }
257
258 if changed {
259 editor.scroll_to_cursor(scroll_controller, line_height);
260 }
261
262 changed
263 });
264 }
265 };
266
267 let on_global_pointer_up = {
268 let mut editor = editor.clone();
269 let font_family = font_family.clone();
270 move |_: Event<PointerEventData>| {
271 editor.write_if(|mut editor| {
272 editor.process(font_size, &font_family, EditableEvent::Release)
273 });
274 }
275 };
276
277 let gutter_editor = editor.clone();
278 let gutter_theme = theme.clone();
279
280 let on_sized = {
281 let mut editor = editor.clone();
282 move |e: Event<SizedEventData>| {
283 editor.write_if(|mut editor| {
284 let changed = editor.viewport != e.area.size;
285 editor.viewport = e.area.size;
286 changed
287 });
288 }
289 };
290
291 rect()
292 .a11y_auto_focus(a11y_auto_focus)
293 .a11y_focusable(true)
294 .a11y_id(a11y_id)
295 .a11y_role(AccessibilityRole::TextInput)
296 .expanded()
297 .background(theme.background)
298 .maybe(!read_only, |el| {
299 el.on_key_down(on_key_down).on_key_up(on_key_up)
300 })
301 .on_global_pointer_up(on_global_pointer_up)
302 .child(
303 rect()
304 .horizontal()
305 .maybe_child(gutter.then(|| {
306 let gutter_width = font_size * 5.0;
307 VirtualScrollView::new(move |item, _| {
308 EditorGutterLineUI {
309 editor: gutter_editor.clone(),
310 font_size,
311 line_height,
312 line_index: item.index,
313 theme: gutter_theme.clone(),
314 }
315 .into()
316 })
317 .width(Size::px(gutter_width))
318 .show_scrollbar(false)
319 .scroll_controller(scroll_controller)
320 .length(lines_len)
321 .item_size(line_height)
322 }))
323 .child(
324 VirtualScrollView::new(move |item, _| {
325 EditorLineUI {
326 editor: editor.clone(),
327 font_size,
328 line_height,
329 line_index: item.index,
330 read_only,
331 show_whitespace,
332 font_family: font_family.clone(),
333 cursor_mode,
334 theme: theme.clone(),
335 a11y_id,
336 scroll_controller,
337 }
338 .into()
339 })
340 .expanded()
341 .scroll_controller(scroll_controller)
342 .on_sized(on_sized)
343 .length(lines_len)
344 .item_size(line_height),
345 ),
346 )
347 }
348}