-
Notifications
You must be signed in to change notification settings - Fork 0
Guide Forms and Text
Text fields are real editable text in ROSACE: click to focus, type, arrow-key navigation, Shift+arrow selection, Home/End, Cmd/Ctrl+A select-all, and clipboard shortcuts all work out of the box. This chapter covers the two text-editing widgets and the rosace-forms crate that binds them to validation.
Like every other interactive widget in ROSACE, TextInput is controlled — you own the true value (typically a ctx.state atom), pass it in via .value(), and get edits back via .on_change(). It never mutates your state itself.
use rosace::prelude::*;
impl Component for LoginForm {
fn build(&self, ctx: &mut Context) -> Element {
let email = ctx.state(String::new());
Column::new()
.spacing(12.0)
.padding(EdgeInsets::all(24.0))
.child(
TextInput::new()
.placeholder("Email")
.value(email.get())
.on_change({
let email = email.clone();
move |v| email.set(v)
}),
)
.into_element()
}
}Useful builder calls: .placeholder(...), .focused() (seed focus on first paint), .obscure() (password field), .width(...)/.height(...), .background(...)/.border(...)/.focus_color(...), and .keyboard_type(...) (hints the soft keyboard layout on mobile — Email, Numeric, Url, Phone).
TextArea shares the same controlled-value shape as TextInput, plus scrolling for content taller than the box:
TextArea::new()
.placeholder("Write something...")
.value(body.get())
.height(200.0)
.on_change({
let body = body.clone();
move |v| body.set(v)
})It also takes .no_scrollbar() and .scrollbar_color(...) if you want to customize or hide the scrollbar.
Both widgets accept .spans(...) — a closure that inspects the current text (and, after the first call, the byte range that just changed) and returns styled Spans, so you can build a markdown or code editor without the crate knowing anything about markdown or code:
TextArea::new()
.value(source.get())
.spans(|text, _changed_range| my_markdown_tokenizer(text)).cursor_style(CursorStyle { .. }) overrides the caret's width, color, corner radius, blink rate, and shape (Bar/Block/Underline/Custom) per field; unset, it falls back to a CursorStyle stashed on the theme via ThemeData::with_ext (see Theming), then to a built-in default.
rosace-forms gives you a FormField — a named value with attached validation rules — and a Form that aggregates several fields:
use rosace::prelude::*;
use rosace_forms::{Form, FormField, Required, Email, MinLength};
impl Component for SignupForm {
fn build(&self, ctx: &mut Context) -> Element {
let email = FormField::for_ctx(ctx, "email").rule(Required).rule(Email);
let name = FormField::for_ctx(ctx, "name").rule(Required).rule(MinLength(2));
let form = Form::new().field(email.clone()).field(name.clone());
Column::new()
.spacing(12.0)
.padding(EdgeInsets::all(24.0))
.child(TextInput::new().placeholder("Name").field(name))
.child(TextInput::new().placeholder("Email").field(email))
.child(Button::new("Sign up").disabled_if(!form.is_valid()).on_press({
let form = form.clone();
move || { form.submit(|| { /* send it */ }); }
}))
.into_element()
}
}A few things to know about this shape:
-
FormField::for_ctx(ctx, name)creates the field's value/errors/touched state as component state (same call-order rule asctx.state— see Components & State) and subscribes the owning component to it, so writes to the field re-render this component.FormField::new(name)also exists for a throwaway field with no live UI updates. -
.rule(...)attaches aValidator. Built-in validators:Required(non-empty after trim),MinLength(n),MaxLength(n),Contains("substr"),Email(has@and a.after it — a simple check, not a full RFC parser), andRange { min, max }for numeric string fields. -
.field(field)onTextInput/TextAreais the binding: it seeds the widget's value fromfield.get(), installs anon_changethat writes back and re-validates on every keystroke, and validates once immediately so an emptyRequiredfield is already reflected before the user types anything. Call.on_change(...)again after.field(...)if you need both — the later call wins. -
FormFieldclones share state. Theemailyou pass toTextInput::field(...)and theemailcaptured byForm/your submit closure are the same underlying atoms — no manual synchronization needed. -
form.is_valid()reflects the last validation run; because.field(...)validates on every change, it stays live as the user types.form.submit(on_valid)runsvalidate_all()across every field (touching all of them, so an untouched-but-invalid field's error becomes visible after a failed submit) and only callson_validif every field passed.
field.errors() returns the current Vec<FieldError> ({ field, message }) after the last validation. Pair it with is_touched() so a blank required field doesn't show red before the user has had a chance to fill it in:
let mut col = Column::new()
.child(TextInput::new().placeholder("Email").field(email.clone()));
if email.is_touched() && !email.is_valid() {
col = col.child(
Text::new(email.errors().first().map(|e| e.message.clone()).unwrap_or_default())
.color(Color::rgb(213, 67, 61)), // or convert theme.colors.error — see Theming's color-type gotcha
);
}(Form::errors() collects every field's errors in one call if you want a single summary list instead of per-field captions.)
Mobile's native soft-keyboard IME (composition for CJK/emoji input on iOS/Android) and a text-selection magnifier loupe are both named, disclosed deferrals — desktop OS IME (composition, dead keys, CJK input) is real and wired today.
Under the hood: the layered text-editing core (keyboard dispatch, OS IME composition, selection/caret persistence on the render tree) is covered in Widget Protocol.
Next: Animation.
Guide
- Overview
- Getting Started
- Components & State
- Layout & Widgets
- Interaction
- Navigation
- Theming
- Forms & Text Input
- Animation
- Persistence & Networking
- Multi-Platform & the rsc CLI
- Hot Reload
Architecture
- Overview
- Core: Component, Element, Context
- State & Reactivity
- Render Pipeline
- Widget Protocol
- Platform & the App Loop
- The rsc CLI
- Hot Reload
Reference