Input Widgets

Overview

WIP. Text input is functional with cursor, placeholder, and focus callbacks; the widget surface described here is stable.

TextInput and NumberInput are the two ways to collect typed user input. Both render as a single-line box, both raise the same lifecycle events, and NumberInput is implemented as a thin wrapper that adds numeric validation on top of TextInput.

TextInput

TextInput is the one-line text field. State and callbacks live together in the widget value:

pub struct TextInput {
    pub text_input_state: TextInputStateWrapper,
    pub placeholder_style: CssPropertyWithConditionsVec,
    pub container_style:   CssPropertyWithConditionsVec,
    pub label_style:       CssPropertyWithConditionsVec,
}

pub struct TextInputState {
    pub text:        U32Vec,
    pub placeholder: OptionString,
    pub max_len:     usize,
    pub selection:   OptionTextInputSelection,
    pub cursor_pos:  usize,
}

The text buffer is stored as U32Vec (Unicode scalar values cast to u32) so cursor positions are character-based and survive multi-byte UTF-8 edits without re-indexing.

Building one

use azul::prelude::*;

let dom = TextInput::create()
    .with_text("hello".into())
    .with_placeholder("type something...".into())
    .dom();

create() returns a TextInput with platform defaults. with_text replaces the buffer; set_text is its mutable counterpart.

Three callbacks

TextInput exposes three callbacks.

  • on_text_input fires when a printable character is typed. It returns OnTextInputReturn { update, valid }.
  • on_virtual_key_down fires when a non-printable key (arrows, Enter, Esc) is pressed. It returns OnTextInputReturn { update, valid }.
  • on_focus_lost fires when focus moves away. It returns Update.

OnTextInputReturn { valid: TextInputValid::No } rejects the just-typed character. The framework rolls the buffer back to the pre-edit state and re-renders. Yes keeps the edit.

use azul::prelude::*;

extern "C" fn validate(
    _data: RefAny, _info: CallbackInfo, state: TextInputState,
) -> OnTextInputReturn {
    let valid = if state.get_text().chars().all(|c| c.is_alphanumeric()) {
        TextInputValid::Yes
    } else {
        TextInputValid::No
    };
    OnTextInputReturn { update: Update::DoNothing, valid }
}

let dom = TextInput::create()
    .with_on_text_input(RefAny::new(()), validate)
    .dom();

By default, the framework writes the new character into the buffer before on_focus_lost and on_virtual_key_down fire. Set update_text_input_before_calling_focus_lost_fn = false if your callback needs to compare against the old buffer.

Cursor and selection

cursor_pos is the character index. selection is OptionTextInputSelection, which is either All or a FromTo range carrying dir_from and dir_to.

Styling

The default style is a white box with a thin border that turns blue on focus. Three slots accept CSS replacements.

  • container_style targets the outer wrapper (border, padding, background).
  • label_style targets the actual text node.
  • placeholder_style targets the placeholder text shown when empty.

NumberInput

NumberInput wraps TextInput and adds f32 validation. Construction takes the initial value:

use azul::prelude::*;

let dom = NumberInput::create(42.0).dom();

State

pub struct NumberInputState {
    pub previous: f32,
    pub number:   f32,
    pub min:      f32,
    pub max:      f32,
}

previous is set to the prior number whenever a successful edit commits, so the value-change callback can compute deltas without storing its own history.

Default min: the constructor leaves min at 0.0. Set it to a negative value if your input must accept negative numbers.

Callbacks

NumberInput exposes its own value-change and focus-lost hooks, plus the underlying TextInput's text-input and virtual-key-down hooks.

  • with_on_value_change(data, fn) fires after a typed string parses successfully as f32 and lies within [min, max].
  • with_on_focus_lost(data, fn) fires when the input loses focus, regardless of validity.
  • with_on_text_input(data, fn) is forwarded to the underlying TextInput.
  • with_on_virtual_key_down(data, fn) is forwarded to the underlying TextInput.
use azul::prelude::*;

extern "C" fn on_change(
    _d: RefAny, 
    _i: CallbackInfo, 
    state: NumberInputState,
) -> Update {
    let _ = state.number - state.previous;
    Update::DoNothing
}

let dom = NumberInput::create(50.0)
    .with_on_value_change(RefAny::new(()), on_change)
    .dom();

Invalid input is rejected by the underlying TextInput's on_text_input callback returning TextInputValid::No, so the buffer never enters an unparseable state.

Picking between them

  • For free-form text, names, or search, use TextInput.
  • For numbers with a known range, use NumberInput.
  • For typed URLs, emails, or regex-validated input, use TextInput with a custom on_text_input returning TextInputValid::No for invalid characters.
  • For a date or time, build one out of three NumberInputs; there's no date/time widget yet.

Coming Up Next

  • Structural Widgets — Panels, splitters, tab views, list views, tree views
  • Text Input — Editable text, IME, and the selection model
  • Events — Callbacks, event filters, and how state triggers relayout

Back to guide index