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