Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Introduction

gpui-form turns an application-owned Rust model into typed GPUI form state. Derive GpuiForm, give each field one form intent, and use the generated holder to validate and reconstruct the model.

This guide is for Rust application developers who use GPUI Kit. The workspace supports Rust 1.98. Use the published facade version shown in Getting started.

Mental model

A derived form separates the source model from the state owned by the view:

  1. The source struct defines the values the application accepts.
  2. Field intents decide which values have components, remain hidden, or stay under application control.
  3. Component shapes define construction, rendering, value binding, and holder storage.
  4. The generated holder collects edits, exposes validation, and reconstructs the source model.

For a source struct named UserProfile, the derive generates:

TypePurpose
UserProfileFormFieldTyped identity for component-backed fields
UserProfileFormFieldsGPUI entities owned by the form view
UserProfileFormComponentsConstructors for those entities
UserProfileFormValueHolderEditable values, defaults, validation, and model conversion

Choose a path

Start with Getting started to derive and wire a form. Use Field intents and generated types for hidden, skipped, or converted values. Continue to Component shapes to select widgets and Validation and conversion before submission.

MCP form tools describes structured submit and headless editing. Prototyping covers generating complete GPUI wiring from inventory metadata.

Getting started

This tutorial adds a typed form model to an existing GPUI application. At the end, cargo check recognizes the generated UserProfileForm* types and the application owns the holder and component entities needed to render the form.

Prerequisites

  • Rust 1.98 or newer.
  • A GPUI application that calls gpui_kit::init(cx).
  • A gpui_kit::component::Root around each first-level window view.
  • gpui-kit 0.6.1.

Add the dependencies

Use gpui-form for the derive and generated runtime paths. This example uses ready-made collection shapes and the collection select derive:

[dependencies]
gpui-kit = "0.6.1"
gpui-form = "0.7"
gpui-form-collection = "0.7"
gpui-form-collection-derive = "0.7"
strum = { version = "0.28", features = ["derive"] }

Derive the form

Give every field one component(...), hidden, or skip intent:

use gpui_form::GpuiForm;
use gpui_form_collection_derive::SelectItem;
use strum::EnumIter;

#[derive(Clone, Debug, Default, EnumIter, PartialEq, SelectItem)]
enum Country {
    #[default]
    UnitedStates,
    France,
    Japan,
}

#[derive(Clone, Debug, Default, GpuiForm)]
struct UserProfile {
    #[gpui_form(component(gpui_form_collection::input::Input::<_>))]
    username: Option<String>,

    #[gpui_form(component(gpui_form_collection::input::Input::<_>))]
    age: Option<u32>,

    #[gpui_form(component(
        gpui_form_collection::select::Select::<_>,
        default = Country::France
    ))]
    country: Country,

    #[gpui_form(component(gpui_form_collection::checkbox::Checkbox))]
    subscribe: bool,
}

The default belongs to the component intent and seeds the generated holder. Optional fields start as None. Direct-storage fields without an explicit default use the form-side type’s Default implementation.

Own the generated state

Create the holder and component entities in the GPUI entity that renders the form:

let holder = UserProfileFormValueHolder::default();
let username = cx.new(|cx| UserProfileFormComponents::username(window, cx));
let age = cx.new(|cx| UserProfileFormComponents::age(window, cx));
let country = cx.new(|cx| UserProfileFormComponents::country(window, cx));
let subscribe = cx.new(|cx| UserProfileFormComponents::subscribe(window, cx));

let fields = UserProfileFormFields {
    username,
    age,
    country,
    subscribe,
};

The constructors create widget state. A complete view must also:

  1. Retain each gpui_kit::Subscription on the owning GPUI entity.
  2. Map component events through gpui_form::runtime::shape::value_change into the holder.
  3. Seed widget state with gpui_form::runtime::shape::seed_value_binding_state.
  4. Render each shape’s generated render component.
  5. Validate and convert the holder before invoking application submission code.

The inventory workflow in Prototyping generates this wiring from the same shape metadata.

Choose optional features

NeedConfiguration
Inventory metadatagpui-form = { version = "0.7", features = ["inventory"] }
MCP tools in a GPUI applicationgpui-form = { version = "0.7", features = ["mcp"] }
Headless MCP formsgpui-form = { version = "0.7", default-features = false, features = ["derive", "mcp"] }
MCP schemas for Chrono or decimal valuesAdd chrono or rust_decimal beside mcp
Localized date, file, or infinite-select shapesAdd gpui-form-component with component-shape and, for the derive, derive

Check the result

Run:

cargo check

A successful check confirms that the dependencies resolve, each field has an intent, and every selected shape supports its form-side value type.

Troubleshooting

SymptomAction
field ... must choose a gpui_form field intentAdd component(...), hidden, or skip to the named field.
A gpui_form_component shape is unavailableEnable that crate’s component-shape feature; infinite-select enums also need its derive feature.
GPUI types from two dependencies do not matchDepend on gpui-kit alone, as shown above.
The widget renders but the holder never changesRetain the subscription and map its event into the holder, or generate the wiring through the prototyping workflow.

Field intents and generated types

Field intents decide which values render, which values stay in the generated holder, and which values the application must supply during reconstruction. Every non-empty-form field must choose exactly one intent.

IntentGenerated componentStored in holderReconstruction
component(...)YesYesUses the holder value
hiddenNoYesUses the holder value
skipNoNoCaller supplies the source value

Component fields

Use component(...) for fields edited by a GPUI widget:

#[gpui_form(component(gpui_form_collection::input::Input::<_>))]
name: String,

The shape owns construction, rendering, value binding, and the storage policy used by the generated holder. A configured shape expression can customize construction:

#[gpui_form(
    component(gpui_form_collection::select::Select::<_>.searchable(true))
)]
country: Country,

Put default = ... inside component(...) when the holder should start with a field-specific value. A component shape also defines how a non-optional field stores an empty value; see Component shapes.

Hidden fields

Use hidden for values that participate in the holder and conversion without a component:

#[gpui_form(hidden(default = request_context()))]
context: RequestContext,

Hidden fields are useful for non-visual values that may still come from a structured client or application default. They do not create a FormFields member or a FormComponents constructor.

Skipped fields

Use skip for source fields the form does not own:

#[gpui_form(skip)]
created_by: UserId,

Reconstruct a skipped-field model by passing those values to holder.into_original(created_by, ...). If another field can fail conversion, the same method returns Result<Model, Error>. holder.present_fields() provides a typed snapshot of the editable fields for preview or diagnostic UI.

Do not combine skip with component or hidden on the same field.

Form-side value types

Use value(...) when a widget edits a type different from the source field:

#[gpui_form(component(
    gpui_form_collection::input::Input::<_>,
    value(
        type = String,
        from_source = format_account_id,
        try_into_source = parse_account_id,
    )
))]
account_id: AccountId,

Write the base form-side type in type = ...; source optionality determines whether the holder stores an optional value. Both from_source and one reverse conversion are required. Use into_source for an infallible reverse conversion or try_into_source for a function returning Result<Source, Error> where the error implements Debug.

For a struct-level #[koruma(newtype)], value(koruma_newtype) edits the inner value and reconstructs the validated wrapper through Koruma’s public newtype traits.

Generated types

For UserProfile, the generated types have distinct jobs:

TypeUse
UserProfileFormFieldIdentify component-backed fields; each variant’s name() returns the exact source field name.
UserProfileFormFieldsStore the GPUI entities owned by the form view.
UserProfileFormComponentsConstruct component state with methods named after source fields.
UserProfileFormValueHolderStore editable values and defaults, convert back to the model, and expose validation when Koruma integration is enabled.

Hidden and skipped fields do not produce UserProfileFormField variants.

Validation and conversion

Validate the generated holder, then use the conversion method emitted for the form’s field contracts. Validation reports invalid editable values; conversion reconstructs the source model and can separately report a missing required value or a failed reverse conversion.

Form contractGenerated conversion
No skipped fields; reconstruction is statically infallibleholder.into_original() returns the model.
No skipped fields; the derive emits a checked pathholder.try_into_original() returns Result<Model, Error>. This includes fallible conversions and shape-policy component fields without a declared field default.
One or more skipped fieldsholder.into_original(skipped_value, ...) accepts those values and returns either the model or Result<Model, Error>, depending on the remaining fields.

A non-optional shape-backed field can still start empty when its shape uses required storage and no default = ... is declared. Fallible holder conversion reports that missing value. When the form enables Koruma integration, generated validate() reports it too.

The derive emits the checked conversion method for any non-defaulted shape-policy component because the policy is resolved through the shape type. A direct-storage policy always supplies a value, so that checked conversion cannot fail at runtime unless another field has a fallible conversion.

Validate with Koruma

Koruma validation can run directly against form-side values:

use gpui_form::GpuiForm;
use koruma::Koruma;
use koruma_collection::{collection::NonEmptyValidation, numeric::RangeValidation};

#[derive(Clone, Debug, GpuiForm)]
#[gpui_form(koruma)]
struct Registration {
    #[gpui_form(component(gpui_form_collection::input::Input::<_>))]
    #[koruma(NonEmptyValidation::<_>)]
    username: String,

    #[gpui_form(component(gpui_form_collection::number_input::NumberInput::<_>))]
    #[koruma(RangeValidation::<_>.min(18).max(120))]
    age: u8,
}

GpuiForm copies the field validators to RegistrationFormValueHolder and derives its Koruma implementation. The Koruma import brings the validate() method into scope; the source model only needs its own Koruma derive when the application also validates that model directly. All configured validators run in attribute order:

let holder = RegistrationFormValueHolder::default();

if let Err(errors) = holder.validate() {
    if errors.username().non_empty_validation().is_some() {
        eprintln!("username must not be empty");
    }
}

Use #[gpui_form(koruma(fluent))] when the application has enabled the Koruma Fluent features and installed its application-owned localizer. The generated holder derives KorumaAllFluent and exposes localized failed-validator values through all(); the source model does not need that derive unless it is also validated directly.

Convert validated newtypes

For #[koruma(newtype)] source fields, use value(koruma_newtype) inside the field intent. The generated holder edits the inner value and reconstructs the validated wrapper through Koruma’s public newtype traits.

Call the fallible conversion method after validate(). Validation is still useful even when conversion is checked because it can expose field-specific validator errors instead of only the first reconstruction failure.

Troubleshooting

SymptomCause and action
validate() reports a required field with no Koruma attributeThe component shape uses required storage. Supply a value or add an intent-scoped default = ....
try_into_original() fails after Koruma validation succeedsA reverse conversion failed or a required value was still absent. Inspect the conversion error’s field name.
Fluent validator types do not compile or messages cannot be resolvedEnable koruma/fluent and the matching koruma-collection Fluent feature, then initialize the application’s Fluent resources.

Component shapes

A component shape connects a form-side value to GPUI state, rendering, and events. Prefer a ready-made collection or component-owned shape. Define an application-owned shape when the existing contracts cannot represent the widget or construction behavior you need.

Choose the package

  • Use gpui-form-collection for common GPUI Kit controls.
  • Use gpui-form-component for localized date and file pickers or cascading infinite selects.
  • Use component-shape-gpui and gpui-form-runtime when a crate defines its own reusable shape.

Applications that only consume existing shapes use the gpui_form::runtime::shape facade and do not need a direct gpui-form-runtime dependency.

Collection shapes

gpui-form-collection provides these form shapes:

NeedShape
A FromStr + ToString + 'static valueinput::Input::<T>
Application-defined text parsing and formattinginput::ParsedInput::<T, Config>
One enum-like valueselect::Select::<T>
Several enum-like valuescombobox::Combobox::<Item>
Boolean valuecheckbox::Checkbox or switch::Switch
Numeric text inputnumber_input::NumberInput::<T>
f32 or gpui_kit::component::slider::SliderValueslider::Slider
gpui_kit::Hslacolor_picker::ColorPicker
chrono::NaiveDate or a date pairdate_picker::DatePicker or DateRangePicker
One-time-password valueotp_input::OtpInput::<T>

ParsedInput<T, Config> uses a ParsedInputConfig<T> implementation for parsing, formatting, placeholder text, empty-as-clear behavior, and optional widget validation.

Select values normally derive SelectItem from gpui-form-collection-derive and EnumIter from strum. The generic parameter of Combobox<Item> is the item type, so a Vec<Country> field uses Combobox::<Country>.

Configure one field

Built-in shapes expose builder expressions inside component(...):

#[gpui_form(component(
    gpui_form_collection::select::Select::<_>.searchable(true)
))]
country: Country,

Use Shape.from(options) when the shape publishes a completed options type. Configuration changes construction for that field while preserving the base shape’s value compatibility, rendering, storage, and metadata.

Component-owned shapes

Enable the form-shape implementations and derives you use:

[dependencies]
gpui-form-component = { version = "0.7", features = ["component-shape", "derive"] }
NeedShapeRequirement
Localized date or date rangegpui_form_component::date_picker::DatePicker or DateRangePickerInitialize application gpui-es-fluent resources
Native file or directory selectiongpui_form_component::file_picker::FilePickerInitialize application gpui-es-fluent resources
Cascading enum choicesgpui_form_component::infinite_select::InfiniteSelect::<T>Derive InfiniteSelect and implement Clone + Default + PartialEq + 'static throughout the enum tree

Nested infinite-select payload types must implement Default. Use InfiniteSelect::<_>.searchable(true) for search or InfiniteSelect::<_>.from(InfiniteSelectOptions::new(true, Some(3))) for search plus a maximum depth.

Define an application-owned shape

Declare an owned rendered component with #[derive(component_shape_gpui::GpuiComponentShape)], or wrap an external state/component pair with component_shape_gpui::component_shape!. Then attach the form storage policy:

impl gpui_form_runtime::shape::GpuiFormComponentShapePolicy for EmailInputShape {
    type ValueStoragePolicy =
        gpui_form_runtime::shape::DirectValueStorage;
}

A reusable shape defines:

  • backing entity state and construction
  • a render component
  • supported value types
  • event-to-value binding when the form should synchronize automatically
  • form holder storage policy
  • a stable prototyping field suffix
  • MCP input metadata when the wire contract needs shape-specific guidance

A shape declared only through a hand-written GpuiComponentShape implementation lacks the declaration marker required by GpuiForm. Use the derive or macro so the generated contract includes that marker and its metadata.

Storage policy

DirectValueStorage stores T for a non-optional field. Initialization comes from an intent-scoped default or the form-side type’s Default implementation.

RequiredValueStorage stores Option<T> so the form can represent missing input. Holder conversion reports an absent required value. Generated validation reports the same condition when Koruma integration is enabled.

Troubleshooting

SymptomAction
A component-owned type cannot be used in component(...)Enable gpui-form-component’s component-shape feature.
The derive rejects an application-owned shape as undeclaredDeclare it with the GpuiComponentShape derive or component_shape! macro.
A configured shape expression fails its boundUse a builder produced for the same base shape, or pass its completed options through Shape.from(...).
Prototyping reports missing capabilitiesAdd the missing render, value-binding, shape-path, or storage metadata to the owning shape and regenerate.

MCP form tools

The mcp feature exposes concrete generated forms as structured submit tools and headless edit sessions. The application still owns submission behavior; gpui-form supplies typed decoding, validation, metadata, registration, and session management.

Add MCP support

For a crate that also renders GPUI forms:

[dependencies]
gpui-form = { version = "0.7", features = ["mcp"] }
serde = { version = "1", features = ["derive"] }

For a headless form server, disable the default runtime and enable the derive explicitly:

gpui-form = { version = "0.7", default-features = false, features = ["derive", "mcp"] }

Add chrono or rust_decimal beside mcp when exposed fields or responses use those values.

Define a form and handler

MCP forms must be concrete and inventory-backed. Opt in on the model and register a free submit function with one owned parameter:

use gpui_form::GpuiForm;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, GpuiForm, Serialize)]
#[gpui_form(mcp(name = "submit_contact", title = "Submit contact request"))]
struct ContactRequest {
    #[gpui_form(hidden)]
    email: String,
}

#[derive(Debug, gpui_form::mcp::McpJsonSchema, Serialize)]
struct ContactResponse {
    accepted: bool,
}

#[gpui_form::mcp_submit]
async fn submit_contact(
    request: ContactRequest,
) -> Result<ContactResponse, String> {
    Ok(ContactResponse {
        accepted: !request.email.is_empty(),
    })
}

fn main() -> gpui_form::mcp::ServeStdioResult {
    gpui_form::mcp::serve_stdio_blocking()
}

Handlers can be synchronous or asynchronous and return Result<T, E>, where the response is serializable with an MCP schema and the error implements Display. Prefer a response struct or newtype with an object-root schema.

Use the source model parameter for normal submission. When a form has skipped fields, accept its generated *FormValueHolder so application code can supply the skipped context.

Share application context

For several forms that submit through the same state:

  1. Add context(MyContext) to each form’s mcp(...) options.
  2. Implement McpContextSubmit<MyContext> for the model, or add submit(path::to::async_fn) so the derive emits the implementation.
  3. Add response(MyResponse) for a precise output schema.
  4. Register all matching forms with register_context_submitters(&mut server, context).

Use map_response(path::to::fn) when a raw application response needs a fallible mapping into the published response. Use submitter(path::to::Trait) when an application trait owns the associated context, response, error, and submit method.

Choose registration behavior

NeedAPI
Default inventory server over stdioserve_stdio_blocking()
Application-owned server metadataserver_named(name, version)
Add generated forms to an existing serverregister(&mut server)
Register shared-context submittersregister_context_submitters(...)
Select submit, editor, or metadata behaviorregister_with_options(...)
Add generated prompt templatesregister_prompt_templates(&mut server)

Use per-form registration helpers when one form needs a manual handler or editor policy.

The MCP server retains editor sessions across calls until EOF, cancellation, idle expiry, or another application-owned shutdown signal. Tool completion does not request shutdown.

Edit a holder through MCP

Generated editor tools use an optimistic revision:

  1. Call *_edit_open and retain session_id and revision.
  2. Call *_edit_patch with values and optional clear.
  3. Pass expected_revision when stale writes must fail.
  4. Call *_edit_validate and inspect form-level and field-level errors.
  5. Call *_edit_submit when the form has a submit handler.
  6. Close abandoned sessions with *_edit_close.

Bulk patches are atomic. Sessions have a default count limit and idle timeout; configure them with McpFormEditorOptions and the corresponding *_with_editor_options registration helper.

Editor sessions hold generated values on the MCP server. They do not mutate live GPUI entities; an application that shows remote edits must provide that bridge.

Use generated metadata

Registered forms publish descriptor, schema, and examples resources at:

  • gpui-form://forms/{tool_name}/descriptor
  • gpui-form://forms/{tool_name}/schema
  • gpui-form://forms/{tool_name}/examples

Prompt templates are opt-in and produce fill_*, repair_*, and submit_* prompts that point clients to those resources and the applicable editor tools.

Field input schemas come from McpToolValue. Custom Serde types normally derive gpui_form::mcp::McpJsonSchema. Component-backed fields also attach value-specific shape MCP metadata when the shape publishes it. Koruma-backed forms expose validation metadata and structured validation issues.

Troubleshooting

SymptomAction
#[gpui_form(mcp)] rejects the formUse a concrete form and keep inventory enabled; remove no_inventory.
Editor tools exist but the submit tool is absentAdd a #[gpui_form::mcp_submit] handler or register a manual/context submitter.
Registration reports a duplicate name or URIGive each exposed form a unique mcp(name = "...") value.
A field fails schema generationDerive McpJsonSchema or implement McpToolValue for the custom value type.
An editor patch reports a stale revisionRead the session again and retry against its current revision.

Prototyping

Inventory prototyping turns concrete GpuiForm registrations into GPUI form scaffolds. A generator should discover the expected form types, write the configured output files, and format them with rustfmt; the workspace example performs all three steps.

Enable inventory

Enable the inventory feature in the dependency graph that contains the form models:

[dependencies]
gpui-form = { version = "0.7", features = ["inventory"] }
gpui-form-prototyping-core = "0.7"

The registry describes each concrete form, its field intents, component shapes, storage policy, rendering capability, value binding, holder conversion, and stable prototyping suffix.

The generator binary must link the crate that owns the forms so its inventory registrations are present. In this example, replace app_models with that crate’s Rust import name. Inspect the registry before writing files:

use gpui_form::schema::registry::{GpuiFormShape, inventory};

extern crate app_models;

for shape in inventory::iter::<GpuiFormShape>() {
    println!("{}: {} fields", shape.struct_name, shape.fields.len());
}

Seeing the expected struct names is the first verification checkpoint. An empty iterator usually means the generator does not link the model crate or the inventory feature is absent from that build graph.

Generate scaffolds

Implement gpui_form_prototyping_core::FormLayout for the application’s target view, then pass each registration to gpui_form_prototyping_core::FormShapeAdapter::new(shape).generate_file(&layout). The adapter produces the component construction, subscriptions, holder seeding, rendering, validation, and conversion fragments used by the layout.

The workspace example is a complete generator. From this repository root, run:

cargo run -p prototyping

The command rewrites Rust scaffolds in examples/some-lib-forms/src/forms and mirrors them under examples/prototyping/output, then runs rustfmt. Success ends with Form generation complete. for each output directory.

Keep registrations concrete

Generic form structs should use #[gpui_form(no_inventory)]; inventory entries must describe a concrete form type.

Generated scaffolds are consumers of component-shape metadata. When generation reports missing render, value-binding, shape-path, or storage information, update the owning shape declaration and regenerate.

Troubleshooting

SymptomAction
No forms are discoveredLink the form-owning crate in the generator and enable gpui-form/inventory in the same dependency graph.
A generic form fails inventory registrationAdd #[gpui_form(no_inventory)] to that generic form and register concrete form types instead.
Generation reports incomplete component capabilitiesAdd the missing render, value-binding, shape path, or storage metadata to the owning shape. The derive uses a generic shape suffix when the shape does not publish one.
Generation fails while formattingInstall the rustfmt component and rerun the generator.