-
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.
-
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 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("用户名不能为空");
// 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("请选择 Java 可执行文件(通常是 bin/java)"),
(Some(path), ValidationState::Error) => Some("无效的 Java 路径:请选择 bin/java"),
_ => None,
};
form()
.child(
div()
.flex()
.items_center()
.gap_3()
.child(
div()
.flex()
.items_center()
.gap_2()
.child(label("显示说明").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("显示错误").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(
"用户名",
div().w(px(320.)).child(
text_input()
.key(ElementId::from((ElementId::from("demo:form"), "username")))
.placeholder("请输入用户名…")
.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("至少 3 个字符。"));
}
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 路径", 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("请选择 Java 可执行文件(通常是 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.