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// ---------------------------------------------------------------------------