Skip to content

Component Form

MeowLynxSea edited this page Jan 24, 2026 · 6 revisions

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.

Building blocks

  • 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.

Example

The key idea is: your view/state owns the data, and you derive:

  • ValidationState (Success/Warning/Error)
  • whether to show help_text or inline_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
        })
}

How InlineError is used

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.

Why validation is "state-driven"

The Form primitives do not run validators for you.

Instead, the pattern is:

  1. store the raw user input in state (use_keyed_state / your view struct)
  2. compute validation (ValidationState) from that state
  3. choose what to render (help_text vs inline_error) based on the result

This keeps the components predictable and makes it easy to integrate with virtualization.

Notes

Alignment

FormRow aligns the label to the left and the control block to the right. The label width is configurable via .label_width(px(...)).

Help / error and virtualization

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.

Validation icon sizing

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(...)).

Clone this wiki locally