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