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}