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