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