Skip to main content

agg_gui/widgets/
text_field.rs

1//! `TextField` — single-line editable text input.
2//!
3//! See [`text_field_core`](super::text_field_core) for the internal helpers,
4//! shared edit state, and undo command type.
5//!
6//! Feature set mirrors C# agg-sharp `InternalTextEditWidget`:
7//! - Character / word navigation (arrows, Ctrl+arrow, Home, End)
8//! - Keyboard selection (Shift+movement), Ctrl+A select-all
9//! - Mouse click to position cursor, drag to extend selection
10//! - Double-click to select the word under the cursor
11//! - Cut / Copy / Paste (Ctrl+X/C/V, Shift+Del, Ctrl/Shift+Ins) — requires
12//!   the `clipboard` crate feature; silently no-ops without it
13//! - Undo / Redo via the shared [`UndoBuffer`](crate::undo::UndoBuffer)
14//! - Blinking cursor (500 ms half-period from the moment focus is gained)
15//! - Horizontal scroll to keep cursor visible
16//! - Placeholder text, read-only mode, SelectAllOnFocus
17//! - Callbacks: on_change, on_enter, on_edit_complete
18
19use std::cell::RefCell;
20use std::rc::Rc;
21use std::sync::Arc;
22
23// web-time provides a WASM-compatible Instant (uses performance.now() in the
24// browser; falls back to Instant on native).
25use web_time::Instant;
26
27use super::text_field_core::{
28    byte_at_x, next_char_boundary, next_word_boundary, prev_char_boundary, prev_word_boundary,
29    word_range_at, TextEditCommand, TextEditState,
30};
31use crate::draw_ctx::DrawCtx;
32use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
33use crate::geometry::{Rect, Size};
34use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
35use crate::text::{measure_advance, Font};
36use crate::undo::UndoBuffer;
37use crate::widget::{BackbufferCache, BackbufferMode, Widget};
38
39// ---------------------------------------------------------------------------
40// Clipboard stubs
41// ---------------------------------------------------------------------------
42
43mod binding;
44mod clipboard;
45mod widget_impl;
46
47use clipboard::{clipboard_get, clipboard_set};
48// ---------------------------------------------------------------------------
49// TextField
50// ---------------------------------------------------------------------------
51
52/// Single-line editable text field.
53pub struct TextField {
54    bounds: Rect,
55    children: Vec<Box<dyn Widget>>,
56    base: WidgetBase,
57
58    // All mutable editing state lives here — shared with undo commands.
59    edit: Rc<RefCell<TextEditState>>,
60
61    // Undo/redo history.
62    undo: UndoBuffer,
63
64    // Pending coalesced insert command (committed when action type changes).
65    pending_insert: Option<TextEditCommand>,
66
67    // Snapshot of text when focus was gained — used to decide if on_edit_complete fires.
68    text_on_focus: String,
69
70    // Font
71    font: Arc<Font>,
72    font_size: f64,
73
74    // Editing options
75    pub read_only: bool,
76    pub select_all_on_focus: bool,
77    /// When `true`, every character is displayed as '•' (U+2022).
78    /// The actual text is stored and edited normally; only the render is masked.
79    pub password_mode: bool,
80
81    // Interaction state
82    focused: bool,
83    hovered: bool,
84    mouse_down: bool,
85    scroll_x: f64,
86
87    // Cursor blink: set to Some(Instant::now()) on FocusGained.
88    focus_time: Option<Instant>,
89    // Blink phase (floor(elapsed_ms / 500)) last drawn by `paint_overlay`.
90    // `needs_draw` compares the current phase against this and reports
91    // dirty when they diverge — i.e. the host-observed time has crossed a
92    // flip boundary since the last paint.  `Cell` so the check can happen
93    // from a `&self` method.  Initialised far out of range so the first
94    // paint after focus always writes the real phase.
95    blink_last_phase: std::cell::Cell<u64>,
96
97    // Double-click detection.
98    last_click_time: Option<Instant>,
99
100    // Content
101    pub placeholder: String,
102
103    // Layout
104    pub padding: f64,
105
106    // Callbacks
107    on_change: Option<Box<dyn FnMut(&str)>>,
108    on_enter: Option<Box<dyn FnMut(&str)>>,
109    on_edit_complete: Option<Box<dyn FnMut(&str)>>,
110    text_cell: Option<Rc<RefCell<String>>>,
111
112    // ── Backbuffer cache ─────────────────────────────────────────────
113    //
114    // Cache holds bg + text + selection + border.  Cursor draws in
115    // `paint_overlay` directly on the outer ctx AFTER the cache blit
116    // so cursor-blink state flips (twice per second) don't invalidate
117    // the cache.  Sig deliberately excludes `blink_visible`.
118    cache: BackbufferCache,
119    last_sig: Option<TextFieldSig>,
120}
121
122#[derive(Clone, PartialEq)]
123struct TextFieldSig {
124    text: String,
125    cursor: usize,
126    anchor: usize,
127    focused: bool,
128    hovered: bool,
129    scroll_x_bits: u64,
130    w_bits: u64,
131    h_bits: u64,
132    // Font identity + size: the cached bitmap was rasterised with a specific
133    // typeface at a specific point size, so any live swap in the System
134    // window (which runs through `font_settings::set_system_font` /
135    // `set_font_size_scale`) must invalidate — otherwise the stale bitmap
136    // keeps blitting until some other field in the sig happens to change
137    // (e.g. the user hovers the control, which flips `hovered`).
138    font_ptr: usize,
139    font_size_bits: u64,
140}
141
142impl TextField {
143    pub fn new(font: Arc<Font>) -> Self {
144        Self {
145            bounds: Rect::default(),
146            children: Vec::new(),
147            base: WidgetBase::new(),
148            edit: Rc::new(RefCell::new(TextEditState::default())),
149            undo: UndoBuffer::new(),
150            pending_insert: None,
151            text_on_focus: String::new(),
152            font,
153            font_size: 14.0,
154            read_only: false,
155            select_all_on_focus: false,
156            password_mode: false,
157            focused: false,
158            hovered: false,
159            mouse_down: false,
160            scroll_x: 0.0,
161            focus_time: None,
162            blink_last_phase: std::cell::Cell::new(u64::MAX),
163            last_click_time: None,
164            placeholder: String::new(),
165            padding: 8.0,
166            on_change: None,
167            on_enter: None,
168            on_edit_complete: None,
169            text_cell: None,
170            cache: BackbufferCache::default(),
171            last_sig: None,
172        }
173    }
174
175    /// Currently-active font — honours the thread-local system-font override
176    /// (`font_settings::current_system_font`) so changes in the System window
177    /// propagate live without a widget-tree rebuild.  Falls back to the font
178    /// passed at construction when no override is set.
179    fn active_font(&self) -> Arc<Font> {
180        crate::font_settings::current_system_font().unwrap_or_else(|| Arc::clone(&self.font))
181    }
182
183    // ── Builder / setter methods ─────────────────────────────────────────────
184
185    pub fn with_font_size(mut self, s: f64) -> Self {
186        self.font_size = s;
187        self
188    }
189    pub fn with_padding(mut self, p: f64) -> Self {
190        self.padding = p;
191        self
192    }
193    pub fn with_read_only(mut self, v: bool) -> Self {
194        self.read_only = v;
195        self
196    }
197    pub fn with_select_all_on_focus(mut self, v: bool) -> Self {
198        self.select_all_on_focus = v;
199        self
200    }
201    pub fn with_password_mode(mut self, v: bool) -> Self {
202        self.password_mode = v;
203        self
204    }
205
206    pub fn with_placeholder(mut self, s: impl Into<String>) -> Self {
207        self.placeholder = s.into();
208        self
209    }
210    pub fn with_text(self, s: impl Into<String>) -> Self {
211        let t = s.into();
212        let len = t.len();
213        let mut st = self.edit.borrow_mut();
214        st.text = t;
215        st.cursor = len;
216        st.anchor = len;
217        drop(st);
218        self
219    }
220
221    pub fn on_change(mut self, cb: impl FnMut(&str) + 'static) -> Self {
222        self.on_change = Some(Box::new(cb));
223        self
224    }
225    pub fn on_enter(mut self, cb: impl FnMut(&str) + 'static) -> Self {
226        self.on_enter = Some(Box::new(cb));
227        self
228    }
229    pub fn on_edit_complete(mut self, cb: impl FnMut(&str) + 'static) -> Self {
230        self.on_edit_complete = Some(Box::new(cb));
231        self
232    }
233
234    pub fn with_margin(mut self, m: Insets) -> Self {
235        self.base.margin = m;
236        self
237    }
238    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
239        self.base.h_anchor = h;
240        self
241    }
242    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
243        self.base.v_anchor = v;
244        self
245    }
246    pub fn with_min_size(mut self, s: Size) -> Self {
247        self.base.min_size = s;
248        self
249    }
250    pub fn with_max_size(mut self, s: Size) -> Self {
251        self.base.max_size = s;
252        self
253    }
254
255    // ── Getters ──────────────────────────────────────────────────────────────
256
257    pub fn text(&self) -> String {
258        self.edit.borrow().text.clone()
259    }
260    pub fn cursor_pos(&self) -> usize {
261        self.edit.borrow().cursor
262    }
263    pub fn selection(&self) -> String {
264        let st = self.edit.borrow();
265        let lo = st.cursor.min(st.anchor);
266        let hi = st.cursor.max(st.anchor);
267        st.text[lo..hi].to_string()
268    }
269
270    pub fn set_text(&mut self, s: impl Into<String>) {
271        let t = s.into();
272        let len = t.len();
273        let mut st = self.edit.borrow_mut();
274        st.text = t.clone();
275        st.cursor = len;
276        st.anchor = len;
277        drop(st);
278        if let Some(cell) = &self.text_cell {
279            *cell.borrow_mut() = t;
280        }
281        self.undo.clear_history();
282        self.pending_insert = None;
283    }
284
285    // ── Private state helpers ────────────────────────────────────────────────
286
287    fn snap(&self) -> TextEditState {
288        self.edit.borrow().clone()
289    }
290    #[allow(dead_code)]
291    fn apply(&self, s: TextEditState) {
292        *self.edit.borrow_mut() = s;
293    }
294
295    #[allow(dead_code)]
296    fn sel_min(&self) -> usize {
297        let s = self.edit.borrow();
298        s.cursor.min(s.anchor)
299    }
300    #[allow(dead_code)]
301    fn sel_max(&self) -> usize {
302        let s = self.edit.borrow();
303        s.cursor.max(s.anchor)
304    }
305    fn has_selection(&self) -> bool {
306        let s = self.edit.borrow();
307        s.cursor != s.anchor
308    }
309
310    /// Commit any pending coalesced insert command to the undo buffer.
311    fn flush_pending(&mut self) {
312        if let Some(cmd) = self.pending_insert.take() {
313            self.undo.add(Box::new(cmd));
314        }
315    }
316
317    /// Convert a pixel x position (in text-local space) to a byte offset in
318    /// `real_text`.  In password mode, measures the masked string and maps back.
319    fn click_to_cursor(&self, real_text: &str, tx: f64) -> usize {
320        let font = self.active_font();
321        if self.password_mode {
322            const BULLET: char = '•';
323            const BULLET_LEN: usize = 3;
324            let n = real_text.chars().count();
325            let masked = BULLET.to_string().repeat(n);
326            let disp = byte_at_x(&font, &masked, self.font_size, tx);
327            // Map masked byte offset → char index → real byte offset.
328            let char_idx = disp / BULLET_LEN;
329            real_text
330                .char_indices()
331                .nth(char_idx)
332                .map(|(i, _)| i)
333                .unwrap_or(real_text.len())
334        } else {
335            byte_at_x(&font, real_text, self.font_size, tx)
336        }
337    }
338
339    /// Scroll `scroll_x` so that the cursor stays visible.
340    fn ensure_cursor_visible(&mut self) {
341        if self.bounds.width < 1.0 {
342            return;
343        }
344        let inner_w = (self.bounds.width - self.padding * 2.0).max(0.0);
345        let font = self.active_font();
346        let cx = {
347            let st = self.edit.borrow();
348            if self.password_mode {
349                const BULLET: char = '•';
350                #[allow(dead_code)]
351                const BULLET_LEN: usize = 3;
352                let n = st.text[..st.cursor].chars().count();
353                let masked = BULLET.to_string().repeat(n);
354                measure_advance(&font, &masked, self.font_size)
355            } else {
356                measure_advance(&font, &st.text[..st.cursor], self.font_size)
357            }
358        };
359        if cx < self.scroll_x {
360            self.scroll_x = cx;
361        } else if cx > self.scroll_x + inner_w {
362            self.scroll_x = cx - inner_w;
363        }
364    }
365
366    // ── Edit operations ──────────────────────────────────────────────────────
367
368    /// Insert `s` at cursor, replacing any selection.
369    /// Consecutive single-char inserts are coalesced into one undo command.
370    fn do_insert(&mut self, s: &str, is_single_char: bool) {
371        let before = self.snap();
372        let had_selection = before.cursor != before.anchor;
373
374        // Apply the change
375        {
376            let mut st = self.edit.borrow_mut();
377            if st.cursor != st.anchor {
378                let lo = st.cursor.min(st.anchor);
379                let hi = st.cursor.max(st.anchor);
380                st.text.drain(lo..hi);
381                st.cursor = lo;
382                st.anchor = lo;
383            }
384            let cursor = st.cursor;
385            st.text.insert_str(cursor, s);
386            st.cursor = cursor + s.len();
387            st.anchor = st.cursor;
388        }
389
390        let after = self.snap();
391
392        if is_single_char && !had_selection {
393            // Extend the pending coalesced command if one exists, otherwise start one.
394            if let Some(ref mut pending) = self.pending_insert {
395                pending.after = after;
396            } else {
397                self.pending_insert = Some(TextEditCommand {
398                    name: "insert text",
399                    before,
400                    after,
401                    target: Rc::clone(&self.edit),
402                });
403            }
404        } else {
405            // Non-char insert (paste) or insert-over-selection: commit pending and push new.
406            self.flush_pending();
407            self.undo.add(Box::new(TextEditCommand {
408                name: "insert text",
409                before,
410                after,
411                target: Rc::clone(&self.edit),
412            }));
413        }
414
415        self.ensure_cursor_visible();
416        self.notify_change();
417    }
418
419    /// Delete the selection (if any) or a single char/word, then push undo.
420    fn do_delete(&mut self, forward: bool, word: bool) {
421        self.flush_pending();
422        let before = self.snap();
423        {
424            let mut st = self.edit.borrow_mut();
425            if st.cursor != st.anchor {
426                let lo = st.cursor.min(st.anchor);
427                let hi = st.cursor.max(st.anchor);
428                st.text.drain(lo..hi);
429                st.cursor = lo;
430                st.anchor = lo;
431            } else if forward {
432                let cursor = st.cursor;
433                let end = if word {
434                    next_word_boundary(&st.text, cursor)
435                } else {
436                    next_char_boundary(&st.text, cursor)
437                };
438                if end > cursor {
439                    st.text.drain(cursor..end);
440                }
441                st.anchor = st.cursor;
442            } else {
443                let cursor = st.cursor;
444                let start = if word {
445                    prev_word_boundary(&st.text, cursor)
446                } else {
447                    prev_char_boundary(&st.text, cursor)
448                };
449                if start < cursor {
450                    st.text.drain(start..cursor);
451                    st.cursor = start;
452                    st.anchor = start;
453                }
454            }
455        }
456        let after = self.snap();
457        self.undo.add(Box::new(TextEditCommand {
458            name: "delete text",
459            before,
460            after,
461            target: Rc::clone(&self.edit),
462        }));
463        self.ensure_cursor_visible();
464        self.notify_change();
465    }
466
467    fn do_undo(&mut self) {
468        self.flush_pending();
469        self.undo.undo();
470        // Clamp positions in case the text changed length.
471        let len = self.edit.borrow().text.len();
472        let mut st = self.edit.borrow_mut();
473        st.cursor = st.cursor.min(len);
474        st.anchor = st.anchor.min(len);
475        drop(st);
476        self.ensure_cursor_visible();
477        self.notify_change();
478    }
479
480    fn do_redo(&mut self) {
481        self.flush_pending();
482        self.undo.redo();
483        let len = self.edit.borrow().text.len();
484        let mut st = self.edit.borrow_mut();
485        st.cursor = st.cursor.min(len);
486        st.anchor = st.anchor.min(len);
487        drop(st);
488        self.ensure_cursor_visible();
489        self.notify_change();
490    }
491
492    // ── Callback dispatchers ─────────────────────────────────────────────────
493
494    fn notify_change(&mut self) {
495        let t = self.text();
496        if let Some(cell) = &self.text_cell {
497            *cell.borrow_mut() = t.clone();
498        }
499        if let Some(mut cb) = self.on_change.take() {
500            cb(&t);
501            self.on_change = Some(cb);
502        }
503    }
504    fn notify_enter(&mut self) {
505        if let Some(mut cb) = self.on_enter.take() {
506            let t = self.text();
507            cb(&t);
508            self.on_enter = Some(cb);
509        }
510    }
511    fn notify_edit_complete(&mut self) {
512        if let Some(mut cb) = self.on_edit_complete.take() {
513            let t = self.text();
514            cb(&t);
515            self.on_edit_complete = Some(cb);
516        }
517    }
518
519    // ── Keyboard handler ─────────────────────────────────────────────────────
520
521    fn handle_key(&mut self, key: &Key, mods: Modifiers) -> EventResult {
522        // Snapshot cursor/anchor before movement so we can keep anchor on Shift.
523        let anchor_before = self.edit.borrow().anchor;
524
525        // Command modifier (clipboard / select-all / undo): `Ctrl` on Windows
526        // and Linux, `Cmd` (meta) on macOS.  Treating the two as equivalent
527        // means the same handler serves both OSes without branching.
528        let cmd = mods.ctrl || mods.meta;
529        // Word-navigation modifier: `Ctrl` on Windows/Linux, `Option`
530        // (alt) on macOS.  Used for Ctrl/Alt+Arrow, Ctrl/Alt+Backspace,
531        // Ctrl/Alt+Delete.
532        let word = mods.ctrl || mods.alt;
533
534        match key {
535            // ── Printable characters (and Ctrl/Cmd shortcuts on Char) ──────
536            Key::Char(c) if !self.read_only || cmd => {
537                if cmd {
538                    return match c {
539                        'a' | 'A' => {
540                            let len = self.edit.borrow().text.len();
541                            let mut st = self.edit.borrow_mut();
542                            st.anchor = 0;
543                            st.cursor = len;
544                            EventResult::Consumed
545                        }
546                        'z' | 'Z' if !mods.shift => {
547                            if !self.read_only {
548                                self.do_undo();
549                            }
550                            EventResult::Consumed
551                        }
552                        'z' | 'Z' | 'y' | 'Y' => {
553                            if !self.read_only {
554                                self.do_redo();
555                            }
556                            EventResult::Consumed
557                        }
558                        'x' | 'X' => {
559                            if !self.read_only && self.has_selection() {
560                                clipboard_set(&self.selection());
561                                self.do_delete(false, false); // delete selection via do_delete
562                            }
563                            EventResult::Consumed
564                        }
565                        'c' | 'C' => {
566                            if self.has_selection() {
567                                clipboard_set(&self.selection());
568                            }
569                            EventResult::Consumed
570                        }
571                        'v' | 'V' => {
572                            if !self.read_only {
573                                if let Some(clip) = clipboard_get() {
574                                    self.do_insert(&clip, false);
575                                }
576                            }
577                            EventResult::Consumed
578                        }
579                        _ => EventResult::Ignored,
580                    };
581                }
582                if self.read_only {
583                    return EventResult::Ignored;
584                }
585                let mut buf = [0u8; 4];
586                let s = c.encode_utf8(&mut buf);
587                self.do_insert(s, true);
588                EventResult::Consumed
589            }
590
591            // ── Insert clipboard shortcuts ────────────────────────────────
592            // Classic Windows bindings (still common on Linux):
593            //   Shift+Insert = Paste
594            //   Ctrl+Insert  = Copy
595            // Plain `Insert` toggles overwrite mode in many editors — we
596            // don't model overwrite, so plain Insert is a no-op here.
597            Key::Insert => {
598                if mods.shift && !self.read_only {
599                    if let Some(clip) = clipboard_get() {
600                        self.do_insert(&clip, false);
601                    }
602                    return EventResult::Consumed;
603                }
604                if cmd {
605                    if self.has_selection() {
606                        clipboard_set(&self.selection());
607                    }
608                    return EventResult::Consumed;
609                }
610                EventResult::Ignored
611            }
612
613            // ── Backspace ─────────────────────────────────────────────────
614            Key::Backspace if !self.read_only => {
615                self.do_delete(false, word);
616                EventResult::Consumed
617            }
618
619            // ── Delete ────────────────────────────────────────────────────
620            Key::Delete if !self.read_only => {
621                if mods.shift {
622                    // Shift+Delete = Cut
623                    if self.has_selection() {
624                        clipboard_set(&self.selection());
625                        self.do_delete(false, false);
626                    }
627                } else {
628                    self.do_delete(true, word);
629                }
630                EventResult::Consumed
631            }
632
633            // ── Arrow Left ────────────────────────────────────────────────
634            // Mac: `Cmd+Left` = start of line (Home behaviour).
635            // Win/Mac: `Ctrl+Left` / `Option+Left` = previous word.
636            // Plain: one character back (or collapse selection to left).
637            Key::ArrowLeft => {
638                self.flush_pending();
639                let (cur, anchor) = {
640                    let st = self.edit.borrow();
641                    (st.cursor, st.anchor)
642                };
643                let new_cur = if mods.meta {
644                    0 // Mac: Cmd+Left = line start
645                } else if !mods.shift && cur != anchor {
646                    cur.min(anchor) // collapse to left
647                } else if word {
648                    prev_word_boundary(&self.edit.borrow().text, cur)
649                } else {
650                    prev_char_boundary(&self.edit.borrow().text, cur)
651                };
652                let new_anchor = if mods.shift { anchor } else { new_cur };
653                let mut st = self.edit.borrow_mut();
654                st.cursor = new_cur;
655                st.anchor = new_anchor;
656                drop(st);
657                if new_cur == 0 {
658                    self.scroll_x = 0.0;
659                }
660                self.ensure_cursor_visible();
661                EventResult::Consumed
662            }
663
664            // ── Arrow Right ───────────────────────────────────────────────
665            // Symmetric with ArrowLeft.  Mac: `Cmd+Right` = end of line.
666            Key::ArrowRight => {
667                self.flush_pending();
668                let text_len = self.edit.borrow().text.len();
669                let (cur, anchor) = {
670                    let st = self.edit.borrow();
671                    (st.cursor, st.anchor)
672                };
673                let new_cur = if mods.meta {
674                    text_len // Mac: Cmd+Right = line end
675                } else if !mods.shift && cur != anchor {
676                    cur.max(anchor) // collapse to right
677                } else if word {
678                    next_word_boundary(&self.edit.borrow().text, cur)
679                } else if cur < text_len {
680                    next_char_boundary(&self.edit.borrow().text, cur)
681                } else {
682                    cur
683                };
684                let new_anchor = if mods.shift { anchor } else { new_cur };
685                let mut st = self.edit.borrow_mut();
686                st.cursor = new_cur;
687                st.anchor = new_anchor;
688                drop(st);
689                self.ensure_cursor_visible();
690                EventResult::Consumed
691            }
692
693            // ── Arrow Up / Down ──────────────────────────────────────────
694            // Single-line field, so vertical arrows only matter for the Mac
695            // `Cmd+Up` / `Cmd+Down` (start / end of document) convention —
696            // treat as Home / End.  Plain arrows fall through so callers
697            // can spin numeric-input-style steppers, etc.
698            Key::ArrowUp if mods.meta => {
699                self.flush_pending();
700                let (_, anchor) = {
701                    let st = self.edit.borrow();
702                    (st.cursor, st.anchor)
703                };
704                let new_cur = 0;
705                let new_anchor = if mods.shift { anchor } else { new_cur };
706                let mut st = self.edit.borrow_mut();
707                st.cursor = new_cur;
708                st.anchor = new_anchor;
709                drop(st);
710                self.scroll_x = 0.0;
711                EventResult::Consumed
712            }
713            Key::ArrowDown if mods.meta => {
714                self.flush_pending();
715                let len = self.edit.borrow().text.len();
716                let (_, anchor) = {
717                    let st = self.edit.borrow();
718                    (st.cursor, st.anchor)
719                };
720                let new_cur = len;
721                let new_anchor = if mods.shift { anchor } else { new_cur };
722                let mut st = self.edit.borrow_mut();
723                st.cursor = new_cur;
724                st.anchor = new_anchor;
725                drop(st);
726                self.ensure_cursor_visible();
727                EventResult::Consumed
728            }
729
730            // ── Home ──────────────────────────────────────────────────────
731            // Ctrl+Home is "start of document" on Windows — for a single-
732            // line field that's the same as plain Home; accept both.
733            Key::Home => {
734                self.flush_pending();
735                let mut st = self.edit.borrow_mut();
736                st.cursor = 0;
737                if !mods.shift {
738                    st.anchor = 0;
739                }
740                drop(st);
741                self.scroll_x = 0.0;
742                EventResult::Consumed
743            }
744
745            // ── End ───────────────────────────────────────────────────────
746            // Ctrl+End analogous to Ctrl+Home — treated as plain End here.
747            Key::End => {
748                self.flush_pending();
749                let len = self.edit.borrow().text.len();
750                let mut st = self.edit.borrow_mut();
751                st.cursor = len;
752                if !mods.shift {
753                    st.anchor = len;
754                }
755                drop(st);
756                self.ensure_cursor_visible();
757                EventResult::Consumed
758            }
759
760            // ── Enter ─────────────────────────────────────────────────────
761            // Commit as edit-complete too so numeric/parsed fields apply the
762            // value on Enter (not only on blur).  Snapshot text to prevent a
763            // second edit-complete firing on later focus loss.
764            Key::Enter => {
765                self.flush_pending();
766                self.notify_enter();
767                if self.text() != self.text_on_focus {
768                    self.notify_edit_complete();
769                    self.text_on_focus = self.text();
770                }
771                EventResult::Consumed
772            }
773
774            // ── Escape: clear selection ───────────────────────────────────
775            Key::Escape => {
776                self.flush_pending();
777                let cur = self.edit.borrow().cursor;
778                self.edit.borrow_mut().anchor = cur;
779                EventResult::Consumed
780            }
781
782            _ => {
783                let _ = anchor_before;
784                EventResult::Ignored
785            }
786        }
787    }
788}
789
790// ---------------------------------------------------------------------------
791// Widget impl
792// ---------------------------------------------------------------------------