drafftink_render/
text_editor.rs

1//! Text editing state using Parley's PlainEditor.
2
3use parley::editing::{Generation, PlainEditor, PlainEditorDriver};
4use parley::{FontContext, LayoutContext, StyleProperty};
5use peniko::Brush;
6use std::time::Duration;
7
8// Use web_time for WASM compatibility
9#[cfg(target_arch = "wasm32")]
10use web_time::Instant;
11#[cfg(not(target_arch = "wasm32"))]
12use std::time::Instant;
13
14/// Keyboard key for text editing.
15#[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/// Keyboard modifiers.
31#[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    /// Get the action modifier (Ctrl on Windows/Linux, Cmd on macOS).
41    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/// Result of handling a text editing event.
51#[derive(Debug, Clone, PartialEq)]
52pub enum TextEditResult {
53    /// Event was handled, text may have changed.
54    Handled,
55    /// Event was handled, user wants to exit editing.
56    ExitEdit,
57    /// Event was not handled (pass to other handlers).
58    NotHandled,
59}
60
61/// Text editor state for a single text shape being edited.
62pub struct TextEditState {
63    /// The Parley PlainEditor for handling text editing.
64    editor: PlainEditor<Brush>,
65    /// Whether the cursor is currently visible (for blinking).
66    cursor_visible: bool,
67    /// Start time for cursor blinking.
68    start_time: Option<Instant>,
69    /// Blink period.
70    blink_period: Duration,
71    /// Whether a mouse drag is in progress for selection.
72    is_dragging: bool,
73    /// Cached layout width for bounds calculation.
74    cached_width: f32,
75    /// Cached layout height for bounds calculation.
76    cached_height: f32,
77}
78
79impl TextEditState {
80    /// Create a new text edit state with the given text content.
81    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        // Set default styles - use SansSerif generic family
89        // The renderer will set the specific font (GelPen) via styles
90        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    /// Get a mutable reference to the PlainEditor.
106    pub fn editor_mut(&mut self) -> &mut PlainEditor<Brush> {
107        &mut self.editor
108    }
109
110    /// Get a reference to the PlainEditor.
111    pub fn editor(&self) -> &PlainEditor<Brush> {
112        &self.editor
113    }
114
115    /// Create a driver for performing edit operations.
116    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    /// Get the current text content.
125    pub fn text(&self) -> String {
126        self.editor.text().to_string()
127    }
128
129    /// Set the text content.
130    pub fn set_text(&mut self, text: &str) {
131        self.editor.set_text(text);
132    }
133
134    /// Set the text brush color.
135    pub fn set_brush(&mut self, brush: Brush) {
136        let styles = self.editor.edit_styles();
137        styles.insert(StyleProperty::Brush(brush));
138    }
139
140    /// Set the font size.
141    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    /// Set the text width constraint.
147    pub fn set_width(&mut self, width: Option<f32>) {
148        self.editor.set_width(width);
149    }
150
151    /// Reset cursor to visible state and start blinking.
152    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    /// Disable cursor blinking.
159    pub fn disable_blink(&mut self) {
160        self.start_time = None;
161    }
162
163    /// Calculate the next blink time.
164    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    /// Update cursor visibility based on blink state.
176    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    /// Check if cursor should be visible.
184    pub fn is_cursor_visible(&self) -> bool {
185        self.cursor_visible
186    }
187
188    /// Get the current generation (for change detection).
189    pub fn generation(&self) -> Generation {
190        self.editor.generation()
191    }
192
193    /// Check if the editor is composing (IME active).
194    pub fn is_composing(&self) -> bool {
195        self.editor.is_composing()
196    }
197
198    /// Get cached layout dimensions.
199    pub fn layout_size(&self) -> (f32, f32) {
200        (self.cached_width, self.cached_height)
201    }
202
203    /// Update cached layout dimensions from the current layout.
204    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    /// Handle a key press event.
211    /// Returns whether the event was handled and if editing should exit.
212    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        // Don't process keys while composing (IME)
220        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                // Handle Ctrl+A for select all
295                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                    // Insert character (including space)
303                    drv.insert_or_replace_selection(c);
304                }
305            }
306        }
307
308        // Update layout cache after changes
309        drop(drv);
310        self.update_layout_cache(font_cx, layout_cx);
311
312        TextEditResult::Handled
313    }
314
315    /// Handle mouse press at the given local coordinates (relative to text position).
316    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    /// Handle mouse drag at the given local coordinates.
336    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    /// Handle mouse release.
353    pub fn handle_mouse_up(&mut self) {
354        self.is_dragging = false;
355    }
356
357    /// Check if a drag is in progress.
358    pub fn is_dragging(&self) -> bool {
359        self.is_dragging
360    }
361
362    /// Handle double-click to select word.
363    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    /// Handle triple-click to select line.
376    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}