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