TextInputField
TextInputField — editable single-line text surface primitive.
Public types
| Kind | Name |
|---|---|
enum | InputPurpose — The semantic purpose of a text field, surfaced to assistive technology as a specialised AccessKit role (WCAG 1.3.5 Identify Input Purpose / EN 301 549) |
enum | EchoMode — How a secure (TextInputField::secure) field echoes typed characters |
enum | AtRevealPolicy — How a revealed secure field reports to assistive technology |
struct | TextInputField — Editable single-line text surface primitive |
struct | TextFieldHandle — A live handle on a TextInputField — its text-editing commands, for a caller outside the widget |
enum | ValidationOutcome — What a validator returns for a given commit attempt |
enum | ValidationFeedback — What composites render |
Public functions
TextInputField
TextFieldHandle
| Returns | Function |
|---|---|
| Constructors | |
Self | detached() |
| Methods | |
Signal<bool> | focused_signal() |
bool | is_live() |
String | text() |
bool | has_selection() |
bool | allows_copy() |
bool | is_read_only() |
select_all() | |
copy(ctx: &EventContext) | |
cut(ctx: &EventContext) | |
paste(ctx: &EventContext) | |
undo() | |
redo() | |
Signal<bool> | can_undo() |
Signal<bool> | can_redo() |
ValidationFeedback
| Returns | Function |
|---|---|
| Methods | |
bool | is_invalid() |
bool | is_corrected() |
Option<String> | message() |
Detailed description
This is the raw editing primitive that powers the styled
TextInput composite and any
other widget that needs inline editable text — SpinBox being
the primary second consumer.
Unlike TextInput, TextInputField paints no frame, no
placeholder overlay, no validation border, and hosts no trailing
slots: it is the focusable text area only. Compose it yourself
with RectWidget, Padding, icons, clear buttons, etc. to
build a styled control. Focus indication is the composite's
responsibility — the Int UI convention is to thicken the
enclosing frame's border to focus_ring_width and recolor it
to the accent focus-ring color.
Features:
- Bound
Signal<String>for two-way text binding. - Full keyboard editing (arrow keys, Home/End, Backspace/Delete, Ctrl+X/C/V, Ctrl+A, Ctrl+Z/Y), IME commit, and pointer caret positioning and drag-select.
- Optional per-character input filter
(
TextInputField::char_filter), max-length cap (TextInputField::max_length), and read-only mode (TextInputField::read_only). - Commit hooks: Enter fires
on_submit_fnand focus loss fireson_blur_fn. - Non-editable trailing
suffix, rendered flush-right inside the field's bounds (Qt'sQSpinBox::suffix). Caret cannot enter it; clicks past the text end clamp to the last character. - Appearance:
text_styleshapes the text in aTextStyleRoleor aTextStyle(size, family, weight) and sizes the field to one line of it;text_alignmentplaces a text narrower than the field at its start, centre or end;text_colorcolours the text and the caret, from a colour, a role or a signal. All three are unset by default, and the field then looks as it always has. - Right-click context menu (Cut / Copy / Paste / Select All).
- AccessKit
Role::TextInputwith value, selection, and character/word boundary metadata.
Example
let text = ctx.signal(String::new());
ctx.add(
TextInputField::new(text.clone())
.placeholder("Enter a name…")
.char_filter(|c| !c.is_ascii_digit())
.on_submit_fn(|ctx| ctx.send_intent(MyIntent::Save)),
);
API reference
📖 Full rustdoc API for this module
pub enum InputPurpose
Import: use teksilo::widgets::InputPurpose;
The semantic purpose of a text field, surfaced to assistive technology as a specialised AccessKit role (WCAG 1.3.5 Identify Input Purpose / EN 301 549).
This is the in-framework-achievable part of SC 1.3.5: a screen reader
announces "email, edit text" instead of a generic "edit text". The FULL
HTML autocomplete-token vocabulary (given-name, postal-code,
cc-number, …) that drives OS/browser autofill has no representation in
AccessKit 0.24 and therefore cannot be exposed from Teksilo — see
docs/a11y/a11y_issues.md. Password entry is configured via
TextInputField::secure, not here.
#![allow(unused)] fn main() { pub enum InputPurpose { /* variants */ } }
Variants
Normal— Ordinary free text (Role::TextInput).Email— Email address (Role::EmailInput).Phone— Telephone number (Role::PhoneNumberInput).Url— URL (Role::UrlInput).Number— Numeric entry — e.g. a quantity or code (Role::NumberInput).Search— Search query (Role::SearchInput).
pub enum EchoMode
Import: use teksilo::widgets::EchoMode;
How a secure (TextInputField::secure) field echoes typed
characters. Mirrors Qt's QLineEdit::EchoMode.
#![allow(unused)] fn main() { pub enum EchoMode { /* variants */ } }
Variants
Masked— Replace every character with the echo glyph (default'•'). The plaintext stays in the boundSignal<String>but never reaches the text engine while masked.NoEcho— Show nothing at all — not even the length. The caret stays at the start. Qt'sNoEcho.RevealWhileTyping— Show plaintext while the field is focused (being edited) and re-mask on blur. Qt'sPasswordEchoOnEdit.
pub enum AtRevealPolicy
Import: use teksilo::widgets::AtRevealPolicy;
How a revealed secure field reports to assistive technology.
#![allow(unused)] fn main() { pub enum AtRevealPolicy { /* variants */ } }
Variants
SwapRole— When revealed, expose the field as a normalRole::TextInputcarrying the plaintext value — matching what is visibly on screen and the webtype=password ↔ type=textswap. When masked, it reverts toRole::PasswordInput. (Default.)AlwaysProtected— Always reportRole::PasswordInputand never expose plaintext to assistive tech, even while visually revealed. Higher confidentiality at the cost of consistency with the screen.
pub struct TextInputField
Import: use teksilo::widgets::TextInputField;
Editable single-line text surface primitive.
See the module docs for the full feature list and a
compositional example.
#![allow(unused)] fn main() { pub struct TextInputField { /* fields */ } }
Methods
pub fn new(text: Signal<String>) -> Self
Construct a new field bound to text.
pub fn placeholder(mut self, text: impl Into<String>) -> Self
Declarative placeholder string. The field itself paints
nothing for placeholder — that visual is the composite
parent's responsibility (TextInput overlays a
TextWidget). The string is still stored here and published
via AccessKit's placeholder property so screen readers
announce it.
pub fn enabled(mut self, enabled: impl Into<Prop<bool>>) -> Self
Set the enabled state, statically or reactively. Disabled blocks input and AccessKit interaction. Forwarded to the arena at build time.
pub fn read_only(mut self, read_only: bool) -> Self
Mark the field read-only. Caret and selection still work; inserts, deletes, paste, undo/redo, and cut are all no-ops.
pub fn max_length(mut self, max_length: usize) -> Self
Hard cap on document length in chars (grapheme count is
approximated — each char counts as one unit, matching
String::chars().count()).
pub fn on_submit_fn(mut self, f: impl Fn(&mut EventContext) + 'static) -> Self
Closure fired on Enter. Unlike on_blur_fn, this does
not move focus — the field stays focused and the caret
stays where it was.
pub fn on_access_set_value( mut self, f: impl Fn(&str, &mut EventContext) -> bool + 'static, ) -> Self
Handle an assistive technology's whole-value write, given the string it set.
Leave it unset for a field whose bound Signal<String> is the
value: the write has already landed and there is nothing to derive.
Install one for a field whose text is a projection of a typed value,
as SpinBox and the date and time editors are. There the string is
only a display of the real value, so without this an
Action::SetValue resolved against the inner text node changes what is
shown, leaves the typed value stale until the next blur, and never
fires the host's change callback — an assistive technology or an
automation client sees a success and the wrong value. The composite's
own node handles SetValue properly; this closes the same door on the
text node beneath it.
The string is handed over rather than read back from the bound signal because the document→signal sync is deferred to the next frame tick, so a host reading the signal here would parse the text from before this edit and revert.
pub fn on_blur_fn(mut self, f: impl Fn(&mut EventContext) + 'static) -> Self
Closure fired once per focus-loss, after selection/scroll have been reset. SpinBox-style callers parse and reformat here; validators revalidate here.
pub fn char_filter(mut self, f: impl Fn(char) -> bool + 'static) -> Self
Per-character input-filter predicate. Applied uniformly to
keyboard input, IME commits, and clipboard paste so a filtered
field cannot receive disallowed characters through any path.
Composes with max_length and the built-in control/newline
strip (filter runs after the strip). Whole-string validity
(e.g. "at most one decimal point") is a commit-time concern
for on_blur / on_submit.
pub fn suffix(mut self, text: impl Into<Prop<String>>) -> Self
Static non-editable trailing string rendered flush-right
inside the field's bounds (Qt's QSpinBox::suffix). The
caret cannot enter the suffix; clicks past the text end
position the caret at the last editable character.
Accepts a static String/&str or a reactive Signal<String> /
Prop<String>; when bound, the field re-measures the suffix glyphs
and relayouts the editable text viewport each time the signal fires.
Typical use: a SpinBox with special_value_text binds an empty
string to the suffix whenever the value equals min, and the
configured unit string otherwise.
pub fn text_height(mut self, height: f32) -> Self
Override the intrinsic text-area height. The field is a
pure leaf with no theme lookup of its own; by default it
reports DEFAULT_TEXT_HEIGHT. A wrapping composite like
TextInput passes the TEXT_FIELD_HEIGHT recipe constant
(in crate::styles::recipe_text_input_style) minus
border + padding here so the visuals line up with the
rest of the form.
Wins over the line height a text_style would
derive: a composite that passes both asked for this height.
pub fn text_style(mut self, style: impl Into<TextStyleProp>) -> Self
The style the text is shaped in: its size, family and weight.
Accepts a TextStyleRole, resolved
against the theme's typography and re-resolved when the theme changes,
or a fixed TextStyle. The global text scale multiplies the size
either way, as it does for every text in the tree.
The style also sets the field's height: unless
text_height overrides it, the text area is one
line of size × line_height tall, and the shaped line is centred in it.
An 11 px style makes a short field, a 20 px one a tall field.
The family resolves through the app's font registry like any other
text: a family that is not registered falls back to the default face,
with a warning on the console. The weight is honoured when the family
has that weight (a variable face covers every weight). The style's
letter_spacing is not applied to the editable text, and its
line_height sizes the field without spacing the glyphs.
Unset, the field shapes in the text engine's default font at its default size, exactly as before this setting existed.
pub fn text_alignment(mut self, alignment: HAlignment) -> Self
Where the text sits when it is narrower than the field.
HAlignment::Leading puts it at the start of the field — the left
edge in a left-to-right layout, the right edge in a right-to-left one —
HAlignment::Center in the middle and HAlignment::Trailing at
the end. Text wider than the field scrolls horizontally to follow the
caret, as an unaligned field does. The caret, a click, the selection,
the IME candidate window, the touch handles and the character extents a
screen reader is given all follow the placement.
Unset, the text starts at the left edge whatever the layout direction, as it always has.
pub fn text_color(mut self, color: impl Into<ColorProp>) -> Self
The colour of the text and of the caret.
Accepts a Color, a theme role, a
Signal<Color> or a Signal<TextRole>, resolved against the theme
every time the field paints, so a role follows the theme and a signal
repaints the field when it changes. A disabled field paints the theme's
disabled text colour whatever is set here, unless the colour is wrapped
in ColorProp::undimmed.
A different colour at rest and while editing is a signal of the
field's focus, which handle exposes before the field
is built:
let field = TextInputField::new(text);
let focused = field.handle().focused_signal();
let field = field.text_color(focused.map(|f| {
if *f { TextRole::Primary } else { TextRole::Secondary }
}));
Unset, the text is the theme's primary text colour.
pub fn interaction_signal(mut self, signal: Signal<InteractionState>) -> Self
Bind an externally-owned InteractionState signal. The
field writes Focused on focus gain and Idle on loss;
other states (Hovered, Pressed, Disabled) are the
composite's responsibility. When unset, the field owns a
private signal that observers can still read via
interaction, but composites
that drive a focus ring or border color usually want to
push their own.
pub fn input_mask(mut self, mask: impl AsRef<str>) -> Self
Set an input mask (Qt grammar). Constrains accepted characters
per position, auto-derives the empty-state template
(__/__/____ for 99/99/9999), and routes typed chars
through the mask's class filter.
Composes with char_filter: a char must
pass both the mask's per-position class AND the user's
char_filter to be accepted.
On parse error (only the trailing-backslash case in practice), the mask is silently dropped — the field falls back to its no-mask behaviour rather than panicking.
pub fn mask_placeholder(mut self, c: char) -> Self
Override the visible character used for unfilled editable mask
positions. Default: the TEXT_FIELD_MASK_PLACEHOLDER_CHAR
recipe constant (in crate::styles::recipe_text_input_style,
_).
pub fn validator(mut self, f: impl Fn(&str) -> ValidationOutcome + 'static) -> Self
Install a validator. The closure runs on every commit (Enter,
Tab-out, focus loss) and returns a ValidationOutcome that
drives validation_feedback_signal.
Does not run per-keystroke — that's char_filter's
job. Mixing per-keystroke text rewriting with validation
produces caret-jump bugs and is explicitly out of scope.
pub fn secure(mut self, echo_mode: EchoMode) -> Self
Turn this into a secure (password) field with the given
EchoMode. Masking happens at the text-engine layer (one echo
glyph per source char), so the plaintext never reaches the
shaper or glyph atlas while masked, and caret / selection /
hit-test stay correct. Also defaults allow_copy to false and
declares the focused node an ImePurpose::Password surface — the
OS IME stays enabled so non-Latin passwords can still be composed,
with the preedit masked on screen and hidden from AT. Pair with
revealed for a reveal toggle.
pub fn input_purpose(mut self, purpose: InputPurpose) -> Self
Declare the field's semantic InputPurpose (WCAG 1.3.5), which
selects a specialised AccessKit role (EmailInput, PhoneNumberInput,
…) so screen readers announce the field's kind. Ignored while secure
(the password role wins). Does not change IME behaviour — winit's
ImePurpose has no email/number/url variants — nor drive OS autofill,
which AccessKit cannot express (see docs/a11y/a11y_issues.md).
pub fn active_descendant(mut self, active: Signal<Option<WidgetId>>) -> Self
Publish active_descendant pointing at the row a separate list is
currently highlighting — the ARIA combobox pattern.
Keyboard focus stays in this field while arrow keys move a highlight through a listbox elsewhere in the tree (a command palette, a type-ahead picker, a suggestion popup). Assistive technology follows the focused node's active descendant, so the announcement has to be published here, on the node that actually holds focus — not on the composite ancestor that owns the list. Without it the arrow keys move a highlight that is announced to nobody.
Bound at AccessibilityOnly, so moving the highlight re-walks the AT
tree without a rebuild or a repaint. Pair with controls.
pub fn controls(mut self, listbox: Signal<Option<WidgetId>>) -> Self
Publish a controls relation to the listbox this field drives, so an
AT client can navigate from the input to the list it is filtering.
The companion of active_descendant.
pub fn echo_char(mut self, c: char) -> Self
Override the masking glyph (default '•', U+2022). Any
uniform-width character works; the engine emits exactly one per
source char.
pub fn revealed(mut self, revealed: Signal<bool>) -> Self
Bind the reveal toggle. When the signal is true the field
shows plaintext regardless of EchoMode; when false it
masks. Shared with the eye IconButton::visibility_toggle.
pub fn at_reveal_policy(mut self, policy: AtRevealPolicy) -> Self
How a revealed secure field reports to assistive tech. Default
AtRevealPolicy::SwapRole.
pub fn allow_copy(mut self, allow: bool) -> Self
Permit (or forbid) copy / cut. Plain fields default true;
secure flips the default to false. Even when
false, copy is allowed while the field is revealed.
pub fn validation_feedback_signal(&self) -> Signal<ValidationFeedback>
Reactive handle on the published ValidationFeedback state.
Composites bind to this to render the inline feedback strip
below the field. Always present; reads Pristine until the
first commit (or forever if no validator is installed).
pub fn text(&self) -> Signal<String>
The Signal<String> this field is bound to.
pub fn share_handle(mut self, handle: &TextFieldHandle) -> Self
Adopt an existing handle instead of minting one.
For a composing widget — TextInput wraps this field — that must hand
out a handle of its own before it builds the field it will delegate
to. Sharing the slot and the focus signal makes the wrapper's handle and
the field's the same handle, rather than two that agree by accident.
pub fn handle(&self) -> TextFieldHandle
A live handle on this field, valid before and after build.
The counterpart of RichTextEditor::handle, and the reason it exists:
an application that routes Undo, Cut, Copy, Paste and Select All to
"whichever text surface holds the caret" has to be able to drive every
such surface, not only the rich editors. Without this, a menu built for
those commands can only grey them out over a rename field or a search
box while the field's own key handling still works — a menu that lies
about what the keyboard can do.
Like caret_setter, the handle reaches its state through the slot the
widget late-populates, so it may be taken while the tree is being
described and used once it is live.
pub fn interaction(&self) -> Signal<InteractionState>
The interaction signal this field writes on focus changes. Call before inserting the field into the tree.
pub fn caret_position(&self) -> Signal<usize>
Reactive caret position in the field's text (in usize char
offsets). Updates after every keyboard or pointer action that
moves the cursor. Used by composing widgets that need to know
where the caret is — e.g. DateEdit reads this to figure out
which date segment Up/Down should step.
pub fn caret_setter(&self) -> std::rc::Rc<dyn Fn(usize)>
Returns a callable that programmatically sets the caret
position (in char offsets) on the field. Capture this on the
builder BEFORE ctx.add(...) consumes the field; call it
after a programmatic text rewrite to restore the caret to the
right column instead of leaving it at the document end (the
default behaviour of cursor.insert_text).
The returned closure becomes a no-op until build() runs;
after build it walks the field's inner state and moves the
document cursor to position, clamped to the document
length. Used by DateEdit / TimeEdit segment-stepping to
keep the caret within its current segment after Up/Down.
pub struct TextFieldHandle
Import: use teksilo::widgets::TextFieldHandle;
A live handle on a TextInputField — its text-editing commands, for a
caller outside the widget.
Every method is a no-op before the field is built (and after it is destroyed), which is the honest answer rather than a panic: a menu row bound to a field that is no longer on screen should do nothing, not crash.
#![allow(unused)] fn main() { pub struct TextFieldHandle { /* fields */ } }
Methods
pub fn detached() -> Self
A handle not yet attached to any field — for a composing widget that
hands one out before building the field it will delegate to. Every
method answers "nothing" until TextInputField::share_handle binds it.
pub fn focused_signal(&self) -> Signal<bool>
true while this field holds the keyboard focus. Observable, so a
router can follow the caret without polling.
pub fn is_live(&self) -> bool
Is the widget built and still alive?
pub fn text(&self) -> String
The field's current text.
pub fn has_selection(&self) -> bool
Is any text selected right now?
pub fn allows_copy(&self) -> bool
May this field's content be copied at all? A password field says no —
see TextInputField::allow_copy.
pub fn is_read_only(&self) -> bool
Is the field refusing edits? Cut and Paste are meaningless when it is.
pub fn select_all(&self)
Select the whole field.
pub fn copy(&self, ctx: &EventContext)
Copy the selection to the clipboard.
pub fn cut(&self, ctx: &EventContext)
Cut the selection to the clipboard.
pub fn paste(&self, ctx: &EventContext)
Paste over the selection.
pub fn undo(&self)
Undo this field's own last edit.
pub fn redo(&self)
Redo this field's own last undone edit.
pub fn can_undo(&self) -> Signal<bool>
Is there anything to undo? Debounced like the editor's twin.
pub fn can_redo(&self) -> Signal<bool>
Is there anything to redo?
pub enum ValidationOutcome
Import: none — ValidationOutcome is public in a private module of teksilo-widgets and nothing re-exports it.
What a validator returns for a given commit attempt.
#![allow(unused)] fn main() { pub enum ValidationOutcome { /* variants */ } }
Variants
Valid— Input is valid as typed. The field commits unchanged and the feedback signal flips toValidationFeedback::Valid.Corrected{ corrected: String, message: LocalizedString }— Input was accepted after normalization. The field replaces its text withcorrected, the boundSignal<String>observes the new value, and the feedback signal carriesmessagefor composites to surface as a polite announcement. Use for clamping, completion, and reformat. Examples:"12/50/2026"→Corrected { corrected: "12/31/2026", … }for "day clamped to month length";"2026"→Corrected { corrected: "2026-01-01", … }for "year-only completed to start of year".Invalid{ message: LocalizedString }— Input is rejected. The field reverts its text to the pre-edit value and the feedback signal carriesmessagefor composites to surface as an assertive error.
pub enum ValidationFeedback
Import: none — ValidationFeedback is public in a private module of teksilo-widgets and nothing re-exports it.
What composites render. Distinct from ValidationOutcome: the
outcome is the validator's return value (no time concept); the
feedback adds a since instant so the visual layer can decay an
auto-correction announcement after a window without re-running the
validator.
#![allow(unused)] fn main() { pub enum ValidationFeedback { /* variants */ } }
Variants
Pristine— No commit has happened yet, or the user is editing again after a previous outcome (typing always clears prior feedback).Valid— Last commit returnedValidationOutcome::Valid. Composites typically render this identically toPristine— the distinction matters for tests and for callers that want to signal "yes, it's confirmed valid" with a checkmark.Corrected{ message: LocalizedString, since: Instant }— Last commit returnedValidationOutcome::Corrected.sinceis the wall-clock instant the correction was applied; composites use it to decay the visual aftercorrected_pulse_duration_msfrom the theme.Invalid{ message: LocalizedString }— Last commit returnedValidationOutcome::Invalid. Persists until the user edits again or an externalPristinereset.
Methods
pub fn is_invalid(&self) -> bool
Convenience: is this state currently signalling an error?
pub fn is_corrected(&self) -> bool
Convenience: was the last commit auto-corrected?
pub fn message(&self) -> Option<String>
Human-readable message, if any.