Form

Form lays out typed fields and an optional footer. The application owns values, validation, submission, and responsive column choices.

Install

#Import

use gpui_kit::component::form::{field, v_form, h_form, Form, Field};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Form, FormGrid, FormSection, FormSeparator, FormField, FormRow } from "form.slint";
use iced::widget::text_editor::{Action, Content};
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};

#Predictable composition

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// One form built the way the page builds it: the label beside the control in
// the column the form measures, two field columns, and the commit row in
// `footer`.
let composed = Form::new()
    .label_layout(LabelLayout::Beside)
    .columns(2)
    .gap(12.0)
    .child(
        Field::new().label("Name").child(
            Input::new("pc-name")
                .value(app.name.clone())
                .placeholder("Ada Lovelace")
                .on_input(Message::Name),
        ),
    )
    .child(
        Field::new().label("Email").required(true).child(
            Input::new("pc-email")
                .value(app.email.clone())
                .placeholder("ada@example.com")
                .on_input(Message::Email),
        ),
    )
    .footer(
        row![
            Button::new("Cancel")
                .outline()
                .on_press(Message::ComposeCancel),
            Button::new("Save")
                .variant(button::Variant::Primary)
                .on_press(Message::ComposeSave),
        ]
        .spacing(8.0),
    );

Form::new() defaults to one column with labels above controls. label_layout(Axis::Horizontal) places labels beside controls; columns(2) independently creates two field columns. Existing horizontal(), vertical(), layout(Axis), h_form(), and v_form() remain available.

Form::new()
    .label_layout(Axis::Horizontal)
    .columns(2)
    .child(Field::new().label("Name").child(Input::new(&name_input)))
    .child(Field::new().label("Email").child(Input::new(&email_input)))
    .footer(Button::new("save").label("Save"))
SlintNo Slint version of this example yet.

child accepts a Field. Put commands in footer, which spans all columns and aligns its content to the trailing edge. Attach submission behavior to the supplied Button; Form does not submit automatically. See the complete application recipe for retained state, callbacks, and window setup.

#Usage

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The basic form: one column, labels above their controls.
let basic = Form::vertical()
    .child(
        Field::new().label("Name").child(
            Input::new("use-name")
                .value(app.name.clone())
                .on_input(Message::Name),
        ),
    )
    .child(
        Field::new().label("Email").required(true).child(
            Input::new("use-email")
                .value(app.email.clone())
                .on_input(Message::Email),
        ),
    )
    .child(
        Field::new().label("Bio").child(
            Textarea::new(&app.bio)
                .placeholder("Tell us about yourself")
                .id("use-bio")
                .on_action(Message::Bio),
        ),
    );

// The horizontal form: the same fields with the labels beside them.
let horizontal = Form::horizontal()
    .child(
        Field::new().label("First Name").child(
            Input::new("h-first")
                .value(app.h_first.clone())
                .placeholder("Ada")
                .on_input(Message::HFirst),
        ),
    )
    .child(
        Field::new().label("Last Name").child(
            Input::new("h-last")
                .value(app.h_last.clone())
                .placeholder("Lovelace")
                .on_input(Message::HLast),
        ),
    );

// The multi-column form: two field columns, and the Bio field spanning both.
let multi = Form::vertical()
    .columns(2)
    .child(
        Field::new().label("First Name").child(
            Input::new("m-first")
                .value(app.h_first.clone())
                .placeholder("Ada")
                .on_input(Message::HFirst),
        ),
    )
    .child(
        Field::new().label("Last Name").child(
            Input::new("m-last")
                .value(app.h_last.clone())
                .placeholder("Lovelace")
                .on_input(Message::HLast),
        ),
    )
    .child(
        Field::new().label("Bio").col_span(2).child(
            Textarea::new(&app.bio)
                .rows(3)
                .placeholder("Tell us about yourself")
                .id("m-bio")
                .on_action(Message::Bio),
        ),
    );

// The registration form: the one example that runs validation end to end.
// **Create account** checks the email and the password and writes the strings
// their fields show; typing a valid value clears the one it corrects.
let registration = Form::vertical()
    .child(
        Field::new().label("Personal Information").child(
            row![
                Input::new("reg-first")
                    .value(app.reg_first.clone())
                    .placeholder("First name")
                    .on_input(Message::RegFirst),
                Input::new("reg-last")
                    .value(app.reg_last.clone())
                    .placeholder("Last name")
                    .on_input(Message::RegLast),
            ]
            .spacing(8.0),
        ),
    )
    .child(
        Field::new()
            .label("Email")
            .required(true)
            .error(app.reg_error_email.clone())
            .child(
                Input::new("reg-email")
                    .value(app.reg_email.clone())
                    .invalid(!app.reg_error_email.is_empty())
                    .placeholder("you@example.com")
                    .on_input(Message::RegEmail),
            ),
    )
    .child(
        Field::new()
            .label("Password")
            .required(true)
            .description("Must be at least 8 characters")
            .error(app.reg_error_password.clone())
            .child(
                Input::new("reg-password")
                    .value(app.reg_password.clone())
                    .secure(true)
                    .invalid(!app.reg_error_password.is_empty())
                    .placeholder("Password")
                    .on_input(Message::RegPassword),
            ),
    )
    .child(
        Field::new().label("Confirm Password").required(true).child(
            Input::new("reg-confirm")
                .value(app.reg_confirm.clone())
                .secure(true)
                .placeholder("Confirm password")
                .on_input(Message::RegConfirm),
        ),
    )
    .child(
        Field::new()
            .label_indent(false)
            .error(app.reg_error_terms.clone())
            .child(
                Checkbox::new(app.reg_terms)
                    .label("I agree to the Terms of Service")
                    .on_toggle(Message::RegTerms),
            ),
    )
    .child(
        Field::new().label_indent(false).child(
            Button::new("Create account")
                .variant(button::Variant::Primary)
                .on_press(Message::RegCreate),
        ),
    );

// The settings form: `Field::section` is the heading over a group and
// `Field::separator` the rule under it, which is Slint's `FormSection` and
// `FormSeparator`.
let settings = Form::vertical()
    .child(Field::section("Profile").description("How you appear to others."))
    .child(Field::separator())
    .child(
        Field::new().label("Display Name").child(
            Input::new("set-display")
                .value(app.name.clone())
                .placeholder("Ada Lovelace")
                .on_input(Message::Name),
        ),
    )
    .child(
        Field::new().label("Email").child(
            Input::new("set-email")
                .value(app.email.clone())
                .placeholder("ada@example.com")
                .on_input(Message::Email),
        ),
    )
    .child(Field::section("Preferences").description("Choose how the app behaves."))
    .child(Field::separator())
    .child(
        Field::new().label("Notifications").child(
            Switch::new(app.g_notify)
                .label("Enable notifications")
                .on_toggle(Message::GNotify),
        ),
    );

let usage = column![
    basic,
    Field::separator(),
    horizontal,
    Field::separator(),
    multi,
    Field::separator(),
    registration,
    muted_line(app.reg_status.as_str()),
    settings,
]
.spacing(16.0);

#Basic Form

v_form()
    .child(
        field()
            .label("Name")
            .child(Input::new(&name_input))
    )
    .child(
        field()
            .label("Email")
            .child(Input::new(&email_input))
            .required(true)
    )
Form {
    FormField {
        label: "Name";
        Input {
            text <=> root.name;
            placeholder: "Ada Lovelace";
        }
    }
    FormField {
        label: "Email";
        required: true;
        Input {
            text <=> root.email;
            placeholder: "ada@example.com";
        }
    }
    HorizontalLayout {
        alignment: end;
        Button {
            text: "Save";
            variant: ButtonVariant.primary;
        }
    }
}

#Horizontal Form Layout

h_form()
    .label_width(px(120.))
    .child(
        field()
            .label("First Name")
            .child(Input::new(&first_name))
    )
    .child(
        field()
            .label("Last Name")
            .child(Input::new(&last_name))
    )
Form {
    FormRow {
        label: "First Name";
        label-width: 120px;
        Input {
            text <=> root.first;
            placeholder: "Ada";
        }
    }
    FormRow {
        label: "Last Name";
        label-width: 120px;
        Input {
            text <=> root.last;
            placeholder: "Lovelace";
        }
    }
}

#Multi-Column Form

v_form()
    .columns(2) // Two-column layout
    .child(
        field()
            .label("First Name")
            .child(Input::new(&first_name))
    )
    .child(
        field()
            .label("Last Name")
            .child(Input::new(&last_name))
    )
    .child(
        field()
            .label("Bio")
            .col_span(2) // Span across both columns
            .child(Input::new(&bio_input))
    )
FormGrid {
    FormField {
        col: 0;
        label: "First Name";
        Input {
            text <=> root.m-first;
            placeholder: "Ada";
        }
    }
    FormField {
        col: 1;
        label: "Last Name";
        Input {
            text <=> root.m-last;
            placeholder: "Lovelace";
        }
    }
    FormField {
        col: 0;
        colspan: 2;
        label: "Bio";
        Textarea {
            text <=> root.m-bio;
            placeholder: "Tell us about yourself";
        }
    }
}

#User Registration Form

struct RegistrationForm {
    first_name: Entity<InputState>,
    last_name: Entity<InputState>,
    email: Entity<InputState>,
    password: Entity<InputState>,
    confirm_password: Entity<InputState>,
    terms_accepted: bool,
}

impl Render for RegistrationForm {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_form()
            .large()
            .child(
                field()
                    .label("Personal Information")
                    .label_indent(false)
                    .child(
                        h_flex()
                            .gap_3()
                            .child(
                                div().flex_1().child(
                                    Input::new(&self.first_name)
                                        .placeholder("First name")
                                )
                            )
                            .child(
                                div().flex_1().child(
                                    Input::new(&self.last_name)
                                        .placeholder("Last name")
                                )
                            )
                    )
            )
            .child(
                field()
                    .label("Email")
                    .required(true)
                    .child(Input::new(&self.email))
            )
            .child(
                field()
                    .label("Password")
                    .required(true)
                    .description("Must be at least 8 characters")
                    .child(Input::new(&self.password))
            )
            .child(
                field()
                    .label("Confirm Password")
                    .required(true)
                    .child(Input::new(&self.confirm_password))
            )
            .child(
                field()
                    .label_indent(false)
                    .child(
                        Checkbox::new("terms")
                            .label("I agree to the Terms of Service")
                            .checked(self.terms_accepted)
                            .on_click(cx.listener(|this, checked, _, cx| {
                                this.terms_accepted = *checked;
                                cx.notify();
                            }))
                    )
            )
            .child(
                field()
                    .label_indent(false)
                    .child(
                        Button::new("register")
                            .primary()
                            .large()
                            .w_full()
                            .child("Create Account")
                    )
            )
    }
}
Form {
    FormField {
        label: "Personal Information";
        HorizontalLayout {
            spacing: 12px;
            cross-axis-alignment: start;
            Input {
                text <=> root.r-first;
                placeholder: "First name";
            }
            Input {
                text <=> root.r-last;
                placeholder: "Last name";
            }
        }
    }
    FormField {
        label: "Email";
        required: true;
        Input {
            text <=> root.r-email;
            placeholder: "you@example.com";
        }
    }
    FormField {
        label: "Password";
        required: true;
        description: "Must be at least 8 characters";
        Input {
            text <=> root.r-password;
            masked: true;
            mask-toggle: true;
            placeholder: "Password";
        }
    }
    FormField {
        label: "Confirm Password";
        required: true;
        Input {
            text <=> root.r-confirm;
            masked: true;
            mask-toggle: true;
            placeholder: "Confirm password";
        }
    }
    Checkbox {
        checked <=> root.r-terms;
        label: "I agree to the Terms of Service";
    }
    Button {
        text: "Create Account";
        variant: ButtonVariant.primary;
        horizontal-stretch: 1;
    }
}

#Settings Form with Sections

v_form()
    .column(2)
    .child(
        field()
            .label("Profile")
            .label_indent(false)
            .col_span(2)
            .child(Separator::horizontal())
    )
    .child(
        field()
            .label("Display Name")
            .child(Input::new(&display_name))
    )
    .child(
        field()
            .label("Email")
            .child(Input::new(&email))
    )
    .child(
        field()
            .label("Bio")
            .col_span(2)
            .items_start()
            .child(Input::new(&bio))
    )
    .child(
        field()
            .label("Preferences")
            .label_indent(false)
            .col_span(2)
            .child(Separator::horizontal())
    )
    .child(
        field()
            .label("Theme")
            .child(Select::new(&theme_state))
    )
    .child(
        field()
            .label("Language")
            .child(Select::new(&language_state))
    )
    .child(
        field()
            .label_indent(false)
            .child(Switch::new("notifications").label("Enable notifications"))
    )
    .child(
        field()
            .label_indent(false)
            .child(Switch::new("marketing").label("Marketing emails"))
    )
Form {
    FormSection {
        title: "Profile";
        description: "How you appear to others.";
    }
    FormSeparator {}
    FormRow {
        label: "Display Name";
        label-width: 160px;
        Input {
            text <=> root.s-display;
            placeholder: "Ada Lovelace";
        }
    }
    FormRow {
        label: "Email";
        label-width: 160px;
        Input {
            text <=> root.s-email;
            placeholder: "ada@example.com";
        }
    }
    FormField {
        label: "Bio";
        Textarea {
            text <=> root.s-bio;
            placeholder: "A short bio";
        }
    }
    FormSection {
        title: "Preferences";
        description: "Choose how the app behaves.";
    }
    FormSeparator {}
    FormRow {
        label: "Theme";
        label-width: 160px;
        Select {
            items: root.theme-items;
            selected <=> root.s-theme;
        }
    }
    FormRow {
        label: "Language";
        label-width: 160px;
        Select {
            items: root.language-items;
            selected <=> root.s-language;
        }
    }
    Switch {
        checked <=> root.s-notify;
        label: "Enable notifications";
    }
    Switch {
        checked <=> root.s-marketing;
        label: "Marketing emails";
    }
}

#Contact Form

v_form()
    .child(
        field()
            .label("Contact Information")
            .child(
                h_flex()
                    .gap_2()
                    .child(
                        Select::new(&prefix_state)
                            .w(px(80.))
                    )
                    .child(
                        div().flex_1().child(
                            Input::new(&name_input)
                                .placeholder("Your name")
                        )
                    )
            )
    )
    .child(
        field()
            .label("Email")
            .required(true)
            .child(Input::new(&email_input))
    )
    .child(
        field()
            .label("Subject")
            .child(Select::new(&subject_state))
    )
    .child(
        field()
            .label("Message")
            .required(true)
            .items_start()
            .description("Please describe your inquiry in detail")
            .child(Input::new(&message_input))
    )
    .child(
        field()
            .label_indent(false)
            .child(
                h_flex()
                    .gap_2()
                    .justify_between()
                    .child(
                        Checkbox::new("copy")
                            .label("Send me a copy")
                    )
                    .child(
                        h_flex()
                            .gap_2()
                            .child(Button::new("cancel").child("Cancel"))
                            .child(Button::new("send").primary().child("Send Message"))
                    )
            )
    )
Form {
    FormField {
        label: "Contact Information";
        HorizontalLayout {
            spacing: 8px;
            cross-axis-alignment: center;
            Select {
                preferred-width: 90px;
                horizontal-stretch: 0;
                items: root.prefix-items;
                selected <=> root.c-prefix;
            }
            Input {
                text <=> root.c-name;
                placeholder: "Your name";
            }
        }
    }
    FormField {
        label: "Email";
        required: true;
        Input {
            text <=> root.c-email;
            placeholder: "you@example.com";
        }
    }
    FormField {
        label: "Subject";
        Select {
            items: root.subject-items;
            selected <=> root.c-subject;
        }
    }
    FormField {
        label: "Message";
        required: true;
        description: "Please describe your inquiry in detail";
        Textarea {
            text <=> root.c-message;
            placeholder: "How can we help?";
        }
    }
    HorizontalLayout {
        alignment: space-between;
        cross-axis-alignment: center;
        Checkbox {
            checked <=> root.c-copy;
            label: "Send me a copy";
        }
        HorizontalLayout {
            spacing: 8px;
            Button {
                text: "Cancel";
            }
            Button {
                text: "Send Message";
                variant: ButtonVariant.primary;
            }
        }
    }
}

#Form Container and Layout

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The vertical default, the horizontal label layout, and the size ladder.
let vertical = Form::vertical()
    .gap(12.0)
    .child(
        Field::new().label("Name").child(
            Input::new("vl-name")
                .value(app.name.clone())
                .on_input(Message::Name),
        ),
    )
    .child(
        Field::new().label("Email").child(
            Input::new("vl-email")
                .value(app.email.clone())
                .on_input(Message::Email),
        ),
    );

let layout_horizontal = Form::horizontal()
    .child(
        Field::new().label("Name").child(
            Input::new("lh-name")
                .value(app.name.clone())
                .on_input(Message::Name),
        ),
    )
    .child(
        Field::new().label("Email").child(
            Input::new("lh-email")
                .value(app.email.clone())
                .on_input(Message::Email),
        ),
    );

let large = Form::vertical().size(ControlSize::Large).child(
    Field::new().label("Title").child(
        Input::new("sz-large")
            .size(ControlSize::Large)
            .value(app.sizing_title.clone())
            .placeholder("Large field")
            .on_input(Message::SizingTitle),
    ),
);

let small = Form::vertical().size(ControlSize::Small).child(
    Field::new().label("Code").child(
        Input::new("sz-small")
            .size(ControlSize::Small)
            .value(app.sizing_code.clone())
            .placeholder("Small field")
            .on_input(Message::SizingCode),
    ),
);

let container_layout = column![vertical, layout_horizontal, large, small].spacing(20.0);

#Vertical Layout (Default)

v_form()
    .gap(px(12.))
    .child(field().label("Name").child(input))
    .child(field().label("Email").child(email_input))
SlintNo Slint version of this example yet.

#Horizontal Layout

h_form()
    .label_width(px(100.))
    .child(field().label("Name").child(input))
    .child(field().label("Email").child(email_input))
SlintNo Slint version of this example yet.

#Custom Sizing

v_form()
    .large() // Large form size
    .label_text_size(rems(1.2))
    .child(field().label("Title").child(input))

v_form()
    .small() // Small form size
    .child(field().label("Code").child(input))
Form {
    FormField {
        label: "Title";
        size: ControlSize.large;
        Input {
            size: ControlSize.large;
            text <=> root.big-title;
            placeholder: "Large field";
        }
    }
    FormField {
        label: "Code";
        size: ControlSize.small;
        Input {
            size: ControlSize.small;
            text <=> root.small-code;
            placeholder: "Small field";
        }
    }
}

#Form Validation

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `required` draws the asterisk; `error` places the message, and the
// description steps aside while one is set. Both are the application's: the
// Validate button writes the string the field shows.
let required = Form::vertical()
    .child(
        Field::new()
            .label("Email")
            .required(true)
            .error(app.v_error.clone())
            .child(
                Input::new("val-email")
                    .value(app.v_email.clone())
                    .invalid(!app.v_error.is_empty())
                    .placeholder("you@example.com")
                    .on_input(Message::VEmail),
            ),
    )
    .child(
        Field::new()
            .label("Password")
            .required(true)
            .description("Must be at least 8 characters long")
            .child(
                Input::new("val-password")
                    .secure(true)
                    .placeholder("Password"),
            ),
    )
    .footer(
        Button::new("Validate")
            .variant(button::Variant::Primary)
            .on_press(Message::VValidate),
    );

let dynamic = Form::vertical().child(
    Field::new()
        .label("Bio")
        .description(if app.bio.text().chars().count() > 40 {
            "That is plenty — shorter reads better."
        } else {
            "Use at most 100 words to describe yourself."
        })
        .child(
            Textarea::new(&app.bio)
                .rows(3)
                .id("val-bio")
                .on_action(Message::Bio),
        ),
);

let conditional = Form::vertical()
    .child(
        Field::new().label("Account Type").child(
            Select::new([Item::new("Personal"), Item::new("Business")])
                .selected(app.v_account)
                .open(app.select_open)
                .width(Length::Fill)
                .on_open_change(Message::SelectOpen)
                .on_chosen(Message::VAccount),
        ),
    )
    .child(
        Field::new()
            .label("Company Name")
            .visible(app.v_account == Some(1))
            .child(
                Input::new("val-company")
                    .value(app.v_company.clone())
                    .placeholder("Acme Inc.")
                    .on_input(Message::VCompany),
            ),
    )
    .child(
        Field::new()
            .label("Tax ID")
            .required(true)
            .visible(app.v_account == Some(1))
            .child(
                Input::new("val-tax")
                    .value(app.v_tax_id.clone())
                    .placeholder("US-0000000")
                    .on_input(Message::VTaxId),
            ),
    );

let validation = column![required, dynamic, conditional].spacing(20.0);

#Required Fields

field()
    .label("Email")
    .required(true) // Shows asterisk (*) next to label
    .child(Input::new(&email_input))
Form {
    FormField {
        label: "Email";
        required: true;
        error: "Enter a valid email address.";
        Input {
            text <=> root.required-email;
            invalid: true;
            placeholder: "you@example.com";
        }
    }
    FormField {
        label: "Password";
        required: true;
        description: "Must be at least 8 characters long";
        Input {
            masked: true;
            mask-toggle: true;
            placeholder: "Password";
        }
    }
}

#Field Descriptions

field()
    .label("Password")
    .description("Must be at least 8 characters long")
    .child(Input::new(&password_input))
SlintNo Slint version of this example yet.

#Dynamic Descriptions

field()
    .label("Bio")
    .description_fn(|_, _| {
        div().child("Use at most 100 words to describe yourself.")
    })
    .child(Input::new(&bio_input))
SlintNo Slint version of this example yet.

#Field Visibility

field()
    .label("Admin Settings")
    .visible(user.is_admin()) // Conditionally show field
    .child(Switch::new("admin-mode"))
SlintNo Slint version of this example yet.

#Submit Handling

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The form does not submit; the button reports, the application validates and
// the status line under the form is what changed.
let submit = Form::vertical()
    .child(
        Field::new().label("Name").child(
            Input::new("sub-name")
                .value(app.s_name.clone())
                .on_input(Message::SName),
        ),
    )
    .child(
        Field::new().label("Email").child(
            Input::new("sub-email")
                .value(app.s_email.clone())
                .on_input(Message::SEmail),
        ),
    )
    .child(
        Field::new().label_indent(false).child(
            Button::new("Submit")
                .variant(button::Variant::Primary)
                .on_press(Message::SSubmit),
        ),
    );

let actions = Form::vertical()
    .child(
        Field::new()
            .label("Title")
            .child(Input::new("act-title").placeholder("Release notes")),
    )
    .child(
        Field::new()
            .label("Content")
            .child(Input::new("act-content").placeholder("What changed?")),
    )
    .footer(
        row![
            Button::new("Save").variant(button::Variant::Primary),
            Button::new("Cancel").outline(),
            Button::new("Preview").variant(button::Variant::Ghost),
        ]
        .spacing(8.0),
    );

let submission = column![submit, muted_line(app.s_status.as_str()), actions].spacing(20.0);

#Basic Submit Pattern

struct FormView {
    name_input: Entity<InputState>,
    email_input: Entity<InputState>,
}

impl FormView {
    fn submit(&mut self, cx: &mut Context<Self>) {
        let name = self.name_input.read(cx).value();
        let email = self.email_input.read(cx).value();

        // Validate inputs
        if name.is_empty() || email.is_empty() {
            // Show validation error
            return;
        }

        // Submit form data
        self.handle_submit(name, email, cx);
    }
}

// Form with submit button
v_form()
    .child(field().label("Name").child(Input::new(&self.name_input)))
    .child(field().label("Email").child(Input::new(&self.email_input)))
    .child(
        field()
            .label_indent(false)
            .child(
                Button::new("submit")
                    .primary()
                    .child("Submit")
                    .on_click(cx.listener(|this, _, _, cx| this.submit(cx)))
            )
    )
SlintNo Slint version of this example yet.

#Form with Action Buttons

v_form()
    .child(field().label("Title").child(Input::new(&title)))
    .child(field().label("Content").child(Input::new(&content)))
    .child(
        field()
            .label_indent(false)
            .child(
                h_flex()
                    .gap_2()
                    .child(Button::new("save").primary().child("Save"))
                    .child(Button::new("cancel").child("Cancel"))
                    .child(Button::new("preview").outline().child("Preview"))
            )
    )
SlintNo Slint version of this example yet.

#Field Groups

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// One label over a row of two controls; one label over a stack; and the
// controls that are not text fields at all.
let related = Form::vertical()
    .child(
        Field::new().label("Name").child(
            row![
                Input::new("grp-first")
                    .value(app.g_first.clone())
                    .placeholder("First name")
                    .on_input(Message::GFirst),
                Input::new("grp-last")
                    .value(app.g_last.clone())
                    .placeholder("Last name")
                    .on_input(Message::GLast),
            ]
            .spacing(8.0),
        ),
    )
    .child(
        Field::new().label("Address").child(
            column![
                Input::new("grp-street")
                    .value(app.g_street.clone())
                    .placeholder("Street address")
                    .on_input(Message::GStreet),
                row![
                    Input::new("grp-city")
                        .value(app.g_city.clone())
                        .placeholder("City")
                        .on_input(Message::GCity),
                    Input::new("grp-zip")
                        .value(app.g_zip.clone())
                        .placeholder("ZIP")
                        .width(120.0)
                        .on_input(Message::GZip),
                ]
                .spacing(8.0),
            ]
            .spacing(8.0),
        ),
    );

let custom = Form::vertical()
    .child(
        Field::new().label("Notifications").child(
            Switch::new(app.g_notify)
                .label("Email notifications")
                .on_toggle(Message::GNotify),
        ),
    )
    .child(
        Field::new().label("Volume").child(
            Slider::new(app.g_volume)
                .minimum(0.0)
                .maximum(100.0)
                .on_change(Message::GVolume),
        ),
    )
    .child(
        Field::new().label("Quantity").child(
            NumberInput::new("grp-qty")
                .value(app.g_quantity)
                .minimum(1.0)
                .maximum(20.0)
                .on_change(Message::GQuantity),
        ),
    );

let groups = column![related, custom].spacing(20.0);
v_form()
    .child(
        field()
            .label("Name")
            .child(
                h_flex()
                    .gap_2()
                    .child(div().flex_1().child(Input::new(&first_name)))
                    .child(div().flex_1().child(Input::new(&last_name)))
            )
    )
    .child(
        field()
            .label("Address")
            .items_start() // Align to start for multi-line content
            .child(
                v_flex()
                    .gap_2()
                    .child(Input::new(&street))
                    .child(
                        h_flex()
                            .gap_2()
                            .child(div().flex_1().child(Input::new(&city)))
                            .child(div().w(px(100.)).child(Input::new(&zip)))
                    )
            )
    )
Form {
    FormField {
        label: "Name";
        HorizontalLayout {
            spacing: 12px;
            cross-axis-alignment: start;
            Input {
                text <=> root.first;
                placeholder: "First name";
            }
            Input {
                text <=> root.last;
                placeholder: "Last name";
            }
        }
    }
    FormField {
        label: "Address";
        VerticalLayout {
            spacing: 8px;
            Input {
                text <=> root.addr-street;
                placeholder: "Street address";
            }
            HorizontalLayout {
                spacing: 12px;
                cross-axis-alignment: start;
                Input {
                    text <=> root.addr-city;
                    placeholder: "City";
                }
                Input {
                    preferred-width: 120px;
                    horizontal-stretch: 0;
                    text <=> root.addr-zip;
                    placeholder: "ZIP";
                }
            }
        }
    }
}

#Custom Field Components

field()
    .label("Theme Color")
    .child(ColorPicker::new(&color_state).small())

field()
    .label("Birth Date")
    .description("We'll send you a birthday gift!")
    .child(DatePicker::new(&date_state))

field()
    .label("Notifications")
    .child(
        v_flex()
            .gap_2()
            .child(Switch::new("email").label("Email notifications"))
            .child(Switch::new("push").label("Push notifications"))
            .child(Switch::new("sms").label("SMS notifications"))
    )
SlintNo Slint version of this example yet.

#Conditional Fields

v_form()
    .child(
        field()
            .label("Account Type")
            .child(Select::new(&account_type))
    )
    .child(
        field()
            .label("Company Name")
            .visible(is_business_account) // Show only for business accounts
            .child(Input::new(&company_name))
    )
    .child(
        field()
            .label("Tax ID")
            .visible(is_business_account)
            .required(is_business_account)
            .child(Input::new(&tax_id))
    )
Form {
    FormField {
        label: "Account Type";
        Select {
            items: root.account-items;
            selected <=> root.account-type;
        }
    }
    if root.is-business: FormField {
        label: "Company Name";
        Input {
            text <=> root.company;
            placeholder: "Acme Inc.";
        }
    }
    if root.is-business: FormField {
        label: "Tax ID";
        required: true;
        Input {
            text <=> root.tax-id;
            placeholder: "US-0000000";
        }
    }
}

#Grid Layout and Positioning

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Three columns, a field spanning all three, and a field placed at a column
// of its own.
let spanning = Form::vertical()
    .columns(3)
    .child(
        Field::new().label("First").child(
            Input::new("g-a")
                .value(app.grid_a.clone())
                .placeholder("One")
                .on_input(Message::GridA),
        ),
    )
    .child(
        Field::new().label("Second").child(
            Input::new("g-b")
                .value(app.grid_b.clone())
                .placeholder("Two")
                .on_input(Message::GridB),
        ),
    )
    .child(
        Field::new().label("Third").child(
            Input::new("g-c")
                .value(app.grid_c.clone())
                .placeholder("Three")
                .on_input(Message::GridC),
        ),
    )
    .child(
        Field::new().label("Full Width").col_span(3).child(
            Input::new("g-full")
                .value(app.grid_full.clone())
                .placeholder("Spans all three columns")
                .on_input(Message::GridFull),
        ),
    );

let positioning = Form::vertical()
    .columns(4)
    .child(
        Field::new()
            .label("A")
            .child(Input::new("g-p1").placeholder("First")),
    )
    .child(
        Field::new()
            .label("B")
            .child(Input::new("g-p2").placeholder("Second")),
    )
    .child(
        Field::new()
            .label("Positioned")
            .col_start(1)
            .col_span(2)
            .child(
                Input::new("g-pos")
                    .value(app.grid_positioned.clone())
                    .placeholder("Columns two and three")
                    .on_input(Message::GridPositioned),
            ),
    );

// The page's Responsive Layout sample: the application chooses the column
// count from the window, and the component clamps the Bio field's span to
// whatever the count is, exactly as the page's `.when(!is_mobile, …)` does.
let responsive = Form::vertical()
    .columns(if app.narrow { 1 } else { 2 })
    .child(
        Field::new().label("Name").child(
            Input::new("resp-name")
                .value(app.resp_name.clone())
                .placeholder("Ada Lovelace")
                .on_input(Message::RespName),
        ),
    )
    .child(
        Field::new().label("Email").child(
            Input::new("resp-email")
                .value(app.resp_email.clone())
                .placeholder("ada@example.com")
                .on_input(Message::RespEmail),
        ),
    )
    .child(
        Field::new().label("Bio").col_span(2).child(
            Textarea::new(&app.bio)
                .rows(3)
                .placeholder("Tell us about yourself")
                .id("resp-bio")
                .on_action(Message::Bio),
        ),
    );

let grid = column![spanning, positioning, responsive].spacing(20.0);

#Column Spanning

v_form()
    .columns(3) // Three-column grid
    .child(field().label("First").child(input1))
    .child(field().label("Second").child(input2))
    .child(field().label("Third").child(input3))
    .child(
        field()
            .label("Full Width")
            .col_span(3) // Spans all three columns
            .child(Input::new(&full_width))
    )
FormGrid {
    FormField {
        col: 0;
        label: "First";
        Input {
            text <=> root.col-a;
            placeholder: "One";
        }
    }
    FormField {
        col: 1;
        label: "Second";
        Input {
            text <=> root.col-b;
            placeholder: "Two";
        }
    }
    FormField {
        col: 2;
        label: "Third";
        Input {
            text <=> root.col-c;
            placeholder: "Three";
        }
    }
    FormField {
        col: 0;
        colspan: 3;
        label: "Full Width";
        Input {
            text <=> root.col-full;
            placeholder: "Spans all three columns";
        }
    }
}

#Column Positioning

v_form()
    .columns(4)
    .child(field().label("A").child(input_a))
    .child(field().label("B").child(input_b))
    .child(
        field()
            .label("Positioned")
            .col_start(1) // Start at column 1
            .col_span(2)  // Span 2 columns
            .child(input_positioned)
    )
SlintNo Slint version of this example yet.

#Responsive Layout

v_form()
    .columns(if is_mobile { 1 } else { 2 })
    .child(field().label("Name").child(name_input))
    .child(field().label("Email").child(email_input))
    .child(
        field()
            .label("Bio")
            .when(!is_mobile, |field| field.col_span(2))
            .child(bio_input)
    )
SlintNo Slint version of this example yet.