Skip to content

Guide Recipes Settings Form

MeowLynxSea edited this page Jan 24, 2026 · 3 revisions

Recipe: Settings form (Form + inputs + validation)

This recipe shows how to build a real settings form using Yororen UI primitives.

Goal:

  • Consistent label/control alignment
  • Inline help + inline error
  • “Uncontrolled” text input (smooth typing)
  • A file path picker with status coloring

Building blocks

  • Form primitives: form(), form_row(...), help_text(...), inline_error(...)
  • Inputs: TextInput, Switch, FilePathInput, NumberInput
  • Theme-driven validation: ValidationState

Example

use std::path::PathBuf;

use gpui::{ElementId, SharedString, Window, div, px};
use yororen_ui::component::{
    FilePathStatus, ValidationState,
    button, file_path_input, form, form_row, help_text, inline_error,
    label, number_input, switch, text_input,
};

fn render_settings(window: &mut Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
    // Own your settings state in keyed state.
    let username = window.use_keyed_state("settings:username", cx, |_, _| SharedString::new_static(""));
    let notifications = window.use_keyed_state("settings:notifications", cx, |_, _| true);
    let retries = window.use_keyed_state("settings:retries", cx, |_, _| 3.0f64);
    let workspace_path = window.use_keyed_state("settings:workspace", cx, |_, _| None::<PathBuf>);

    // Derive validation.
    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
    };

    let username_error = (username_len == 0).then_some("Username cannot be empty");

    let path_validation = if workspace_path.read(cx).is_some() {
        ValidationState::Success
    } else {
        ValidationState::Warning
    };

    let path_status = workspace_path.read(cx).as_ref().map(|_| match path_validation {
        ValidationState::Success => FilePathStatus::Ok,
        ValidationState::Warning => FilePathStatus::Warning,
        ValidationState::Error => FilePathStatus::Error,
    });

    form()
        .child(
            form_row(
                "Username",
                div().w(px(320.)).child(
                    text_input()
                        .key(ElementId::from((ElementId::from("settings"), "username")))
                        .placeholder("Enter a username…")
                        .on_change({
                            let username = username.clone();
                            move |value, _window, cx| {
                                username.update(cx, |state, _| *state = value);
                            }
                        }),
                ),
            )
            .validation(username_validation)
            .help(help_text("At least 3 characters."))
            .error(username_error.map(inline_error)),
        )
        .child(
            form_row(
                "Notifications",
                switch()
                    .key(("settings", "notifications"))
                    .checked(*notifications.read(cx))
                    .on_toggle({
                        let notifications = notifications.clone();
                        move |checked, _ev, _window, cx| {
                            notifications.update(cx, |state, _| *state = checked);
                        }
                    }),
            )
            .validation(ValidationState::Success),
        )
        .child(
            form_row(
                "Retries",
                div().w(px(240.)).child(
                    number_input()
                        .key(("settings", "retries"))
                        .min(0.0)
                        .max(10.0)
                        .step(1.0)
                        .value(*retries.read(cx))
                        .on_change({
                            let retries = retries.clone();
                            move |value, _window, cx| {
                                retries.update(cx, |state, _| *state = value);
                            }
                        }),
                ),
            )
            .validation(ValidationState::Success),
        )
        .child(
            form_row(
                "Workspace",
                div().w(px(520.)).child({
                    let mut input = file_path_input()
                        .key(ElementId::from((ElementId::from("settings"), "workspace")))
                        .placeholder("Choose a folder…")
                        .on_change({
                            let workspace_path = workspace_path.clone();
                            move |path, _window, cx| {
                                workspace_path.update(cx, |state, _| *state = Some(path));
                            }
                        });

                    if let Some(status) = path_status {
                        input = input.status(status);
                    }

                    input
                }),
            )
            .validation(path_validation)
            .help(help_text("Used for storing caches and profiles.")),
        )
        .child(
            div()
                .pt_3()
                .flex()
                .justify_end()
                .child(button().child(label("Save").inherit_color(true))),
        )
}

Notes

  • TextInput is best used "uncontrolled": avoid feeding .content(...) every render.
  • FilePathInput owns internal value state; use on_change to persist it.

Clone this wiki locally