1use std::borrow::Cow;
2
3use freya_components::{
4 get_theme_or_default,
5 scrollviews::{
6 ScrollController,
7 ScrollEvent,
8 VirtualScrollView,
9 },
10};
11use freya_core::prelude::*;
12use freya_edit::EditableEvent;
13
14use crate::{
15 editor_data::CodeEditorData,
16 editor_line::EditorLineUI,
17 editor_theme::{
18 EditorTheme,
19 EditorThemePartial,
20 EditorThemePreference,
21 },
22};
23
24#[derive(PartialEq, Clone)]
25pub struct CodeEditor {
26 editor: Writable<CodeEditorData>,
27 font_size: f32,
28 line_height: f32,
29 read_only: bool,
30 gutter: bool,
31 show_whitespace: bool,
32 font_family: Cow<'static, str>,
33 a11y_id: AccessibilityId,
34 a11y_auto_focus: bool,
35 pub(crate) theme: Option<EditorThemePartial>,
36 on_pre_key_down: Callback<Event<KeyboardEventData>, bool>,
37}
38
39impl CodeEditor {
40 pub fn new(editor: impl Into<Writable<CodeEditorData>>, a11y_id: AccessibilityId) -> Self {
44 Self {
45 editor: editor.into(),
46 font_size: 14.0,
47 line_height: 1.4,
48 read_only: false,
49 gutter: true,
50 show_whitespace: true,
51 font_family: Cow::Borrowed("Jetbrains Mono"),
52 a11y_id,
53 a11y_auto_focus: false,
54 theme: None,
55 on_pre_key_down: Callback::new(|e: Event<KeyboardEventData>| {
56 e.stop_propagation();
57 if let Key::Named(NamedKey::Tab) = &e.key {
58 e.prevent_default();
59 }
60 true
61 }),
62 }
63 }
64
65 pub fn font_size(mut self, size: f32) -> Self {
66 self.font_size = size;
67 self
68 }
69
70 pub fn line_height(mut self, height: f32) -> Self {
72 self.line_height = height;
73 self
74 }
75
76 pub fn read_only(mut self, read_only: bool) -> Self {
78 self.read_only = read_only;
79 self
80 }
81
82 pub fn gutter(mut self, gutter: bool) -> Self {
84 self.gutter = gutter;
85 self
86 }
87
88 pub fn show_whitespace(mut self, show_whitespace: bool) -> Self {
90 self.show_whitespace = show_whitespace;
91 self
92 }
93
94 pub fn font_family(mut self, font_family: impl Into<Cow<'static, str>>) -> Self {
96 self.font_family = font_family.into();
97 self
98 }
99
100 pub fn a11y_auto_focus(mut self, a11y_auto_focus: bool) -> Self {
102 self.a11y_auto_focus = a11y_auto_focus;
103 self
104 }
105
106 pub fn on_pre_key_down(
109 mut self,
110 on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
111 ) -> Self {
112 self.on_pre_key_down = on_pre_key_down.into();
113 self
114 }
115}
116
117impl Component for CodeEditor {
118 fn render(&self) -> impl IntoElement {
119 let CodeEditor {
120 editor,
121 font_size,
122 line_height,
123 read_only,
124 gutter,
125 show_whitespace,
126 font_family,
127 a11y_id,
128 a11y_auto_focus,
129 theme,
130 on_pre_key_down,
131 } = self.clone();
132
133 let theme = get_theme_or_default!(&theme, EditorThemePreference, "code_editor", || {
134 EditorTheme::light().into()
135 });
136
137 let editor_data = editor.read();
138
139 let scroll_controller = use_hook(|| {
140 let notifier = State::create(());
141 let requests = State::create(vec![]);
142 ScrollController::managed(
143 notifier,
144 requests,
145 State::create(Callback::new({
146 let mut editor = editor.clone();
147 move |ev| {
148 editor.write_if(|mut editor| {
149 let current = editor.scrolls;
150 match ev {
151 ScrollEvent::X(x) => {
152 editor.scrolls.0 = x;
153 }
154 ScrollEvent::Y(y) => {
155 editor.scrolls.1 = y;
156 }
157 }
158 current != editor.scrolls
159 })
160 }
161 })),
162 State::create(Callback::new({
163 let editor = editor.clone();
164 move |_| {
165 let editor = editor.read();
166 editor.scrolls
167 }
168 })),
169 )
170 });
171
172 let line_height = (font_size * line_height).floor();
173 let lines_len = editor_data.metrics.syntax_blocks.len();
174
175 let on_key_up = {
176 let mut editor = editor.clone();
177 let font_family = font_family.clone();
178 move |e: Event<KeyboardEventData>| {
179 editor.write_if(|mut editor| {
180 editor.process(
181 font_size,
182 &font_family,
183 EditableEvent::KeyUp { key: &e.key },
184 )
185 });
186 }
187 };
188
189 let on_key_down = {
190 let mut editor = editor.clone();
191 let font_family = font_family.clone();
192 move |e: Event<KeyboardEventData>| {
193 const LINES_JUMP_ALT: usize = 5;
194 const LINES_JUMP_CONTROL: usize = 3;
195
196 let key = e.key.clone();
197 let modifiers = e.modifiers;
198
199 if !on_pre_key_down.call(e) {
200 return;
201 }
202
203 editor.write_if(|mut editor| {
204 let lines_jump = (line_height * LINES_JUMP_ALT as f32).ceil() as i32;
205 let min_height = -(lines_len as f32 * line_height) as i32;
206 let max_height = 0; let current_scroll = editor.scrolls.1;
208
209 let events = match &key {
210 Key::Named(NamedKey::ArrowUp) if modifiers.contains(Modifiers::ALT) => {
211 let jump = (current_scroll + lines_jump).clamp(min_height, max_height);
212 editor.scrolls.1 = jump;
213 (0..LINES_JUMP_ALT)
214 .map(|_| EditableEvent::KeyDown {
215 key: &key,
216 modifiers,
217 editor_line: None,
218 holder: None,
219 })
220 .collect::<Vec<EditableEvent>>()
221 }
222 Key::Named(NamedKey::ArrowDown) if modifiers.contains(Modifiers::ALT) => {
223 let jump = (current_scroll - lines_jump).clamp(min_height, max_height);
224 editor.scrolls.1 = jump;
225 (0..LINES_JUMP_ALT)
226 .map(|_| EditableEvent::KeyDown {
227 key: &key,
228 modifiers,
229 editor_line: None,
230 holder: None,
231 })
232 .collect::<Vec<EditableEvent>>()
233 }
234 Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
235 if modifiers.contains(Modifiers::CONTROL) =>
236 {
237 (0..LINES_JUMP_CONTROL)
238 .map(|_| EditableEvent::KeyDown {
239 key: &key,
240 modifiers,
241 editor_line: None,
242 holder: None,
243 })
244 .collect::<Vec<EditableEvent>>()
245 }
246 _ => vec![EditableEvent::KeyDown {
247 key: &key,
248 modifiers,
249 editor_line: None,
250 holder: None,
251 }],
252 };
253
254 let mut changed = false;
255
256 for event in events {
257 changed |= editor.process(font_size, &font_family, event);
258 }
259
260 changed
261 });
262 }
263 };
264
265 let on_global_pointer_press = {
266 let mut editor = editor.clone();
267 let font_family = font_family.clone();
268 move |_: Event<PointerEventData>| {
269 editor.write_if(|mut editor_editor| {
270 editor_editor.process(font_size, &font_family, EditableEvent::Release)
271 });
272 }
273 };
274
275 rect()
276 .a11y_auto_focus(a11y_auto_focus)
277 .a11y_focusable(true)
278 .a11y_id(a11y_id)
279 .a11y_role(AccessibilityRole::TextInput)
280 .expanded()
281 .background(theme.background)
282 .maybe(!read_only, |el| {
283 el.on_key_down(on_key_down).on_key_up(on_key_up)
284 })
285 .on_global_pointer_press(on_global_pointer_press)
286 .child(
287 VirtualScrollView::new(move |item, _| {
288 EditorLineUI {
289 editor: editor.clone(),
290 font_size,
291 line_height,
292 line_index: item.index,
293 read_only,
294 gutter,
295 show_whitespace,
296 font_family: font_family.clone(),
297 theme: theme.clone(),
298 a11y_id,
299 }
300 .into()
301 })
302 .scroll_controller(scroll_controller)
303 .length(lines_len)
304 .item_size(line_height),
305 )
306 }
307}