Skip to main content

teksilo_widgets/primitives/
text_input_field.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `TextInputField` — editable single-line text surface primitive.
5//!
6//! This is the raw editing primitive that powers the styled
7//! [`TextInput`](crate::text_input::TextInput) composite and any
8//! other widget that needs inline editable text — [`SpinBox`] being
9//! the primary second consumer.
10//!
11//! Unlike `TextInput`, `TextInputField` paints no frame, no
12//! placeholder overlay, no validation border, and hosts no trailing
13//! slots: it is the focusable text area only. Compose it yourself
14//! with `RectWidget`, `Padding`, icons, clear buttons, etc. to
15//! build a styled control. Focus indication is the composite's
16//! responsibility — the Int UI convention is to thicken the
17//! enclosing frame's border to `focus_ring_width` and recolor it
18//! to the accent focus-ring color.
19//!
20//! Features:
21//! - Bound `Signal<String>` for two-way text binding.
22//! - Full keyboard editing (arrow keys, Home/End, Backspace/Delete,
23//!   Ctrl+X/C/V, Ctrl+A, Ctrl+Z/Y), IME commit, and pointer caret
24//!   positioning and drag-select.
25//! - Optional per-character input filter
26//!   ([`TextInputField::char_filter`]), max-length cap
27//!   ([`TextInputField::max_length`]), and read-only mode
28//!   ([`TextInputField::read_only`]).
29//! - Commit hooks: Enter fires
30//!   [`on_submit_fn`](TextInputField::on_submit_fn) and focus loss
31//!   fires [`on_blur_fn`](TextInputField::on_blur_fn).
32//! - Non-editable trailing
33//!   [`suffix`](TextInputField::suffix), rendered flush-right inside
34//!   the field's bounds (Qt's `QSpinBox::suffix`). Caret cannot
35//!   enter it; clicks past the text end clamp to the last
36//!   character.
37//! - Appearance: [`text_style`](TextInputField::text_style) shapes the
38//!   text in a `TextStyleRole` or a `TextStyle` (size, family, weight) and
39//!   sizes the field to one line of it;
40//!   [`text_alignment`](TextInputField::text_alignment) places a text
41//!   narrower than the field at its start, centre or end;
42//!   [`text_color`](TextInputField::text_color) colours the text and the
43//!   caret, from a colour, a role or a signal. All three are unset by
44//!   default, and the field then looks as it always has.
45//! - Right-click context menu (Cut / Copy / Paste / Select All).
46//! - AccessKit `Role::TextInput` with value, selection, and
47//!   character/word boundary metadata.
48//!
49//! # Example
50//!
51//! ```ignore
52//! let text = ctx.signal(String::new());
53//! ctx.add(
54//!     TextInputField::new(text.clone())
55//!         .placeholder("Enter a name…")
56//!         .char_filter(|c| !c.is_ascii_digit())
57//!         .on_submit_fn(|ctx| ctx.send_intent(MyIntent::Save)),
58//! );
59//! ```
60//!
61//! [`SpinBox`]: crate::spin_box::SpinBox
62
63mod keyboard;
64pub mod mask;
65mod mouse;
66pub(crate) mod state;
67pub(crate) mod touch;
68pub mod validator;
69mod widget_impl;
70
71use std::rc::Rc;
72use teksilo_i18n::tr_widget;
73
74use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
75use teksilo_core::accessibility::AccessNodeBuilder;
76use teksilo_core::accessibility::text_runs::{
77    RetainedText, TextRunEmission, TextRunSource, push_text_runs,
78};
79use teksilo_core::build_context::BuildContext;
80use teksilo_core::color_prop::{ColorProp, TextStyleProp};
81use teksilo_core::event::EventResponse;
82use teksilo_core::shortcut::StandardEdit;
83use teksilo_core::signal::{Prop, Signal};
84use teksilo_core::widget::{
85    CursorIcon, EventContext, LayoutContext, PaintContext, Widget, WidgetPlacement,
86};
87use teksilo_core::widget_builder::HandlerSet;
88use teksilo_core::widget_id::WidgetId;
89use teksilo_text::text_document::SelectionType;
90use teksilo_text::{CursorAffinity, CursorDisplay, RichTextEngine, SharedTypesetter};
91use teksilo_tokens::{HAlignment, InputTokens, TextStyle, TypographyTokens};
92
93use crate::button::InteractionState;
94use crate::menu_item::MenuItem;
95use crate::menu_list::{MenuList, MenuSeparator};
96use crate::rich_text::paint::{PaintParams, paint_frame};
97
98use self::state::{
99    AtPublished, SharedState, TextInputConfig, TextInputState, publish_cursor_signals,
100    sync_cursor_signals,
101};
102pub(crate) use self::state::{CharFilter, CommandFactory};
103
104pub use self::mask::{InputMask, MaskClass, MaskError, MaskPosition};
105pub use self::validator::{ValidationFeedback, ValidationOutcome, ValidatorFn};
106
107// The caret blink period and the debounce window are shared with every other
108// text surface — see `common::editor_runtime`. They used to be re-declared
109// here as private constants ("same as RichTextEditor", said the comment),
110// which is exactly the kind of duplication that drifts silently: two carets
111// blinking at different rates is invisible to tests and obvious to users.
112use crate::common::editor_runtime::CaretPolicy;
113use teksilo_core::styles::density::spacing;
114
115/// Horizontal scroll margin in pixels. The caret stays at least this
116/// far from the left/right edge of the viewport.
117const SCROLL_MARGIN: f32 = 4.0;
118
119/// [`SCROLL_MARGIN`] scaled by the density's `spacing_factor`
120/// (1.00 / 1.15 / 1.30).
121fn scroll_margin(tokens: &InputTokens) -> f32 {
122    spacing(SCROLL_MARGIN, tokens)
123}
124
125/// Default text-area height when the caller does not override it
126/// via [`TextInputField::text_height`]. Sits close to the Int UI
127/// `TEXT_FIELD_HEIGHT` recipe constant (in
128/// `crate::styles::recipe_text_input_style`) minus its border and
129/// vertical padding — the 18 dp the `TextInput` composite passes —
130/// so a bare `TextInputField` added to a tree without its composite
131/// still looks right.
132const DEFAULT_TEXT_HEIGHT: f32 = 20.0;
133
134/// The semantic purpose of a text field, surfaced to assistive technology as
135/// a specialised AccessKit role (WCAG 1.3.5 Identify Input Purpose / EN 301 549).
136///
137/// This is the in-framework-achievable part of SC 1.3.5: a screen reader
138/// announces "email, edit text" instead of a generic "edit text". The FULL
139/// HTML `autocomplete`-token vocabulary (`given-name`, `postal-code`,
140/// `cc-number`, …) that drives OS/browser autofill has **no representation in
141/// AccessKit 0.24** and therefore cannot be exposed from Teksilo — see
142/// `docs/a11y/a11y_issues.md`. Password entry is configured via
143/// [`TextInputField::secure`], not here.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
145pub enum InputPurpose {
146    /// Ordinary free text (`Role::TextInput`).
147    #[default]
148    Normal,
149    /// Email address (`Role::EmailInput`).
150    Email,
151    /// Telephone number (`Role::PhoneNumberInput`).
152    Phone,
153    /// URL (`Role::UrlInput`).
154    Url,
155    /// Numeric entry — e.g. a quantity or code (`Role::NumberInput`).
156    Number,
157    /// Search query (`Role::SearchInput`).
158    Search,
159}
160
161impl InputPurpose {
162    /// The AccessKit role for a non-secure field with this purpose.
163    pub(crate) fn to_role(self) -> teksilo_core::accesskit::Role {
164        use teksilo_core::accesskit::Role;
165        match self {
166            InputPurpose::Normal => Role::TextInput,
167            InputPurpose::Email => Role::EmailInput,
168            InputPurpose::Phone => Role::PhoneNumberInput,
169            InputPurpose::Url => Role::UrlInput,
170            InputPurpose::Number => Role::NumberInput,
171            InputPurpose::Search => Role::SearchInput,
172        }
173    }
174}
175
176/// How a secure ([`TextInputField::secure`]) field echoes typed
177/// characters. Mirrors Qt's `QLineEdit::EchoMode`.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
179pub enum EchoMode {
180    /// Replace every character with the echo glyph (default `'•'`).
181    /// The plaintext stays in the bound `Signal<String>` but never
182    /// reaches the text engine while masked.
183    #[default]
184    Masked,
185    /// Show nothing at all — not even the length. The caret stays at
186    /// the start. Qt's `NoEcho`.
187    NoEcho,
188    /// Show plaintext while the field is focused (being edited) and
189    /// re-mask on blur. Qt's `PasswordEchoOnEdit`.
190    RevealWhileTyping,
191}
192
193/// How a *revealed* secure field reports to assistive technology.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195pub enum AtRevealPolicy {
196    /// When revealed, expose the field as a normal `Role::TextInput`
197    /// carrying the plaintext value — matching what is visibly on
198    /// screen and the web `type=password ↔ type=text` swap. When
199    /// masked, it reverts to `Role::PasswordInput`. (Default.)
200    #[default]
201    SwapRole,
202    /// Always report `Role::PasswordInput` and never expose plaintext
203    /// to assistive tech, even while visually revealed. Higher
204    /// confidentiality at the cost of consistency with the screen.
205    AlwaysProtected,
206}
207
208/// Editable single-line text surface primitive.
209///
210/// See the [module docs](self) for the full feature list and a
211/// compositional example.
212pub struct TextInputField {
213    // ── Configuration (builder methods, consumed in build) ───────────
214    text: Signal<String>,
215    /// Enabled state, static or reactive; forwarded to the arena at build
216    /// time.
217    enabled: Prop<bool>,
218    read_only: bool,
219    max_length: Option<usize>,
220    placeholder: String,
221    on_submit: Option<CommandFactory>,
222    on_access_set_value: Option<super::text_input_field::state::AccessSetValue>,
223    on_blur: Option<CommandFactory>,
224    char_filter: Option<CharFilter>,
225    /// Fixed trailing label rendered inside the field's border.
226    /// Accepts both plain strings and `Signal<String>` — when bound,
227    /// the field re-measures the suffix and relayouts each time the
228    /// signal fires, so composites like `SpinBox` can derive the
229    /// suffix from the widget state (e.g. hide it while
230    /// `special_value_text` is active).
231    suffix: Prop<String>,
232    text_height: Option<f32>,
233    /// See [`text_style`](Self::text_style). `None` shapes in the engine's
234    /// default font, as every field did before the style could be chosen.
235    text_style: Option<TextStyleProp>,
236    /// See [`text_alignment`](Self::text_alignment).
237    text_alignment: Option<HAlignment>,
238    /// See [`text_color`](Self::text_color). `None` paints the theme's
239    /// primary text colour.
240    text_color: Option<ColorProp>,
241    external_interaction: Option<Signal<InteractionState>>,
242
243    /// Optional input mask. When set, the field auto-derives a
244    /// placeholder template (`__/__/____` for `99/99/9999`) and
245    /// rejects non-fitting characters via a position-aware filter
246    /// composed with the user's `char_filter`. See [`InputMask`] for
247    /// the grammar.
248    mask: Option<InputMask>,
249    /// Visible char used for unfilled editable positions in the mask
250    /// template. Defaults to the `TEXT_FIELD_MASK_PLACEHOLDER_CHAR`
251    /// recipe constant (in `crate::styles::recipe_text_input_style`,
252    /// `_`).
253    mask_placeholder_override: Option<char>,
254    /// Validator closure called on every commit (Enter, Tab-out,
255    /// blur). Returns a [`ValidationOutcome`] that drives
256    /// [`feedback`](Self::validation_feedback_signal).
257    validator: Option<ValidatorFn>,
258    /// Published feedback signal. Composites bind to this to render
259    /// the inline validation strip below the field.
260    feedback: Signal<ValidationFeedback>,
261
262    // ── Secure / password masking (set via `secure`) ────────────────
263    secure: bool,
264    echo_mode: EchoMode,
265    echo_char: char,
266    revealed: Option<Signal<bool>>,
267    at_reveal_policy: AtRevealPolicy,
268    allow_copy: bool,
269
270    /// Semantic purpose → specialised AT role (WCAG 1.3.5). Ignored while the
271    /// field is `secure` (password role wins).
272    input_purpose: InputPurpose,
273
274    /// ARIA combobox wiring — see [`active_descendant`](Self::active_descendant).
275    active_descendant: Option<Signal<Option<WidgetId>>>,
276    /// The listbox this field drives, if any — see
277    /// [`controls`](Self::controls).
278    controls: Option<Signal<Option<WidgetId>>>,
279
280    // ── Internal (set during build) ─────────────────────────────────
281    state: Option<SharedState>,
282    /// Interaction signal actually used at runtime. Either the one
283    /// supplied by a wrapping composite via
284    /// [`TextInputField::interaction_signal`] or a fresh one owned
285    /// by the field. Read by the focus handler to repaint a
286    /// parent's focus ring / border on gain/loss.
287    interaction: Signal<InteractionState>,
288    /// Mirror of the inner state's `cursor_position` for external
289    /// readers. Wired in `build()` via a `ctx.effect`. Composing
290    /// widgets that need the caret (e.g. `DateEdit` for segment
291    /// stepping) read this via [`TextInputField::caret_position`].
292    caret_position: Signal<usize>,
293    /// Late-bound handle to the inner `SharedState`, populated in
294    /// `build()`. Lets composing widgets capture a `caret_setter`
295    /// closure BEFORE the field is moved into the tree, then call
296    /// it later to programmatically reposition the caret. Required
297    /// because the inner state doesn't exist before `build()` runs,
298    /// but the composing widget loses ownership of `self` once it
299    /// hands the field to `ctx.add(...)`.
300    state_slot: std::rc::Rc<std::cell::RefCell<Option<SharedState>>>,
301    /// Minted with the widget, not with its state, so a [`TextFieldHandle`]
302    /// taken before `build` observes the signal the built widget writes.
303    focus_signal: Signal<bool>,
304    /// Touch selection: the controller, and the ids of the two overlays it
305    /// raises. Minted with the widget rather than in `build` so the handle
306    /// survives a rebuild even though the controller inside it is replaced.
307    /// See [`touch`].
308    pub(crate) touch: Rc<touch::FieldTouch>,
309    /// Natural intrinsic width in logical pixels, cached during
310    /// `build()`. When an [`InputMask`] is set, this measures the
311    /// mask's worst-case filled template (e.g. `00/00/0000`, plus a
312    /// safety `M`) in the theme body font and adds a small caret
313    /// slack — so a date / time / phone field reports a width that
314    /// matches its content envelope instead of the generic 200 dp
315    /// fallback. Composing widgets like `DateEdit` rely on this so
316    /// their unconstrained natural width tracks the format pattern.
317    natural_width: f32,
318    /// What the field's text measured, kept for the accessibility pass.
319    ///
320    /// A reader reviewing the field by character, word or line needs
321    /// per-character extents, and the editing engine's own layout is not
322    /// reachable from `accessibility()`. Written by `place_children` — the
323    /// first pass that knows the final width — and again by `paint`, because
324    /// a keystroke dirties the field at `RepaintOnly` / `AccessibilityOnly`
325    /// and never relayouts: geometry taken from `place_children` alone would
326    /// be one edit stale for as long as anyone is typing.
327    retained: Rc<std::cell::RefCell<Option<RetainedText>>>,
328}
329
330impl std::fmt::Debug for TextInputField {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        f.debug_struct("TextInputField")
333            .field("placeholder", &self.placeholder)
334            .field("enabled", &self.enabled.get())
335            .field("read_only", &self.read_only)
336            .finish_non_exhaustive()
337    }
338}
339
340impl TextInputField {
341    /// Construct a new field bound to `text`.
342    pub fn new(text: Signal<String>) -> Self {
343        let state_slot: std::rc::Rc<std::cell::RefCell<Option<SharedState>>> =
344            std::rc::Rc::new(std::cell::RefCell::new(None));
345        Self {
346            text,
347            enabled: Prop::Static(true),
348            read_only: false,
349            max_length: None,
350            placeholder: String::new(),
351            on_submit: None,
352            on_access_set_value: None,
353            on_blur: None,
354            char_filter: None,
355            suffix: Prop::Static(String::new()),
356            text_height: None,
357            text_style: None,
358            text_alignment: None,
359            text_color: None,
360            external_interaction: None,
361            mask: None,
362            mask_placeholder_override: None,
363            validator: None,
364            feedback: Signal::new(ValidationFeedback::Pristine),
365            secure: false,
366            echo_mode: EchoMode::Masked,
367            echo_char: '\u{2022}',
368            revealed: None,
369            at_reveal_policy: AtRevealPolicy::SwapRole,
370            allow_copy: true,
371            input_purpose: InputPurpose::Normal,
372            active_descendant: None,
373            controls: None,
374            state: None,
375            interaction: Signal::new(InteractionState::Idle),
376            caret_position: Signal::new(0),
377            state_slot: state_slot.clone(),
378            focus_signal: Signal::new(false),
379            touch: touch::FieldTouch::new(state_slot),
380            natural_width: 200.0,
381            retained: Rc::new(std::cell::RefCell::new(None)),
382        }
383    }
384
385    /// Declarative placeholder string. The field itself paints
386    /// nothing for placeholder — that visual is the composite
387    /// parent's responsibility (`TextInput` overlays a
388    /// `TextWidget`). The string is still stored here and published
389    /// via AccessKit's `placeholder` property so screen readers
390    /// announce it.
391    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
392        self.placeholder = text.into();
393        self
394    }
395
396    /// Set the enabled state, statically or reactively. Disabled blocks
397    /// input and AccessKit interaction. Forwarded to the arena at build
398    /// time.
399    pub fn enabled(mut self, enabled: impl Into<Prop<bool>>) -> Self {
400        self.enabled = enabled.into();
401        self
402    }
403
404    /// Mark the field read-only. Caret and selection still work;
405    /// inserts, deletes, paste, undo/redo, and cut are all no-ops.
406    pub fn read_only(mut self, read_only: bool) -> Self {
407        self.read_only = read_only;
408        self
409    }
410
411    /// Hard cap on document length in `char`s (grapheme count is
412    /// approximated — each `char` counts as one unit, matching
413    /// `String::chars().count()`).
414    pub fn max_length(mut self, max_length: usize) -> Self {
415        self.max_length = Some(max_length);
416        self
417    }
418
419    /// Closure fired on `Enter`. Unlike `on_blur_fn`, this does
420    /// not move focus — the field stays focused and the caret
421    /// stays where it was.
422    pub fn on_submit_fn(mut self, f: impl Fn(&mut EventContext) + 'static) -> Self {
423        self.on_submit = Some(Box::new(f));
424        self
425    }
426
427    /// Handle an assistive technology's whole-value write, given the string it
428    /// set.
429    ///
430    /// Leave it unset for a field whose bound `Signal<String>` **is** the
431    /// value: the write has already landed and there is nothing to derive.
432    ///
433    /// Install one for a field whose text is a *projection* of a typed value,
434    /// as `SpinBox` and the date and time editors are. There the string is
435    /// only a display of the real value, so without this an
436    /// `Action::SetValue` resolved against the inner text node changes what is
437    /// shown, leaves the typed value stale until the next blur, and never
438    /// fires the host's change callback — an assistive technology or an
439    /// automation client sees a success and the wrong value. The composite's
440    /// own node handles `SetValue` properly; this closes the same door on the
441    /// text node beneath it.
442    ///
443    /// The string is handed over rather than read back from the bound signal
444    /// because the document→signal sync is deferred to the next frame tick, so
445    /// a host reading the signal here would parse the text from *before* this
446    /// edit and revert.
447    pub fn on_access_set_value(
448        mut self,
449        f: impl Fn(&str, &mut EventContext) -> bool + 'static,
450    ) -> Self {
451        self.on_access_set_value = Some(std::rc::Rc::new(f));
452        self
453    }
454
455    /// Closure fired once per focus-loss, after selection/scroll
456    /// have been reset. SpinBox-style callers parse and reformat
457    /// here; validators revalidate here.
458    pub fn on_blur_fn(mut self, f: impl Fn(&mut EventContext) + 'static) -> Self {
459        self.on_blur = Some(Box::new(f));
460        self
461    }
462
463    /// Per-character input-filter predicate. Applied uniformly to
464    /// keyboard input, IME commits, and clipboard paste so a filtered
465    /// field cannot receive disallowed characters through any path.
466    /// Composes with `max_length` and the built-in control/newline
467    /// strip (filter runs after the strip). Whole-string validity
468    /// (e.g. "at most one decimal point") is a commit-time concern
469    /// for `on_blur` / `on_submit`.
470    pub fn char_filter(mut self, f: impl Fn(char) -> bool + 'static) -> Self {
471        self.char_filter = Some(Rc::new(f));
472        self
473    }
474
475    /// Static non-editable trailing string rendered flush-right
476    /// inside the field's bounds (Qt's `QSpinBox::suffix`). The
477    /// caret cannot enter the suffix; clicks past the text end
478    /// position the caret at the last editable character.
479    ///
480    /// Accepts a static `String`/`&str` or a reactive `Signal<String>` /
481    /// `Prop<String>`; when bound, the field re-measures the suffix glyphs
482    /// and relayouts the editable text viewport each time the signal fires.
483    /// Typical use: a `SpinBox` with `special_value_text` binds an empty
484    /// string to the suffix whenever the value equals `min`, and the
485    /// configured unit string otherwise.
486    pub fn suffix(mut self, text: impl Into<Prop<String>>) -> Self {
487        self.suffix = text.into();
488        self
489    }
490
491    /// Override the intrinsic text-area height. The field is a
492    /// pure leaf with no theme lookup of its own; by default it
493    /// reports `DEFAULT_TEXT_HEIGHT`. A wrapping composite like
494    /// `TextInput` passes the `TEXT_FIELD_HEIGHT` recipe constant
495    /// (in `crate::styles::recipe_text_input_style`) minus
496    /// border + padding here so the visuals line up with the
497    /// rest of the form.
498    ///
499    /// Wins over the line height a [`text_style`](Self::text_style) would
500    /// derive: a composite that passes both asked for this height.
501    pub fn text_height(mut self, height: f32) -> Self {
502        self.text_height = Some(height);
503        self
504    }
505
506    /// The style the text is shaped in: its size, family and weight.
507    ///
508    /// Accepts a [`TextStyleRole`](teksilo_tokens::TextStyleRole), resolved
509    /// against the theme's typography and re-resolved when the theme changes,
510    /// or a fixed [`TextStyle`]. The global text scale multiplies the size
511    /// either way, as it does for every text in the tree.
512    ///
513    /// The style also sets the field's height: unless
514    /// [`text_height`](Self::text_height) overrides it, the text area is one
515    /// line of `size × line_height` tall, and the shaped line is centred in it.
516    /// An 11 px style makes a short field, a 20 px one a tall field.
517    ///
518    /// The family resolves through the app's font registry like any other
519    /// text: a family that is not registered falls back to the default face,
520    /// with a warning on the console. The weight is honoured when the family
521    /// has that weight (a variable face covers every weight). The style's
522    /// `letter_spacing` is not applied to the editable text, and its
523    /// `line_height` sizes the field without spacing the glyphs.
524    ///
525    /// Unset, the field shapes in the text engine's default font at its
526    /// default size, exactly as before this setting existed.
527    pub fn text_style(mut self, style: impl Into<TextStyleProp>) -> Self {
528        self.text_style = Some(style.into());
529        self
530    }
531
532    /// Where the text sits when it is narrower than the field.
533    ///
534    /// [`HAlignment::Leading`] puts it at the start of the field — the left
535    /// edge in a left-to-right layout, the right edge in a right-to-left one —
536    /// [`HAlignment::Center`] in the middle and [`HAlignment::Trailing`] at
537    /// the end. Text wider than the field scrolls horizontally to follow the
538    /// caret, as an unaligned field does. The caret, a click, the selection,
539    /// the IME candidate window, the touch handles and the character extents a
540    /// screen reader is given all follow the placement.
541    ///
542    /// Unset, the text starts at the left edge whatever the layout direction,
543    /// as it always has.
544    pub fn text_alignment(mut self, alignment: HAlignment) -> Self {
545        self.text_alignment = Some(alignment);
546        self
547    }
548
549    /// The colour of the text and of the caret.
550    ///
551    /// Accepts a [`Color`](teksilo_tokens::Color), a theme role, a
552    /// `Signal<Color>` or a `Signal<TextRole>`, resolved against the theme
553    /// every time the field paints, so a role follows the theme and a signal
554    /// repaints the field when it changes. A disabled field paints the theme's
555    /// disabled text colour whatever is set here, unless the colour is wrapped
556    /// in [`ColorProp::undimmed`].
557    ///
558    /// A different colour at rest and while editing is a signal of the
559    /// field's focus, which [`handle`](Self::handle) exposes before the field
560    /// is built:
561    ///
562    /// ```ignore
563    /// let field = TextInputField::new(text);
564    /// let focused = field.handle().focused_signal();
565    /// let field = field.text_color(focused.map(|f| {
566    ///     if *f { TextRole::Primary } else { TextRole::Secondary }
567    /// }));
568    /// ```
569    ///
570    /// Unset, the text is the theme's primary text colour.
571    pub fn text_color(mut self, color: impl Into<ColorProp>) -> Self {
572        self.text_color = Some(color.into());
573        self
574    }
575
576    /// Bind an externally-owned `InteractionState` signal. The
577    /// field writes `Focused` on focus gain and `Idle` on loss;
578    /// other states (`Hovered`, `Pressed`, `Disabled`) are the
579    /// composite's responsibility. When unset, the field owns a
580    /// private signal that observers can still read via
581    /// [`interaction`](TextInputField::interaction), but composites
582    /// that drive a focus ring or border color usually want to
583    /// push their own.
584    pub fn interaction_signal(mut self, signal: Signal<InteractionState>) -> Self {
585        self.external_interaction = Some(signal);
586        self
587    }
588
589    /// Set an input mask (Qt grammar). Constrains accepted characters
590    /// per position, auto-derives the empty-state template
591    /// (`__/__/____` for `99/99/9999`), and routes typed chars
592    /// through the mask's class filter.
593    ///
594    /// Composes with [`char_filter`](Self::char_filter): a char must
595    /// pass *both* the mask's per-position class AND the user's
596    /// `char_filter` to be accepted.
597    ///
598    /// On parse error (only the trailing-backslash case in practice),
599    /// the mask is silently dropped — the field falls back to its
600    /// no-mask behaviour rather than panicking.
601    pub fn input_mask(mut self, mask: impl AsRef<str>) -> Self {
602        match InputMask::parse(mask.as_ref()) {
603            Ok(m) => self.mask = Some(m),
604            Err(_) => self.mask = None,
605        }
606        self
607    }
608
609    /// Override the visible character used for unfilled editable mask
610    /// positions. Default: the `TEXT_FIELD_MASK_PLACEHOLDER_CHAR`
611    /// recipe constant (in `crate::styles::recipe_text_input_style`,
612    /// `_`).
613    pub fn mask_placeholder(mut self, c: char) -> Self {
614        self.mask_placeholder_override = Some(c);
615        self
616    }
617
618    /// Install a validator. The closure runs on every commit (Enter,
619    /// Tab-out, focus loss) and returns a [`ValidationOutcome`] that
620    /// drives [`validation_feedback_signal`](Self::validation_feedback_signal).
621    ///
622    /// **Does not run per-keystroke** — that's [`char_filter`](Self::char_filter)'s
623    /// job. Mixing per-keystroke text rewriting with validation
624    /// produces caret-jump bugs and is explicitly out of scope.
625    pub fn validator(mut self, f: impl Fn(&str) -> ValidationOutcome + 'static) -> Self {
626        self.validator = Some(Rc::new(f));
627        self
628    }
629
630    /// Turn this into a secure (password) field with the given
631    /// [`EchoMode`]. Masking happens at the text-engine layer (one echo
632    /// glyph per source `char`), so the plaintext never reaches the
633    /// shaper or glyph atlas while masked, and caret / selection /
634    /// hit-test stay correct. Also defaults `allow_copy` to `false` and
635    /// declares the focused node an `ImePurpose::Password` surface — the
636    /// OS IME stays enabled so non-Latin passwords can still be composed,
637    /// with the preedit masked on screen and hidden from AT. Pair with
638    /// [`revealed`](Self::revealed) for a reveal toggle.
639    pub fn secure(mut self, echo_mode: EchoMode) -> Self {
640        self.secure = true;
641        self.echo_mode = echo_mode;
642        self.allow_copy = false;
643        self
644    }
645
646    /// Declare the field's semantic [`InputPurpose`] (WCAG 1.3.5), which
647    /// selects a specialised AccessKit role (`EmailInput`, `PhoneNumberInput`,
648    /// …) so screen readers announce the field's kind. Ignored while `secure`
649    /// (the password role wins). Does not change IME behaviour — winit's
650    /// `ImePurpose` has no email/number/url variants — nor drive OS autofill,
651    /// which AccessKit cannot express (see `docs/a11y/a11y_issues.md`).
652    pub fn input_purpose(mut self, purpose: InputPurpose) -> Self {
653        self.input_purpose = purpose;
654        self
655    }
656
657    /// Publish `active_descendant` pointing at the row a *separate* list is
658    /// currently highlighting — the ARIA combobox pattern.
659    ///
660    /// Keyboard focus stays in this field while arrow keys move a highlight
661    /// through a listbox elsewhere in the tree (a command palette, a
662    /// type-ahead picker, a suggestion popup). Assistive technology follows
663    /// the focused node's active descendant, so the announcement has to be
664    /// published **here**, on the node that actually holds focus — not on the
665    /// composite ancestor that owns the list. Without it the arrow keys move a
666    /// highlight that is announced to nobody.
667    ///
668    /// Bound at `AccessibilityOnly`, so moving the highlight re-walks the AT
669    /// tree without a rebuild or a repaint. Pair with [`controls`](Self::controls).
670    pub fn active_descendant(mut self, active: Signal<Option<WidgetId>>) -> Self {
671        self.active_descendant = Some(active);
672        self
673    }
674
675    /// Publish a `controls` relation to the listbox this field drives, so an
676    /// AT client can navigate from the input to the list it is filtering.
677    /// The companion of [`active_descendant`](Self::active_descendant).
678    pub fn controls(mut self, listbox: Signal<Option<WidgetId>>) -> Self {
679        self.controls = Some(listbox);
680        self
681    }
682
683    /// Override the masking glyph (default `'•'`, U+2022). Any
684    /// uniform-width character works; the engine emits exactly one per
685    /// source `char`.
686    pub fn echo_char(mut self, c: char) -> Self {
687        self.echo_char = c;
688        self
689    }
690
691    /// Bind the reveal toggle. When the signal is `true` the field
692    /// shows plaintext regardless of [`EchoMode`]; when `false` it
693    /// masks. Shared with the eye [`IconButton::visibility_toggle`].
694    ///
695    /// [`IconButton::visibility_toggle`]: crate::IconButton::visibility_toggle
696    pub fn revealed(mut self, revealed: Signal<bool>) -> Self {
697        self.revealed = Some(revealed);
698        self
699    }
700
701    /// How a *revealed* secure field reports to assistive tech. Default
702    /// [`AtRevealPolicy::SwapRole`].
703    pub fn at_reveal_policy(mut self, policy: AtRevealPolicy) -> Self {
704        self.at_reveal_policy = policy;
705        self
706    }
707
708    /// Permit (or forbid) copy / cut. Plain fields default `true`;
709    /// [`secure`](Self::secure) flips the default to `false`. Even when
710    /// `false`, copy is allowed while the field is revealed.
711    pub fn allow_copy(mut self, allow: bool) -> Self {
712        self.allow_copy = allow;
713        self
714    }
715
716    /// Reactive handle on the published [`ValidationFeedback`] state.
717    /// Composites bind to this to render the inline feedback strip
718    /// below the field. Always present; reads `Pristine` until the
719    /// first commit (or forever if no validator is installed).
720    pub fn validation_feedback_signal(&self) -> Signal<ValidationFeedback> {
721        self.feedback.clone()
722    }
723
724    /// The `Signal<String>` this field is bound to.
725    pub fn text(&self) -> Signal<String> {
726        self.text.clone()
727    }
728
729    /// Adopt an existing handle instead of minting one.
730    ///
731    /// For a composing widget — `TextInput` wraps this field — that must hand
732    /// out a handle of its own **before** it builds the field it will delegate
733    /// to. Sharing the slot and the focus signal makes the wrapper's handle and
734    /// the field's the same handle, rather than two that agree by accident.
735    pub fn share_handle(mut self, handle: &TextFieldHandle) -> Self {
736        self.state_slot = handle.slot.clone();
737        self.focus_signal = handle.focus_signal.clone();
738        self
739    }
740
741    /// A live handle on this field, valid before and after `build`.
742    ///
743    /// The counterpart of `RichTextEditor::handle`, and the reason it exists:
744    /// an application that routes Undo, Cut, Copy, Paste and Select All to
745    /// "whichever text surface holds the caret" has to be able to *drive* every
746    /// such surface, not only the rich editors. Without this, a menu built for
747    /// those commands can only grey them out over a rename field or a search
748    /// box while the field's own key handling still works — a menu that lies
749    /// about what the keyboard can do.
750    ///
751    /// Like `caret_setter`, the handle reaches its state through the slot the
752    /// widget late-populates, so it may be taken while the tree is being
753    /// described and used once it is live.
754    pub fn handle(&self) -> TextFieldHandle {
755        TextFieldHandle {
756            slot: self.state_slot.clone(),
757            focus_signal: self.focus_signal.clone(),
758        }
759    }
760
761    /// The interaction signal this field writes on focus changes.
762    /// Call before inserting the field into the tree.
763    pub fn interaction(&self) -> Signal<InteractionState> {
764        self.interaction.clone()
765    }
766
767    /// Reactive caret position in the field's text (in `usize` char
768    /// offsets). Updates after every keyboard or pointer action that
769    /// moves the cursor. Used by composing widgets that need to know
770    /// where the caret is — e.g. `DateEdit` reads this to figure out
771    /// which date segment Up/Down should step.
772    pub fn caret_position(&self) -> Signal<usize> {
773        self.caret_position.clone()
774    }
775
776    /// Returns a callable that programmatically sets the caret
777    /// position (in char offsets) on the field. Capture this on the
778    /// builder BEFORE `ctx.add(...)` consumes the field; call it
779    /// after a programmatic text rewrite to restore the caret to the
780    /// right column instead of leaving it at the document end (the
781    /// default behaviour of `cursor.insert_text`).
782    ///
783    /// The returned closure becomes a no-op until `build()` runs;
784    /// after build it walks the field's inner state and moves the
785    /// document cursor to `position`, clamped to the document
786    /// length. Used by `DateEdit` / `TimeEdit` segment-stepping to
787    /// keep the caret within its current segment after Up/Down.
788    pub fn caret_setter(&self) -> std::rc::Rc<dyn Fn(usize)> {
789        let slot = self.state_slot.clone();
790        std::rc::Rc::new(move |position: usize| {
791            if let Some(state) = slot.borrow().as_ref() {
792                let st = state.borrow();
793                st.cursor
794                    .set_position(position, teksilo_text::text_document::MoveMode::MoveAnchor);
795                let actual = st.cursor.position();
796                if st.cursor_position.get() != actual {
797                    st.cursor_position.set(actual);
798                }
799            }
800        })
801    }
802}
803
804impl TextInputField {
805    /// Shape the field's text once more and keep the geometry.
806    ///
807    /// The whole line is measured with no width cap: the field scrolls
808    /// rather than ellipsizes, so every character has an extent even while
809    /// it sits outside the viewport, and `accessibility` slides the result
810    /// by the scroll offset.
811    fn retain_text_geometry(
812        &self,
813        bounds: Rect,
814        style: &TextStyle,
815        backend: &Rc<std::cell::RefCell<dyn teksilo_canvas::TextBackend>>,
816        base_direction: teksilo_core::accesskit::TextDirection,
817    ) {
818        let Some(state) = self.state.as_ref() else {
819            return;
820        };
821        let text = {
822            let st = state.borrow();
823            let text = st.document.to_plain_text().unwrap_or_default();
824            if !st.should_mask() {
825                text
826            } else if st.echo_mode == EchoMode::NoEcho {
827                String::new()
828            } else {
829                // Masking happens inside the editing engine precisely so the
830                // secret never reaches the shaper or the glyph atlas; measuring
831                // it here would put it there. Measure what is painted instead,
832                // the mask, which is also the text a masked field's runs carry.
833                st.echo_char.to_string().repeat(text.chars().count())
834            }
835        };
836        let layout = backend.borrow_mut().layout_single_line(&text, style, None);
837        *self.retained.borrow_mut() = Some(RetainedText {
838            text,
839            geometry: layout.geometry.clone(),
840            bounds,
841            base_direction,
842        });
843    }
844
845    /// The style the field's text is measured in for the accessibility pass,
846    /// at the scale the tree is laid out at. `typography` is the layout or
847    /// paint context's, which the global text scale has already been applied
848    /// to; a fixed style is scaled here to match.
849    ///
850    /// Without a style this is the body style, as it has always been.
851    fn measured_style(&self, typography: &TypographyTokens, text_scale: f32) -> TextStyle {
852        match &self.text_style {
853            None => typography.body.clone(),
854            Some(TextStyleProp::Role(role)) => role.resolve(typography),
855            Some(TextStyleProp::Static(style)) => TextStyle {
856                size: style.size * text_scale,
857                ..style.clone()
858            },
859        }
860    }
861
862    /// The text area's height at a text scale of 1: the explicit
863    /// [`text_height`](Self::text_height), else one line of the resolved
864    /// [`text_style`](Self::text_style), else the default.
865    fn base_text_height(&self) -> f32 {
866        if let Some(height) = self.text_height {
867            return height;
868        }
869        self.state
870            .as_ref()
871            .and_then(|state| state.borrow().text_style.as_ref().map(line_height_of))
872            .unwrap_or(DEFAULT_TEXT_HEIGHT)
873    }
874
875    /// Borrow the shared state. Panics if called before `build()`
876    /// has run — the state is allocated in `build()` from the
877    /// builder config.
878    fn state(&self) -> &SharedState {
879        self.state
880            .as_ref()
881            .expect("TextInputField::state called before build")
882    }
883
884    /// The suffix the field lays out and paints now, for the tests of the
885    /// widgets that drive it.
886    #[cfg(test)]
887    pub(crate) fn suffix_shown(&self) -> String {
888        self.state().borrow().suffix.clone()
889    }
890}
891
892/// One line of `style`, in logical pixels: its size times its line height.
893pub(crate) fn line_height_of(style: &TextStyle) -> f32 {
894    style.size * style.line_height
895}
896
897/// The reading direction the field's runs are announced with.
898///
899/// The single-line editing engine reports no per-segment direction, so the
900/// ambient layout direction is the only answer available; a right-to-left
901/// field therefore announces right-to-left even for Latin content, which is
902/// what the surrounding UI does too.
903fn base_text_direction(
904    direction: teksilo_core::environment::LayoutDirection,
905) -> teksilo_core::accesskit::TextDirection {
906    match direction {
907        teksilo_core::environment::LayoutDirection::RightToLeft => {
908            teksilo_core::accesskit::TextDirection::RightToLeft
909        }
910        _ => teksilo_core::accesskit::TextDirection::LeftToRight,
911    }
912}
913
914/// Adjust `scroll_x` so the caret stays within the visible viewport.
915///
916/// `text_viewport_width` is the portion of the viewport reserved for
917/// editable text, i.e. `viewport_width - suffix_width`. Callers pass
918/// the reduced width explicitly so the scroll never slides text
919/// behind the non-editable suffix.
920///
921/// With a [`text_alignment`](TextInputField::text_alignment) set, a text
922/// that fits is placed by a *negative* scroll: every conversion between the
923/// engine's space and the field's already subtracts `scroll_x` — paint, the
924/// caret, hit-testing, the selection, the IME area, the touch handles, the
925/// reader's character extents — so moving the text this way moves all of them
926/// with it, and none can be forgotten. A text that overflows scrolls exactly as
927/// an unaligned one does.
928fn ensure_caret_visible_h(
929    st: &mut TextInputState,
930    text_viewport_width: f32,
931    tokens: &InputTokens,
932    rtl: bool,
933) {
934    if !st.engine.has_full_layout() || text_viewport_width <= 0.0 {
935        return;
936    }
937    let pos = st.cursor.position();
938    // Single-line input: no wrap, affinity is a no-op.
939    let caret = st.engine.caret_rect(pos, CursorAffinity::Downstream);
940    let caret_x = caret[0];
941    let caret_w = caret[2].max(1.0);
942    let vw = text_viewport_width;
943
944    if let Some(alignment) = st.text_alignment {
945        // The caret's width is part of the run, or a trailing-aligned caret at
946        // the end of the text would sit one pixel outside the clip.
947        let run = st.engine.max_content_width() + caret_w;
948        if run <= vw {
949            st.scroll_x = -alignment.resolve(run, vw, rtl);
950            return;
951        }
952        // Overflowing: whatever offset placed a short text is not a scroll.
953        st.scroll_x = st.scroll_x.max(0.0);
954    }
955
956    let margin = scroll_margin(tokens);
957    if caret_x - st.scroll_x < margin {
958        st.scroll_x = (caret_x - margin).max(0.0);
959    } else if caret_x + caret_w - st.scroll_x > vw - margin {
960        st.scroll_x = caret_x + caret_w - vw + margin;
961    }
962}
963
964/// Update the cached suffix text and re-run layout on the suffix
965/// engine. Called from `build()` for the initial value and from
966/// the reactive effect when the bound suffix signal fires.
967fn relayout_suffix(state: &SharedState, new_text: &str) {
968    let mut st = state.borrow_mut();
969    st.suffix = new_text.to_string();
970    if new_text.is_empty() {
971        st.suffix_width = 0.0;
972        // Leave the engine in place (cheap to reuse) but don't
973        // lay out — paint skips the suffix when width is zero.
974        return;
975    }
976    // No engine allocated (pure-static path that started empty and never
977    // became non-empty) makes this a no-op — `build()` allocates the engine
978    // eagerly for every bound suffix, so a late signal flip never reaches
979    // that case.
980    st.relayout_suffix_engine();
981}
982
983/// Paint glyphs from a pre-laid-out suffix `RenderFrame` at a fixed
984/// origin. Decorations, selection rectangles, and caret are ignored —
985/// the suffix is plain non-editable text, so only the glyph pass is
986/// needed. Kept inline (rather than reusing `paint_frame`) to avoid
987/// the `TextDocument` / `ImageCache` parameters `paint_frame`
988/// requires for inline images the suffix never contains.
989fn paint_suffix_glyphs(canvas: &mut Canvas, frame: &teksilo_text::RenderFrame, origin: Point) {
990    use teksilo_canvas::GlyphQuad as CanvasGlyphQuad;
991    for g in frame.glyphs.iter() {
992        let quad = CanvasGlyphQuad {
993            screen: [
994                g.screen[0] + origin.x,
995                g.screen[1] + origin.y,
996                g.screen[2],
997                g.screen[3],
998            ],
999            atlas: g.atlas,
1000            color: g.color,
1001            is_color: g.is_color,
1002        };
1003        canvas.draw_glyph_quad(quad);
1004    }
1005}
1006
1007/// The band a selection is painted in. Two axes decide it, and they do *not*
1008/// decide it the same way:
1009///
1010/// | field focus | window | band |
1011/// | --- | --- | --- |
1012/// | focused | active | vivid `selection_bg_active` |
1013/// | focused | inactive | muted `selection_bg_inactive` |
1014/// | not focused | either | nothing — fully transparent |
1015///
1016/// **An entry that does not hold focus paints no selection**, which is what
1017/// every native single-line field does. A Win32 edit control hides the
1018/// selection on focus-out unless it was created with `ES_NOHIDESEL`, and
1019/// WinForms spells the same default `TextBoxBase.HideSelection = true`.
1020/// `QLineEdit::focusOutEvent` goes further and calls `deselect()` outright for
1021/// every focus reason except `ActiveWindowFocusReason` and `PopupFocusReason`.
1022/// On macOS an `NSTextField` that stops being first responder has its shared
1023/// field editor detached, so there is no selection left to draw. GTK's entry is
1024/// the one toolkit that keeps a defocused selection lit, and that has been
1025/// filed against it as a papercut rather than defended as a design.
1026///
1027/// The *window* axis is the one where dimming, not hiding, is correct — and
1028/// the same three toolkits say so: Qt's carve-out for `ActiveWindowFocusReason`
1029/// exists precisely so a focused field keeps its selection when the window goes
1030/// to the background, and AppKit renders it there in
1031/// `unemphasizedSelectedTextBackgroundColor`. Losing the window is not the same
1032/// event as losing the caret.
1033///
1034/// Multi-line editors (`RichTextEditor`, `CodeEditor`, `LogView`) are
1035/// deliberately **not** on this rule: `QTextEdit` / `NSTextView` / every code
1036/// editor keep a visible selection in a blurred view, because there the
1037/// selection is a region of a document the user is working with rather than a
1038/// transient edit state.
1039///
1040/// The selection *state* survives blur either way — the `on_focus(false)` arm
1041/// spells out why (the right-click Copy path needs it) — this decides only what
1042/// is drawn.
1043fn field_selection_color(
1044    colors: &teksilo_tokens::ColorTokens,
1045    window_active: bool,
1046    has_focus: bool,
1047) -> [f32; 4] {
1048    match (has_focus, window_active) {
1049        (false, _) => [0.0; 4],
1050        (true, true) => colors.selection_bg_active.to_array(),
1051        (true, false) => colors.selection_bg_inactive.to_array(),
1052    }
1053}
1054
1055/// Simplified frame-loop tick for single-line text input.
1056fn tick(state: &mut TextInputState, delta: f32) -> bool {
1057    state.apply_pending_chars();
1058
1059    let had_events = state.drain_events();
1060
1061    // Blink only when focused AND the host window is active — the caret hides
1062    // in an inactive window (the universal desktop convention). The shared
1063    // blink machine's inactive arm then turns it off, since `!caret_active`
1064    // now also covers the window-inactive case.
1065    let caret_active = state.has_focus && state.window_active;
1066    let caret_visible = state.caret_visible.clone();
1067    let wake = state.frame_wake_at.clone();
1068    // A single-line field always blinks (no read-only/static presets), so it
1069    // hands the shared machine a fixed `Blinking` policy.
1070    state.blink.tick(
1071        CaretPolicy::Blinking,
1072        caret_active,
1073        &caret_visible,
1074        wake.as_ref(),
1075    );
1076
1077    if state.needs_full_layout && state.viewport_width > 0.0 {
1078        state.layout_full_masked();
1079        state.needs_full_layout = false;
1080        state.content_dirty = true;
1081    }
1082
1083    if state.pending_text_changed {
1084        let new_text = state.document.to_plain_text().unwrap_or_default();
1085        if state.text_signal.get() != new_text {
1086            state.deferred_text_update = Some(new_text);
1087        }
1088    }
1089
1090    if state.debounce.tick(delta) {
1091        if state.pending_text_changed {
1092            state.pending_text_changed = false;
1093        }
1094        if let Some((cu, cr)) = state.pending_undo_redo.take() {
1095            if state.can_undo.get() != cu {
1096                state.can_undo.set(cu);
1097            }
1098            if state.can_redo.get() != cr {
1099                state.can_redo.set(cr);
1100            }
1101        }
1102    }
1103    let debounce_work = state.pending_text_changed || state.pending_undo_redo.is_some();
1104
1105    had_events || debounce_work
1106}
1107
1108/// Handle AccessKit actions (SetValue, SetTextSelection, Focus).
1109fn handle_access_action(
1110    state: &SharedState,
1111    action: teksilo_core::accesskit::Action,
1112    data: Option<teksilo_core::accesskit::ActionData>,
1113    ctx: &mut EventContext,
1114) -> EventResponse {
1115    use teksilo_core::accesskit::{Action, ActionData};
1116
1117    match (action, data) {
1118        (Action::SetTextSelection, Some(ActionData::SetTextSelection(sel))) => {
1119            let st = state.borrow();
1120            st.cursor.set_position(
1121                sel.anchor.character_index,
1122                teksilo_text::text_document::MoveMode::MoveAnchor,
1123            );
1124            st.cursor.set_position(
1125                sel.focus.character_index,
1126                teksilo_text::text_document::MoveMode::KeepAnchor,
1127            );
1128            drop(st);
1129            sync_cursor_signals(state);
1130            ctx.request_frame();
1131            EventResponse::Handled
1132        }
1133        // Both write arms are gated on the field being editable. The node
1134        // advertises neither action while `read_only` is set, but an adapter
1135        // dispatches what the technology asks for rather than what the node
1136        // offered — AT-SPI publishes `EditableText` off the interface set, not
1137        // off the action list — so a read-only `SpinBox`, `DateEdit`,
1138        // `TimeEdit`, `DateTimeEdit` or `DateRangeEdit` had its value rewritten
1139        // by anything that tried. Refusing here is the only place that covers
1140        // every host at once, and it reports the refusal instead of a silent
1141        // no-op.
1142        (Action::SetValue | Action::ReplaceSelectedText, _) if state.borrow().read_only => {
1143            EventResponse::Ignored
1144        }
1145        (Action::SetValue, Some(ActionData::Value(value))) => {
1146            // Snapshot the document before overwriting it. A host that refuses
1147            // the string has to be able to put the field back, and it cannot do
1148            // it from its own side: its revert writes the bound
1149            // `Signal<String>`, which still holds the *pre-edit* display at
1150            // this point (the document→signal sync is deferred to the next
1151            // frame tick), so the write is a no-op and the rejected string is
1152            // what the deferred sync then publishes.
1153            let before = {
1154                let st = state.borrow();
1155                st.document.to_plain_text().unwrap_or_default()
1156            };
1157            let st = state.borrow();
1158            st.cursor.select(SelectionType::Document);
1159            let _ = st.cursor.insert_text(value.as_ref());
1160            // A composite whose text only *projects* a typed value handles the
1161            // write itself, because an assistive technology's `SetValue` is a
1162            // finished edit, not a keystroke: without it a `SpinBox` would
1163            // show the new number, keep the old value until the next blur, and
1164            // never fire `on_value_changed`. It is handed the string, not left
1165            // to read the bound signal, which this edit has not synced yet.
1166            let host = st.on_access_set_value.clone();
1167            drop(st);
1168            sync_cursor_signals(state);
1169            ctx.request_frame();
1170            // The host owns the verdict: it parses, clamps and — on a string it
1171            // cannot read — reverts the display to the value the composite
1172            // still holds. Reporting `Handled` regardless told the technology a
1173            // write had landed when the field had just thrown it away, so
1174            // Orca's value entry and macOS's `setAccessibilityValue:` both read
1175            // back success on `"twelve"`.
1176            match host {
1177                Some(host) if !host(value.as_ref(), ctx) => {
1178                    // Refused, so the field must not keep the refused string:
1179                    // reporting `Ignored` over a document still showing it is
1180                    // the same lie the other way round.
1181                    let st = state.borrow();
1182                    st.cursor.select(SelectionType::Document);
1183                    let _ = st.cursor.insert_text(&before);
1184                    drop(st);
1185                    sync_cursor_signals(state);
1186                    EventResponse::Ignored
1187                }
1188                _ => EventResponse::Handled,
1189            }
1190        }
1191        (Action::ReplaceSelectedText, Some(ActionData::Value(value))) => {
1192            // Insert at the caret, replacing the active selection (if
1193            // any) — NOT the whole document like `SetValue`. This is the
1194            // AT-SPI (Linux) / UIA (Windows) braille-keyboard and
1195            // dictation insertion path; macOS routes insertion through
1196            // `SetValue` instead, so this never fires there. We advertise
1197            // the action in `accessibility()`, so we must service it.
1198            let st = state.borrow();
1199            let _ = st.cursor.insert_text(value.as_ref());
1200            drop(st);
1201            sync_cursor_signals(state);
1202            ctx.request_frame();
1203            EventResponse::Handled
1204        }
1205        (Action::Focus, _) => {
1206            if let Some(id) = state.borrow().field_widget_id {
1207                ctx.request_focus(id);
1208            }
1209            EventResponse::Handled
1210        }
1211        _ => EventResponse::Ignored,
1212    }
1213}
1214
1215/// Build a fresh right-click context menu widget. Called from the
1216/// `.context_menu(...)` factory on every right-click, so each open
1217/// reads live `has_selection` / `is_empty` state when computing each
1218/// item's enabled flag.
1219fn build_context_menu_widget(state: &SharedState) -> Box<dyn Widget> {
1220    let st = state.borrow();
1221    let has_selection = st.cursor.has_selection();
1222    let doc_non_empty = !st.document.to_plain_text().unwrap_or_default().is_empty();
1223    // Secure fields suppress Cut / Copy while masked (still allowed when
1224    // revealed or when the developer opted in via `allow_copy`).
1225    let copy_allowed = st.copy_allowed();
1226    drop(st);
1227
1228    Box::new(
1229        MenuList::new()
1230            .item(menu_row_cut(state).enabled(has_selection && copy_allowed))
1231            .item(menu_row_copy(state).enabled(has_selection && copy_allowed))
1232            .item(menu_row_paste(state))
1233            .item(MenuSeparator)
1234            .item(menu_row_select_all(state).enabled(doc_non_empty)),
1235    )
1236}
1237
1238// The four command rows, shared by the right-click menu above and the touch
1239// selection toolbar in `touch.rs`. One implementation of each command, reached
1240// two ways — the menu decides enablement from a snapshot taken as it opens, the
1241// toolbar decides *visibility* from the controller's derived
1242// `ClipboardActions`, and neither owns the command itself.
1243
1244pub(crate) fn menu_row_cut(state: &SharedState) -> MenuItem {
1245    let state = state.clone();
1246    MenuItem::new(tr_widget!(menu_cut()))
1247        .for_shortcut(StandardEdit::Cut.id())
1248        .on_activate_fn(move |ctx| {
1249            {
1250                let mut st = state.borrow_mut();
1251                keyboard::clipboard_cut(&mut st, ctx);
1252            }
1253            sync_cursor_signals(&state);
1254            ctx.request_frame();
1255        })
1256}
1257
1258pub(crate) fn menu_row_copy(state: &SharedState) -> MenuItem {
1259    let state = state.clone();
1260    MenuItem::new(tr_widget!(menu_copy()))
1261        .for_shortcut(StandardEdit::Copy.id())
1262        .on_activate_fn(move |ctx| {
1263            let mut st = state.borrow_mut();
1264            keyboard::clipboard_copy(&mut st, ctx);
1265        })
1266}
1267
1268pub(crate) fn menu_row_paste(state: &SharedState) -> MenuItem {
1269    let state = state.clone();
1270    MenuItem::new(tr_widget!(menu_paste()))
1271        .for_shortcut(StandardEdit::Paste.id())
1272        .on_activate_fn(move |ctx| {
1273            {
1274                let mut st = state.borrow_mut();
1275                keyboard::clipboard_paste(&mut st, ctx);
1276            }
1277            sync_cursor_signals(&state);
1278            ctx.request_frame();
1279        })
1280}
1281
1282pub(crate) fn menu_row_select_all(state: &SharedState) -> MenuItem {
1283    let state = state.clone();
1284    MenuItem::new(tr_widget!(menu_select_all()))
1285        .for_shortcut(StandardEdit::SelectAll.id())
1286        .on_activate_fn(move |ctx| {
1287            {
1288                let st = state.borrow();
1289                st.cursor.select(SelectionType::Document);
1290            }
1291            sync_cursor_signals(&state);
1292            ctx.request_frame();
1293        })
1294}
1295
1296/// Run the validator on the bound text and update the feedback signal.
1297///
1298/// On `Corrected`, also writes the corrected text back to the bound
1299/// signal — the field's external→internal sync effect picks this up
1300/// and rewrites the document in the next frame. On `Invalid`, the
1301/// text is left as-typed; composites that want a "revert on invalid"
1302/// behaviour observe the feedback signal and rewrite the text from
1303/// their own source of truth (e.g., `DateEdit` reformats from its
1304/// `Signal<Option<Date>>`).
1305fn run_validator_and_apply(
1306    validator: &ValidatorFn,
1307    bound_text: &Signal<String>,
1308    feedback: &Signal<ValidationFeedback>,
1309) {
1310    let raw = bound_text.get();
1311    match validator(&raw) {
1312        ValidationOutcome::Valid => {
1313            feedback.set(ValidationFeedback::Valid);
1314        }
1315        ValidationOutcome::Corrected { corrected, message } => {
1316            // Write the corrected text first so observers of the
1317            // bound signal see the new value before the feedback
1318            // signal flips. Composites that bind to BOTH signals
1319            // (rare) will see a consistent pair: text + correction
1320            // notice describing the change.
1321            if bound_text.get() != corrected {
1322                bound_text.set(corrected);
1323            }
1324            feedback.set(ValidationFeedback::Corrected {
1325                message,
1326                since: std::time::Instant::now(),
1327            });
1328        }
1329        ValidationOutcome::Invalid { message } => {
1330            feedback.set(ValidationFeedback::Invalid { message });
1331        }
1332    }
1333}
1334
1335/// Build the worst-case-glyph version of an [`InputMask`] for
1336/// natural-width measurement: every editable slot holds the widest
1337/// plausible character its class can accept, and every fixed slot
1338/// holds its literal. Used by `build()` to size the field's
1339/// intrinsic envelope so a fully-typed value never overflows the
1340/// reported natural width.
1341///
1342/// Per-class worst-case glyph (Inter and most UI sans-serifs):
1343/// - `Digit` → `0` (tabular figures are constant-width, but `0` is
1344///   representative for fonts that aren't)
1345/// - `Letter` / `Alphanumeric` / `Any` → `M` (widest cap glyph)
1346/// - `HexDigit` → `0`
1347fn worst_case_template(mask: &InputMask) -> String {
1348    let mut s = String::with_capacity(mask.len());
1349    for pos in mask.positions() {
1350        match pos {
1351            MaskPosition::Editable { class, .. } => {
1352                s.push(match class {
1353                    MaskClass::Digit | MaskClass::HexDigit => '0',
1354                    MaskClass::Letter | MaskClass::Alphanumeric | MaskClass::Any => 'M',
1355                });
1356            }
1357            MaskPosition::Fixed(c) => s.push(*c),
1358        }
1359    }
1360    s
1361}
1362
1363/// Measure the advance width of `text` in logical pixels using the
1364/// app-wide `SharedTypesetter` (the same backend the field paints
1365/// with). Falls back to a per-character-class heuristic when no
1366/// typesetter is installed (headless tests) so the caller still gets
1367/// a non-zero width and any natural-width / cap logic behaves
1368/// reasonably even there. The fallback weights match Inter's body
1369/// proportions closely enough that the difference between an
1370/// underscore and a wide cap glyph (`M`) shows up in headless tests
1371/// — important for verifying the worst-case-glyph mask measurement
1372/// without booting a typesetter.
1373fn measure_width_px(ctx: &mut BuildContext, text: &str, style: &TextStyle) -> f32 {
1374    if text.is_empty() {
1375        return 0.0;
1376    }
1377    if let Some(ts) = ctx.app_state::<SharedTypesetter>() {
1378        let backend = ts.as_text_backend();
1379        let layout = backend.borrow_mut().layout_single_line(text, style, None);
1380        return layout.width;
1381    }
1382    let em = style.size;
1383    text.chars()
1384        .map(|c| match c {
1385            ' ' => 0.30,
1386            '_' => 0.45,
1387            ':' | '.' | ',' | ';' | '/' | '|' | '!' | 'i' | 'l' | 'I' => 0.30,
1388            '0'..='9' => 0.55,
1389            'M' | 'W' | 'm' | 'w' => 0.85,
1390            'A'..='Z' => 0.65,
1391            'a'..='z' => 0.50,
1392            _ => 0.55,
1393        })
1394        .map(|w: f32| w * em)
1395        .sum()
1396}
1397
1398#[cfg(test)]
1399mod text_run_tests {
1400    use super::*;
1401    use std::cell::RefCell;
1402    use teksilo_canvas::{MockTextBackend, SizeProposal};
1403    use teksilo_core::accesskit::Role;
1404    use teksilo_core::signal::Signal;
1405    use teksilo_core::widget_id::WidgetId;
1406    use teksilo_core::widget_tree::WidgetTree;
1407    use teksilo_text::text_document::MoveMode;
1408
1409    fn tree_with_mock_backend() -> WidgetTree {
1410        WidgetTree::new()
1411            .with_theme(teksilo_core::presets::intui::light())
1412            .with_text_backend(Rc::new(RefCell::new(MockTextBackend::new())))
1413    }
1414
1415    fn state_of(tree: &WidgetTree, id: WidgetId) -> SharedState {
1416        tree.widget_as_any(id)
1417            .and_then(|w| w.downcast_ref::<TextInputField>())
1418            .map(|field| field.state().clone())
1419            .expect("a built TextInputField")
1420    }
1421
1422    #[test]
1423    fn a_caret_past_255_chars_lands_in_the_second_chunk() {
1424        // `accesskit_consumer` probes a position's character index as a `u8`,
1425        // so the emitter splits a long line into 255-character runs. The
1426        // field still speaks in document character offsets, and a caret at
1427        // 260 has to resolve to the run that holds it — reporting it against
1428        // the first run would put the caret 255 characters behind the text.
1429        let mut tree = tree_with_mock_backend();
1430        let id = tree.add(TextInputField::new(Signal::new("a".repeat(300))));
1431        tree.layout(SizeProposal::exact(200.0, 20.0));
1432
1433        state_of(&tree, id)
1434            .borrow_mut()
1435            .cursor
1436            .set_position(260, MoveMode::MoveAnchor);
1437
1438        let update = tree.sync_accessibility();
1439        let (_, input) = update
1440            .nodes
1441            .iter()
1442            .find(|(_, node)| node.role() == Role::TextInput)
1443            .expect("the field reports a text-input node");
1444        let runs = input.children();
1445        assert_eq!(runs.len(), 2, "300 characters split at the 255 cap");
1446
1447        let selection = input
1448            .text_selection()
1449            .expect("the field exposes its caret to assistive technology");
1450        assert_eq!(selection.focus.node, runs[1]);
1451        assert_eq!(selection.focus.character_index, 5);
1452    }
1453
1454    #[test]
1455    fn a_protected_field_emits_its_mask_and_never_the_secret() {
1456        // A masked field shows a reader what it shows a sighted user: one
1457        // mask character per character of the secret. The runs carry that
1458        // mask, which is what gives the node a Text interface on AT-SPI and
1459        // lets a reader hear each keystroke; nothing in the node or its runs
1460        // may carry the plaintext.
1461        let mut tree = tree_with_mock_backend();
1462        let _id = tree
1463            .add(TextInputField::new(Signal::new("hunter2".to_string())).secure(EchoMode::Masked));
1464        tree.layout(SizeProposal::exact(200.0, 20.0));
1465
1466        let update = tree.sync_accessibility();
1467        let (_, field) = update
1468            .nodes
1469            .iter()
1470            .find(|(_, node)| node.role() == Role::PasswordInput)
1471            .expect("a masked field reports Role::PasswordInput");
1472        assert_eq!(field.value(), Some("•••••••"));
1473        let runs: Vec<_> = update
1474            .nodes
1475            .iter()
1476            .filter(|(_, node)| node.role() == Role::TextRun)
1477            .collect();
1478        assert_eq!(runs.len(), 1, "the mask is one run");
1479        assert!(
1480            field.children().contains(&runs[0].0),
1481            "the run hangs off the field, where a text change can reach a reader"
1482        );
1483        assert_eq!(runs[0].1.value(), Some("•••••••"));
1484        for (_, node) in &update.nodes {
1485            for text in [node.value(), node.label()].into_iter().flatten() {
1486                assert!(!text.contains("hunter"), "the secret leaked: {text:?}");
1487            }
1488        }
1489        assert!(
1490            field.text_selection().is_some(),
1491            "the caret is reported against the mask, one character per character"
1492        );
1493    }
1494
1495    #[test]
1496    fn a_no_echo_field_emits_one_empty_run_and_no_value() {
1497        let mut tree = tree_with_mock_backend();
1498        let _id = tree
1499            .add(TextInputField::new(Signal::new("hunter2".to_string())).secure(EchoMode::NoEcho));
1500        tree.layout(SizeProposal::exact(200.0, 20.0));
1501
1502        let update = tree.sync_accessibility();
1503        let (_, field) = update
1504            .nodes
1505            .iter()
1506            .find(|(_, node)| node.role() == Role::PasswordInput)
1507            .expect("a NoEcho field reports Role::PasswordInput");
1508        assert_eq!(field.value(), None, "NoEcho hides even the length");
1509        let runs: Vec<_> = update
1510            .nodes
1511            .iter()
1512            .filter(|(_, node)| node.role() == Role::TextRun)
1513            .collect();
1514        assert_eq!(runs.len(), 1);
1515        assert_eq!(runs[0].1.value(), Some(""));
1516    }
1517}
1518
1519#[cfg(test)]
1520mod window_active_tests;
1521
1522/// **The appearance knobs** — text style, alignment, colour — read back from
1523/// the engine and the painted frame.
1524#[cfg(test)]
1525mod appearance_tests;
1526
1527/// **What a screen reader is told as the field is edited**, read through
1528/// `accesskit_consumer` the way the AT-SPI adapter reads it: caret moves,
1529/// selections, the first character typed into an empty field, and a masked
1530/// field's echo.
1531#[cfg(test)]
1532mod reader_tests;
1533
1534/// **Pointer editing, both devices.** The mouse half is a baseline the touch
1535/// work needed before it could change `mouse.rs`: nothing in this stack
1536/// dispatched a press-move-release pair or a multi-click at a field, so
1537/// "the suite still passes" was satisfiable with drag-select broken.
1538#[cfg(test)]
1539mod pointer_tests;
1540
1541/// **A key the platform decorates with control text must still bubble.**
1542///
1543/// These dispatch `KeyDown` with the `text` a real keyboard carries. Every
1544/// synthetic helper in the workspace sends `text: None`, which skips the branch
1545/// under test entirely — so a test written with `press_key` passes on the bug.
1546#[cfg(test)]
1547mod key_text_bubbling_tests;
1548
1549/// **The keyboard's route to the context menu acts where the caret is.** Read
1550/// as a screen reader reads the field, through the platform adapter.
1551#[cfg(test)]
1552mod context_menu_tests;
1553
1554/// **Clipboard, undo and select-all follow the user's bindings**: they come
1555/// through the rebindable `edit.*` shortcuts, not a chord table of the field's
1556/// own.
1557#[cfg(test)]
1558mod standard_edit_tests;
1559
1560/// **Only Tab selects the text**: a programmatic, assistive or restored
1561/// focus leaves it as it was.
1562#[cfg(test)]
1563mod focus_origin_tests;
1564
1565/// A live handle on a [`TextInputField`] — its text-editing commands, for a
1566/// caller outside the widget.
1567///
1568/// Every method is a no-op before the field is built (and after it is
1569/// destroyed), which is the honest answer rather than a panic: a menu row bound
1570/// to a field that is no longer on screen should do nothing, not crash.
1571#[derive(Clone)]
1572pub struct TextFieldHandle {
1573    slot: std::rc::Rc<std::cell::RefCell<Option<SharedState>>>,
1574    focus_signal: Signal<bool>,
1575}
1576
1577impl std::fmt::Debug for TextFieldHandle {
1578    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1579        f.debug_struct("TextFieldHandle")
1580            .field("live", &self.slot.borrow().is_some())
1581            .field("focused", &self.focus_signal.get())
1582            .finish()
1583    }
1584}
1585
1586impl TextFieldHandle {
1587    /// A handle not yet attached to any field — for a composing widget that
1588    /// hands one out before building the field it will delegate to. Every
1589    /// method answers "nothing" until [`TextInputField::share_handle`] binds it.
1590    pub fn detached() -> Self {
1591        Self {
1592            slot: std::rc::Rc::new(std::cell::RefCell::new(None)),
1593            focus_signal: Signal::new(false),
1594        }
1595    }
1596
1597    /// `true` while this field holds the keyboard focus. Observable, so a
1598    /// router can follow the caret without polling.
1599    pub fn focused_signal(&self) -> Signal<bool> {
1600        self.focus_signal.clone()
1601    }
1602
1603    /// Is the widget built and still alive?
1604    pub fn is_live(&self) -> bool {
1605        self.slot.borrow().is_some()
1606    }
1607
1608    fn with<R>(&self, f: impl FnOnce(&mut TextInputState) -> R) -> Option<R> {
1609        let slot = self.slot.borrow();
1610        let state = slot.as_ref()?;
1611        let mut st = state.borrow_mut();
1612        Some(f(&mut st))
1613    }
1614
1615    /// The field's current text.
1616    pub fn text(&self) -> String {
1617        self.with(|st| st.document.to_plain_text().unwrap_or_default())
1618            .unwrap_or_default()
1619    }
1620
1621    /// The field's text with every keystroke in it, including those the
1622    /// frame tick has not inserted yet, which this inserts first.
1623    ///
1624    /// For a composite that acts on what was typed from inside a key
1625    /// handler: two keys delivered in one batch reach it before any frame
1626    /// runs, and reading [`text`](Self::text), or the bound signal, which
1627    /// lags further still, would act on the text as it stood before the
1628    /// first of them.
1629    ///
1630    /// `None` when the field is not built, or when its state is borrowed by
1631    /// a caller further up the stack. Such a caller is the field itself,
1632    /// mid-edit, so there is nothing settled to read; the answer is "not
1633    /// now" rather than a panic.
1634    pub(crate) fn text_as_typed(&self) -> Option<String> {
1635        let slot = self.slot.try_borrow().ok()?;
1636        let state = slot.as_ref()?;
1637        let mut st = state.try_borrow_mut().ok()?;
1638        st.apply_pending_chars();
1639        Some(st.document.to_plain_text().unwrap_or_default())
1640    }
1641
1642    /// Is any text selected right now?
1643    pub fn has_selection(&self) -> bool {
1644        self.with(|st| st.cursor.has_selection()).unwrap_or(false)
1645    }
1646
1647    /// May this field's content be copied at all? A password field says no —
1648    /// see [`TextInputField::allow_copy`].
1649    pub fn allows_copy(&self) -> bool {
1650        self.with(|st| st.allow_copy).unwrap_or(false)
1651    }
1652
1653    /// Is the field refusing edits? Cut and Paste are meaningless when it is.
1654    pub fn is_read_only(&self) -> bool {
1655        self.with(|st| st.read_only).unwrap_or(true)
1656    }
1657
1658    /// Select the whole field.
1659    pub fn select_all(&self) {
1660        self.with(|st| st.cursor.select(SelectionType::Document));
1661    }
1662
1663    /// Copy the selection to the clipboard.
1664    pub fn copy(&self, ctx: &EventContext) {
1665        self.with(|st| keyboard::clipboard_copy(st, ctx));
1666    }
1667
1668    /// Cut the selection to the clipboard.
1669    pub fn cut(&self, ctx: &EventContext) {
1670        self.with(|st| keyboard::clipboard_cut(st, ctx));
1671    }
1672
1673    /// Paste over the selection.
1674    pub fn paste(&self, ctx: &EventContext) {
1675        self.with(|st| keyboard::clipboard_paste(st, ctx));
1676    }
1677
1678    /// Undo this field's own last edit.
1679    pub fn undo(&self) {
1680        self.with(|st| {
1681            let _ = st.document.undo();
1682        });
1683    }
1684
1685    /// Redo this field's own last undone edit.
1686    pub fn redo(&self) {
1687        self.with(|st| {
1688            let _ = st.document.redo();
1689        });
1690    }
1691
1692    /// Is there anything to undo? Debounced like the editor's twin.
1693    pub fn can_undo(&self) -> Signal<bool> {
1694        self.with(|st| st.can_undo.clone())
1695            .unwrap_or_else(|| Signal::new(false))
1696    }
1697
1698    /// Is there anything to redo?
1699    pub fn can_redo(&self) -> Signal<bool> {
1700        self.with(|st| st.can_redo.clone())
1701            .unwrap_or_else(|| Signal::new(false))
1702    }
1703}
1704
1705// ── The framework's uniform view of a text-editing widget ────────────────────
1706
1707impl teksilo_core::text_surface::TextSurface for TextFieldHandle {
1708    fn can_undo(&self) -> bool {
1709        TextFieldHandle::can_undo(self).get()
1710    }
1711
1712    fn can_redo(&self) -> bool {
1713        TextFieldHandle::can_redo(self).get()
1714    }
1715
1716    fn undo(&self) {
1717        TextFieldHandle::undo(self);
1718    }
1719
1720    fn redo(&self) {
1721        TextFieldHandle::redo(self);
1722    }
1723
1724    fn has_selection(&self) -> bool {
1725        TextFieldHandle::has_selection(self)
1726    }
1727
1728    fn is_read_only(&self) -> bool {
1729        TextFieldHandle::is_read_only(self)
1730    }
1731
1732    fn allows_copy(&self) -> bool {
1733        TextFieldHandle::allows_copy(self)
1734    }
1735
1736    fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
1737        TextFieldHandle::cut(self, ctx);
1738    }
1739
1740    fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
1741        TextFieldHandle::copy(self, ctx);
1742    }
1743
1744    fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
1745        TextFieldHandle::paste(self, ctx);
1746    }
1747
1748    /// A one-line field carries no formatting to strip, so the plain paste
1749    /// *is* the paste. Answering "nothing" here would make Edit ▸ Paste without
1750    /// formatting silently dead over a rename box.
1751    fn paste_plain(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
1752        TextFieldHandle::paste(self, ctx);
1753    }
1754
1755    fn select_all(&self) {
1756        TextFieldHandle::select_all(self);
1757    }
1758}