-
Notifications
You must be signed in to change notification settings - Fork 9
Component Form
Form primitives for building labeled settings/forms.
This is a component-level primitive set (visual building blocks), not a full "form" framework. Validation rules and data storage live in your view/state.
- Settings screens and preference panes
- Any UI that needs consistent label/control alignment and validation messaging
Not a good fit:
- A full form framework (schema-driven validation, submission lifecycle, etc.)
Guide-Recipes-Settings-Form
-
form(): vertical container that spaces rows. -
form_row(label, control): a single row with aligned label + control. -
help_text("..."): secondary guidance text. -
inline_error("..."): inline error message. -
validation_state_icon(...): small status marker.
The Form module is a set of small helpers rather than one big type:
-
form(): vertical container that spaces rows. -
form_row(label, control): build a labeled row.-
.validation(ValidationState): attach validation state. -
.help(help_text("...")): attach help text. -
.error(inline_error("...")): attach inline error text.
-
-
help_text("..."): secondary guidance text. -
inline_error("..."): inline error message. -
validation_state_icon(ValidationState): small status marker.
-
form()spaces rows vertically. -
form_row(...)aligns label and control with consistent spacing.
The key idea is: your view/state owns the data, and you derive:
-
ValidationState(Success/Warning/Error) - whether to show
help_textorinline_error
from that state.
use std::path::PathBuf;
use gpui::{ElementId, SharedString, Window, div, px};
use yororen_ui::component::{
FilePathStatus, ValidationState, file_path_input, form, form_row, help_text, inline_error,
label, switch, text_input,
};
// In your render function:
fn render_form(window: &mut Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
// 1) Own the data in keyed state.
let username = window.use_keyed_state("demo:form:username", cx, |_, _| {
SharedString::new_static("")
});
let show_help = window.use_keyed_state("demo:form:show_help", cx, |_, _| true);
let show_error = window.use_keyed_state("demo:form:show_error", cx, |_, _| true);
let java_path = window.use_keyed_state("demo:form:java_path", cx, |_, _| {
None::<PathBuf>
});
// 2) Derive validation state from the data.
let username_value = username.read(cx).as_ref().trim();
let username_len = username_value.chars().count();
let username_validation = if username_len == 0 {
ValidationState::Error
} else if username_len < 3 {
ValidationState::Warning
} else {
ValidationState::Success
};
// 3) Derive what message to show.
let username_error = (username_len == 0).then_some("Username cannot be empty");
// Java validation (demo logic): accept paths that end with `/java` or `\\java.exe`.
let java_validation = if let Some(path) = java_path.read(cx).as_ref() {
let path = path.to_string_lossy();
if path.ends_with("/java") || path.ends_with("\\\\java.exe") {
ValidationState::Success
} else {
ValidationState::Error
}
} else {
ValidationState::Error
};
let java_status = match java_validation {
ValidationState::Success => Some(FilePathStatus::Ok),
ValidationState::Warning => Some(FilePathStatus::Warning),
ValidationState::Error => Some(FilePathStatus::Error),
};
let java_error_message = match (java_path.read(cx).as_ref(), java_validation) {
(None, ValidationState::Error) => Some("Choose a Java executable (usually bin/java)"),
(Some(_path), ValidationState::Error) => Some("Invalid Java path: choose bin/java"),
_ => None,
};
form()
.child(
div()
.flex()
.items_center()
.gap_3()
.child(
div()
.flex()
.items_center()
.gap_2()
.child(label("Show help").muted(true))
.child(
switch()
.key(ElementId::from((ElementId::from("demo:form"), "help")))
.checked(*show_help.read(cx))
.on_toggle({
let show_help = show_help.clone();
move |_checked, _ev, _window, cx| {
show_help.update(cx, |state, _| *state = !*state);
}
}),
),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.child(label("Show errors").muted(true))
.child(
switch()
.key(ElementId::from((ElementId::from("demo:form"), "error")))
.checked(*show_error.read(cx))
.on_toggle({
let show_error = show_error.clone();
move |_checked, _ev, _window, cx| {
show_error.update(cx, |state, _| *state = !*state);
}
}),
),
),
)
.child({
let mut row = form_row(
"Username",
div().w(px(320.)).child(
text_input()
.key(ElementId::from((ElementId::from("demo:form"), "username")))
.placeholder("Enter a username…")
.on_change({
let username = username.clone();
move |value, _window, cx| {
username.update(cx, |state, _| *state = value);
}
}),
),
)
.validation(username_validation);
// Use `InlineError` when invalid; otherwise show `HelpText`.
if *show_error.read(cx) {
if let Some(message) = username_error {
row = row.error(inline_error(message));
}
}
if *show_help.read(cx) {
row = row.help(help_text("At least 3 characters."));
}
row
})
.child({
let mut input = file_path_input()
.key(ElementId::from((ElementId::from("demo:form"), "java")))
.placeholder("/path/to/java")
.on_change({
let java_path = java_path.clone();
move |path, _window, cx| {
java_path.update(cx, |state, _| *state = Some(path));
}
});
if let Some(status) = java_status {
input = input.status(status);
}
let mut row = form_row("Java path", div().w(px(520.)).child(input))
.validation(java_validation);
if *show_error.read(cx) {
if let Some(message) = java_error_message {
row = row.error(inline_error(message));
}
}
if *show_help.read(cx) {
row = row.help(help_text("Choose a Java executable (usually bin/java)."));
}
row
})
}InlineError is not automatic. You decide when to show it:
- call
.error(inline_error("..."))to show an error message - call
.help(help_text("..."))to show guidance
Pick one based on your validation result.
The Form primitives do not run validators for you.
Instead, the pattern is:
- store the raw user input in state (
use_keyed_state/ your view struct) - compute validation (
ValidationState) from that state - choose what to render (
help_textvsinline_error) based on the result
This keeps the components predictable and makes it easy to integrate with virtualization.
FormRow aligns the label to the left and the control block to the right. The label width is
configurable via .label_width(px(...)).
Showing/hiding help_text / inline_error changes row height.
When FormRow is used inside VirtualList / VirtualRow, showing/hiding help/error changes row
height. Notify the list controller about height changes to avoid scroll jitter:
VirtualListController.splice(...)VirtualListController.reset(...)
In performance-sensitive lists, prefer keeping variable-height content within a fixed expanded region, or only toggling at coarse granularity.
FormRow renders a ValidationStateIcon next to the control when .validation(...) is set.
If you need to match a specific layout density, use .validation_icon_size(px(...)).
Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.