1use parley::editing::{Generation, PlainEditor, PlainEditorDriver};
4use parley::{FontContext, LayoutContext, StyleProperty};
5use peniko::Brush;
6use std::time::Duration;
7
8#[cfg(target_arch = "wasm32")]
10use web_time::Instant;
11#[cfg(not(target_arch = "wasm32"))]
12use std::time::Instant;
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum TextKey {
17 Character(String),
18 Backspace,
19 Delete,
20 Enter,
21 Left,
22 Right,
23 Up,
24 Down,
25 Home,
26 End,
27 Escape,
28}
29
30#[derive(Debug, Clone, Copy, Default)]
32pub struct TextModifiers {
33 pub shift: bool,
34 pub ctrl: bool,
35 pub alt: bool,
36 pub meta: bool,
37}
38
39impl TextModifiers {
40 pub fn action_mod(&self) -> bool {
42 if cfg!(target_os = "macos") {
43 self.meta
44 } else {
45 self.ctrl
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum TextEditResult {
53 Handled,
55 ExitEdit,
57 NotHandled,
59}
60
61pub struct TextEditState {
63 editor: PlainEditor<Brush>,
65 cursor_visible: bool,
67 start_time: Option<Instant>,
69 blink_period: Duration,
71 is_dragging: bool,
73 cached_width: f32,
75 cached_height: f32,
77}
78
79impl TextEditState {
80 pub fn new(text: &str, font_size: f32) -> Self {
82 use parley::GenericFamily;
83
84 let mut editor = PlainEditor::new(font_size);
85 editor.set_text(text);
86 editor.set_scale(1.0);
87
88 let styles = editor.edit_styles();
91 styles.insert(GenericFamily::SansSerif.into());
92 styles.insert(StyleProperty::Brush(Brush::Solid(peniko::Color::BLACK)));
93
94 Self {
95 editor,
96 cursor_visible: true,
97 start_time: None,
98 blink_period: Duration::ZERO,
99 is_dragging: false,
100 cached_width: 0.0,
101 cached_height: 0.0,
102 }
103 }
104
105 pub fn editor_mut(&mut self) -> &mut PlainEditor<Brush> {
107 &mut self.editor
108 }
109
110 pub fn editor(&self) -> &PlainEditor<Brush> {
112 &self.editor
113 }
114
115 pub fn driver<'a>(
117 &'a mut self,
118 font_cx: &'a mut FontContext,
119 layout_cx: &'a mut LayoutContext<Brush>,
120 ) -> PlainEditorDriver<'a, Brush> {
121 self.editor.driver(font_cx, layout_cx)
122 }
123
124 pub fn text(&self) -> String {
126 self.editor.text().to_string()
127 }
128
129 pub fn set_text(&mut self, text: &str) {
131 self.editor.set_text(text);
132 }
133
134 pub fn set_brush(&mut self, brush: Brush) {
136 let styles = self.editor.edit_styles();
137 styles.insert(StyleProperty::Brush(brush));
138 }
139
140 pub fn set_font_size(&mut self, size: f32) {
142 let styles = self.editor.edit_styles();
143 styles.insert(StyleProperty::FontSize(size));
144 }
145
146 pub fn set_width(&mut self, width: Option<f32>) {
148 self.editor.set_width(width);
149 }
150
151 pub fn cursor_reset(&mut self) {
153 self.start_time = Some(Instant::now());
154 self.blink_period = Duration::from_millis(500);
155 self.cursor_visible = true;
156 }
157
158 pub fn disable_blink(&mut self) {
160 self.start_time = None;
161 }
162
163 pub fn next_blink_time(&self) -> Option<Instant> {
165 self.start_time.map(|start_time| {
166 let phase = Instant::now().duration_since(start_time);
167 start_time
168 + Duration::from_nanos(
169 ((phase.as_nanos() / self.blink_period.as_nanos() + 1)
170 * self.blink_period.as_nanos()) as u64,
171 )
172 })
173 }
174
175 pub fn cursor_blink(&mut self) {
177 self.cursor_visible = self.start_time.is_some_and(|start_time| {
178 let elapsed = Instant::now().duration_since(start_time);
179 (elapsed.as_millis() / self.blink_period.as_millis()) % 2 == 0
180 });
181 }
182
183 pub fn is_cursor_visible(&self) -> bool {
185 self.cursor_visible
186 }
187
188 pub fn generation(&self) -> Generation {
190 self.editor.generation()
191 }
192
193 pub fn is_composing(&self) -> bool {
195 self.editor.is_composing()
196 }
197
198 pub fn layout_size(&self) -> (f32, f32) {
200 (self.cached_width, self.cached_height)
201 }
202
203 pub fn update_layout_cache(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext<Brush>) {
205 let layout = self.editor.layout(font_cx, layout_cx);
206 self.cached_width = layout.width();
207 self.cached_height = layout.height();
208 }
209
210 pub fn handle_key(
213 &mut self,
214 key: TextKey,
215 modifiers: TextModifiers,
216 font_cx: &mut FontContext,
217 layout_cx: &mut LayoutContext<Brush>,
218 ) -> TextEditResult {
219 if self.editor.is_composing() {
221 return TextEditResult::NotHandled;
222 }
223
224 self.cursor_reset();
225 let action_mod = modifiers.action_mod();
226 let shift = modifiers.shift;
227
228 let mut drv = self.editor.driver(font_cx, layout_cx);
229
230 match key {
231 TextKey::Escape => {
232 return TextEditResult::ExitEdit;
233 }
234 TextKey::Backspace => {
235 if action_mod {
236 drv.backdelete_word();
237 } else {
238 drv.backdelete();
239 }
240 }
241 TextKey::Delete => {
242 if action_mod {
243 drv.delete_word();
244 } else {
245 drv.delete();
246 }
247 }
248 TextKey::Enter => {
249 drv.insert_or_replace_selection("\n");
250 }
251 TextKey::Left => {
252 if action_mod {
253 if shift { drv.select_word_left(); } else { drv.move_word_left(); }
254 } else if shift {
255 drv.select_left();
256 } else {
257 drv.move_left();
258 }
259 }
260 TextKey::Right => {
261 if action_mod {
262 if shift { drv.select_word_right(); } else { drv.move_word_right(); }
263 } else if shift {
264 drv.select_right();
265 } else {
266 drv.move_right();
267 }
268 }
269 TextKey::Up => {
270 if shift { drv.select_up(); } else { drv.move_up(); }
271 }
272 TextKey::Down => {
273 if shift { drv.select_down(); } else { drv.move_down(); }
274 }
275 TextKey::Home => {
276 if action_mod {
277 if shift { drv.select_to_text_start(); } else { drv.move_to_text_start(); }
278 } else if shift {
279 drv.select_to_line_start();
280 } else {
281 drv.move_to_line_start();
282 }
283 }
284 TextKey::End => {
285 if action_mod {
286 if shift { drv.select_to_text_end(); } else { drv.move_to_text_end(); }
287 } else if shift {
288 drv.select_to_line_end();
289 } else {
290 drv.move_to_line_end();
291 }
292 }
293 TextKey::Character(ref c) => {
294 if action_mod && (c == "a" || c == "A") {
296 if shift {
297 drv.collapse_selection();
298 } else {
299 drv.select_all();
300 }
301 } else if !action_mod {
302 drv.insert_or_replace_selection(c);
304 }
305 }
306 }
307
308 drop(drv);
310 self.update_layout_cache(font_cx, layout_cx);
311
312 TextEditResult::Handled
313 }
314
315 pub fn handle_mouse_down(
317 &mut self,
318 local_x: f32,
319 local_y: f32,
320 shift: bool,
321 font_cx: &mut FontContext,
322 layout_cx: &mut LayoutContext<Brush>,
323 ) {
324 self.cursor_reset();
325 self.is_dragging = true;
326
327 let mut drv = self.editor.driver(font_cx, layout_cx);
328 if shift {
329 drv.extend_selection_to_point(local_x, local_y);
330 } else {
331 drv.move_to_point(local_x, local_y);
332 }
333 }
334
335 pub fn handle_mouse_drag(
337 &mut self,
338 local_x: f32,
339 local_y: f32,
340 font_cx: &mut FontContext,
341 layout_cx: &mut LayoutContext<Brush>,
342 ) {
343 if !self.is_dragging {
344 return;
345 }
346
347 self.cursor_reset();
348 let mut drv = self.editor.driver(font_cx, layout_cx);
349 drv.extend_selection_to_point(local_x, local_y);
350 }
351
352 pub fn handle_mouse_up(&mut self) {
354 self.is_dragging = false;
355 }
356
357 pub fn is_dragging(&self) -> bool {
359 self.is_dragging
360 }
361
362 pub fn handle_double_click(
364 &mut self,
365 local_x: f32,
366 local_y: f32,
367 font_cx: &mut FontContext,
368 layout_cx: &mut LayoutContext<Brush>,
369 ) {
370 self.cursor_reset();
371 let mut drv = self.editor.driver(font_cx, layout_cx);
372 drv.select_word_at_point(local_x, local_y);
373 }
374
375 pub fn handle_triple_click(
377 &mut self,
378 local_x: f32,
379 local_y: f32,
380 font_cx: &mut FontContext,
381 layout_cx: &mut LayoutContext<Brush>,
382 ) {
383 self.cursor_reset();
384 let mut drv = self.editor.driver(font_cx, layout_cx);
385 drv.select_hard_line_at_point(local_x, local_y);
386 }
387}
388
389impl Default for TextEditState {
390 fn default() -> Self {
391 Self::new("", 32.0)
392 }
393}