2 releases
Uses new Rust 2024
| 0.2.1 | Dec 14, 2025 |
|---|---|
| 0.2.0 | Dec 14, 2025 |
#2437 in GUI
Used in 2 crates
495KB
8K
SLoC
waterui-form
A comprehensive form building system for WaterUI applications with automatic component generation, validation, and secure data handling.
Overview
waterui-form provides an ergonomic, type-safe approach to building interactive forms in WaterUI applications. The crate centers around the FormBuilder trait, which enables automatic mapping from Rust data structures to platform-native UI components. It includes specialized pickers (color, date, multi-date), secure input handling with automatic memory zeroing, and a flexible validation system.
Key features:
- Automatic form generation via
#[derive(FormBuilder)]macro - Type-to-component mapping - primitives automatically map to appropriate controls
- Field labels from field names -
user_namebecomes "User Name" - Placeholder text from doc comments - documentation becomes UI hints
- Secure data handling - password fields with bcrypt hashing and memory zeroing
- Validation system - composable validators with error messages
- Specialized pickers - color, date/time, and multi-date selection
- Reactive bindings - forms automatically update when data changes
This crate is part of the WaterUI workspace and integrates tightly with waterui-core for reactivity, waterui-controls for base components, and waterui-layout for composition.
Installation
Add to your Cargo.toml:
[dependencies]
waterui-form = "0.1.0"
# Optional: enable serde support
waterui-form = { version = "0.1.0", features = ["serde"] }
Quick Start
The most common use case is deriving FormBuilder on a struct:
use waterui::prelude::*;
use waterui_form::{FormBuilder, form};
#[derive(Default, Clone, Debug, FormBuilder)]
struct UserProfile {
/// Enter your full name
name: String,
/// Your current age
age: i32,
/// Account is active
active: bool,
}
fn profile_form() -> impl View {
let form_binding = UserProfile::binding();
vstack((
form(&form_binding),
button("Save", move || {
tracing::debug!("Profile: {:?}", form_binding.get());
}),
))
}
This generates a vertical stack with:
- A text field labeled "Name" with placeholder "Enter your full name"
- A stepper labeled "Age" with placeholder "Your current age"
- A toggle labeled "Active" with placeholder "Account is active"
Core Concepts
FormBuilder Trait
The FormBuilder trait is the foundation of the form system. It maps Rust types to UI components:
| Rust Type | UI Component | Description |
|---|---|---|
String, Str |
TextField |
Single-line text input with optional placeholder |
bool |
Toggle |
Boolean switch/checkbox |
i32 |
Stepper |
Integer stepper with +/- buttons |
f32, f64 |
Slider |
Numeric slider (0.0-1.0 range by default) |
Color |
ColorPicker |
Platform-native color selection |
Date |
DatePicker |
Date/time selection (requires time crate types) |
BTreeSet<Date> |
MultiDatePicker |
Multiple date selection |
Secure |
SecureField |
Masked password input with memory zeroing |
The #[derive(FormBuilder)] Macro
The derive macro automatically implements FormBuilder by:
- Converting field names to labels:
user_name→ "User Name" - Extracting doc comments as placeholders:
/// Enter emailbecomes placeholder text - Mapping field types to components: Uses the table above
- Arranging fields in a VStack: Creates a vertical layout of all fields
The macro requires the struct to have named fields and generates an implementation compatible with the Project trait for field-level reactive bindings.
Reactive Bindings and Project
Forms use Binding<T> from the nami crate for reactive state. The Project trait (auto-derived alongside FormBuilder) allows accessing individual field bindings:
let form_binding = UserProfile::binding();
let projected = form_binding.project();
// Access individual field bindings
projected.name.set("Alice".to_string());
projected.age.set(30);
// Read entire form state
let profile = form_binding.get();
Examples
Pre-filled Form Data
Initialize a form with existing data instead of defaults:
use waterui::prelude::*;
use waterui_form::{FormBuilder, form};
#[derive(Default, Clone, Debug, FormBuilder)]
struct LoginForm {
username: String,
password: String,
}
fn login_view() -> impl View {
let initial = LoginForm {
username: "[email protected]".to_string(),
password: String::new(),
};
let form_binding = Binding::new(initial);
vstack((
form(&form_binding),
button("Login", move || {
let credentials = form_binding.get();
tracing::debug!("Logging in as: {}", credentials.username);
}),
))
}
Displaying Form Values Reactively
Use projected bindings to display live form data:
use waterui::prelude::*;
use waterui_form::{FormBuilder, form};
#[derive(Default, Clone, Debug, FormBuilder)]
struct ContactForm {
/// Your full name
name: String,
/// Email address
email: String,
/// Receive newsletter
subscribe: bool,
}
fn contact_form_view() -> impl View {
let form_binding = ContactForm::binding();
let projected = form_binding.project();
vstack((
form(&form_binding),
text(projected.name.map(|n| format!("Hello, {}!", n))),
text(projected.email.map(|e| format!("Email: {}", e))),
))
}
Secure Password Input
Use SecureField for sensitive data with automatic memory zeroing:
use waterui::prelude::*;
use waterui_form::secure::{Secure, SecureField, secure};
fn password_form() -> impl View {
let password = Binding::new(Secure::default());
let confirm = Binding::new(Secure::default());
vstack((
SecureField::new("Password", &password),
secure("Confirm Password", &confirm),
button("Create Account", move || {
let hash = password.get().hash();
tracing::debug!("Password hash: {}", hash);
}),
))
}
The Secure type:
- Implements
Zeroizeto clear memory on drop - Has
Debugimpl that printsSecure(****)instead of the actual value - Provides
hash()method using bcrypt with default cost - Use
expose()to access the underlying string when needed
Form Validation
Compose validators to enforce rules on form fields:
use waterui::prelude::*;
use waterui_form::valid::{Validator, ValidatableView};
use regex::Regex;
fn validated_form() -> impl View {
let age_binding = Binding::new(0i32);
let email_binding = Binding::new(String::new());
let age_range = 18..=100;
let email_pattern = Regex::new(r"^[^@]+@[^@]+\.[^@]+$").unwrap();
vstack((
ValidatableView::new(
Stepper::new(&age_binding).label("Age"),
age_range,
),
ValidatableView::new(
TextField::new(&email_binding).label("Email"),
email_pattern,
),
))
}
Validators can be combined:
.and(other)- both must succeed.or(other)- at least one must succeed
Built-in validators:
Range<T>- validates value is within rangeRegex- validates string matches patternRequired- validatesOption<T>isSomeor string is non-empty
Date Picker
Use DatePicker for date and time selection:
use waterui::prelude::*;
use waterui_form::picker::{DatePicker, DatePickerType};
use time::Date;
fn event_form() -> impl View {
let event_date = Binding::new(Date::MIN);
vstack((
DatePicker::new(&event_date)
.label("Event Date")
.ty(DatePickerType::DateHourAndMinute),
DatePicker::new(&event_date)
.label("Date Only")
.ty(DatePickerType::Date),
))
}
Available picker types:
DatePickerType::Date- Date onlyDatePickerType::HourAndMinute- Time only (hour:minute)DatePickerType::HourMinuteAndSecond- Time with secondsDatePickerType::DateHourAndMinute- Date and time (default)DatePickerType::DateHourMinuteAndSecond- Date and time with seconds
Color Picker
Use ColorPicker for platform-native color selection:
use waterui::prelude::*;
use waterui_form::picker::ColorPicker;
use waterui_color::Color;
fn theme_editor() -> impl View {
let primary_color = Binding::new(Color::rgb(0.0, 0.5, 1.0));
let background = Binding::new(Color::rgb(1.0, 1.0, 1.0));
vstack((
ColorPicker::new(&primary_color).label("Primary Color"),
ColorPicker::new(&background).label("Background"),
))
}
Multi-Date Selection
Use MultiDatePicker for selecting multiple dates:
use waterui::prelude::*;
use waterui_form::picker::multi_date::MultiDatePicker;
use alloc::collections::BTreeSet;
use time::Date;
fn availability_calendar() -> impl View {
let available_dates = Binding::new(BTreeSet::<Date>::new());
vstack((
MultiDatePicker::new(&available_dates)
.label("Select Available Dates"),
text(available_dates.map(|dates| {
format!("Selected {} dates", dates.len())
})),
))
}
Manual FormBuilder Implementation
For custom layouts or specialized behavior, implement FormBuilder manually:
use waterui::prelude::*;
use waterui_form::FormBuilder;
struct TwoColumnForm {
left_field: String,
right_field: String,
}
impl FormBuilder for TwoColumnForm {
type View = HStack<(TextField, TextField)>;
fn view(binding: &Binding<Self>, _label: AnyView, _placeholder: Str) -> Self::View {
let projected = binding.project();
hstack((
TextField::new(&projected.left_field).label("Left"),
TextField::new(&projected.right_field).label("Right"),
))
}
}
API Overview
Core Types
FormBuilder- Trait for types that can render as form UIform()- Function to create a form view from aBinding<T: FormBuilder>
Secure Module
Secure- Wrapper type for sensitive strings with automatic memory zeroingSecureField- Password input componentsecure()- Helper function to create aSecureField
Picker Module
Picker- Generic picker for selecting from a listColorPicker- Platform-native color selectionDatePicker- Date and time selection with multiple stylesMultiDatePicker- Multiple date selectionPickerItem<T>- Type alias for picker items (TaggedView<T, Text>)
Validation Module
Validatable- Trait for views that can be validatedValidator<T>- Trait for value validation logicValidatableView<V, T>- Wraps a view with a validatorAnd<A, B>- Combines validators with logical ANDOr<A, B>- Combines validators with logical ORRequired- Validator for required fieldsOutOfRange<T>- Error type for range validationNotMatch- Error type for regex validation
Features
Default Features
None. The crate works out-of-the-box with no feature flags.
Optional Features
serde- EnablesSerializeandDeserializederives on form typeswaterui-form = { version = "0.1.0", features = ["serde"] }
Workspace Dependencies
waterui-core- ProvidesViewtrait,Binding,Environment, andAnyViewwaterui-controls- Base components (TextField,Toggle,Stepper,Slider,Button)waterui-layout- Layout primitives (VStack,HStack,ZStack)waterui-text- Text rendering componentswaterui-color- Color type used byColorPicker
External Dependencies
nami- Fine-grained reactivity system (providesBinding,Computed)time- Date/time types forDatePickerzeroize- Secure memory zeroing forSecuretypebcrypt- Password hashing forSecure::hash()regex- Pattern matching for validation
Derive Macros
The FormBuilder derive macro is provided by waterui-macros, which is automatically included when using waterui::prelude::*.
Notes
- This crate is
#![no_std]compatible (usesextern crate alloc) - All code examples in this README are extracted from actual source code or documentation
- The crate follows WaterUI's layout contract system - see component source for detailed layout behavior
- Forms are rendered to native platform widgets (UIKit/AppKit on Apple, Android View on Android)
Dependencies
~11–16MB
~217K SLoC