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 scroll_controller = use_hook(|| {
119 let notifier = State::create(());
120 let requests = State::create(vec![]);
121 ScrollController::managed(
122 notifier,
123 requests,
124 State::create(Callback::new({
125 let mut editor = editor.clone();
126 move |ev| {
127 editor.write_if(|mut editor| {
128 let current = editor.scrolls;
129 match ev {
130 ScrollEvent::X(x) => {
131 editor.scrolls.0 = x;
132 }
133 ScrollEvent::Y(y) => {
134 editor.scrolls.1 = y;
135 }
136 }
137 current != editor.scrolls
138 })
139 }
140 })),
141 State::create(Callback::new({
142 let editor = editor.clone();
143 move |_| {
144 let editor = editor.read();
145 editor.scrolls
146 }
147 })),
148 )
149 });
150
151 let line_height = (font_size * line_height).floor();
152 let lines_len = editor_data.metrics.syntax_blocks.len();
153
154 let on_pointer_down = move |e: Event<PointerEventData>| {
155 e.prevent_default();
156 e.stop_propagation();
157 a11y_id.request_focus();
158 };
159
160 let on_key_up = {
161 let mut editor = editor.clone();
162 let font_family = font_family.clone();
163 move |e: Event<KeyboardEventData>| {
164 editor.write_if(|mut editor| {
165 editor.process(
166 font_size,
167 &font_family,
168 EditableEvent::KeyUp { key: &e.key },
169 )
170 });
171 }
172 };
173
174 let on_key_down = {
175 let mut editor = editor.clone();
176 let font_family = font_family.clone();
177 move |e: Event<KeyboardEventData>| {
178 e.stop_propagation();
179
180 if let Key::Named(NamedKey::Tab) = &e.key {
181 e.prevent_default();
182 }
183
184 const LINES_JUMP_ALT: usize = 5;
185 const LINES_JUMP_CONTROL: usize = 3;
186
187 editor.write_if(|mut editor| {
188 let lines_jump = (line_height * LINES_JUMP_ALT as f32).ceil() as i32;
189 let min_height = -(lines_len as f32 * line_height) as i32;
190 let max_height = 0; let current_scroll = editor.scrolls.1;
192
193 let events = match &e.key {
194 Key::Named(NamedKey::ArrowUp) if e.modifiers.contains(Modifiers::ALT) => {
195 let jump = (current_scroll + lines_jump).clamp(min_height, max_height);
196 editor.scrolls.1 = jump;
197 (0..LINES_JUMP_ALT)
198 .map(|_| EditableEvent::KeyDown {
199 key: &e.key,
200 modifiers: e.modifiers,
201 })
202 .collect::<Vec<EditableEvent>>()
203 }
204 Key::Named(NamedKey::ArrowDown) if e.modifiers.contains(Modifiers::ALT) => {
205 let jump = (current_scroll - lines_jump).clamp(min_height, max_height);
206 editor.scrolls.1 = jump;
207 (0..LINES_JUMP_ALT)
208 .map(|_| EditableEvent::KeyDown {
209 key: &e.key,
210 modifiers: e.modifiers,
211 })
212 .collect::<Vec<EditableEvent>>()
213 }
214 Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
215 if e.modifiers.contains(Modifiers::CONTROL) =>
216 {
217 (0..LINES_JUMP_CONTROL)
218 .map(|_| EditableEvent::KeyDown {
219 key: &e.key,
220 modifiers: e.modifiers,
221 })
222 .collect::<Vec<EditableEvent>>()
223 }
224 _ if e.code == Code::Escape
225 || e.modifiers.contains(Modifiers::ALT)
226 || (e.modifiers.contains(Modifiers::CONTROL)
227 && e.code == Code::KeyS) =>
228 {
229 Vec::new()
230 }
231 _ => {
232 vec![EditableEvent::KeyDown {
233 key: &e.key,
234 modifiers: e.modifiers,
235 }]
236 }
237 };
238
239 let mut changed = false;
240
241 for event in events {
242 changed |= editor.process(font_size, &font_family, event);
243 }
244
245 changed
246 });
247 }
248 };
249
250 let on_global_pointer_press = {
251 let mut editor = editor.clone();
252 let font_family = font_family.clone();
253 move |_: Event<PointerEventData>| {
254 editor.write_if(|mut editor_editor| {
255 editor_editor.process(font_size, &font_family, EditableEvent::Release)
256 });
257 }
258 };
259
260 rect()
261 .a11y_auto_focus(a11y_auto_focus)
262 .a11y_focusable(true)
263 .a11y_id(a11y_id)
264 .a11y_role(AccessibilityRole::TextInput)
265 .expanded()
266 .background(theme.read().background)
267 .maybe(!read_only, |el| {
268 el.on_key_down(on_key_down).on_key_up(on_key_up)
269 })
270 .on_global_pointer_press(on_global_pointer_press)
271 .on_pointer_down(on_pointer_down)
272 .child(
273 VirtualScrollView::new(move |line_index, _| {
274 EditorLineUI {
275 editor: editor.clone(),
276 font_size,
277 line_height,
278 line_index,
279 read_only,
280 gutter,
281 show_whitespace,
282 font_family: font_family.clone(),
283 theme: theme.clone(),
284 }
285 .into()
286 })
287 .scroll_controller(scroll_controller)
288 .length(lines_len)
289 .item_size(line_height),
290 )
291 }
292}