gpuicn Components

Component 21 of 60

Field

Composes a control with its label, description, and errors.

site/src/previews/field.rs

Starting the preview. GPUI compiles to WebAssembly, so the first load takes a few seconds. This browser has neither WebGPU nor WebGL 2, so the live preview cannot run. The code on this page still works. The preview did not start.

Field, running in the browser. The second tab shows the preview's own source, which includes its layout and demo state. For the smallest working example, see Usage below. Download the whole showcase for its imports and helpers.

Installation

After the one-time setup, run the command from your app root. The CLI writes the modules and keeps src/ui/mod.rs in step.

gpuicn add field

If you prefer to copy the files by hand, declare them yourself:

// src/ui/mod.rs
pub mod theme;
pub mod input;
pub mod field;

// src/main.rs
mod ui;

Usage

Put this in a view module and return example(name) from its render method.

use crate::ui::field::*;
use crate::ui::input::InputState;
use gpui_kit::{Entity, IntoElement};

fn example(name: &Entity<InputState>) -> impl IntoElement {
    Field::new("name", name)
        .label("Full name")
        .required(true)
        .description("Use the name on your account.")
}

API reference

pub fn new(id: impl Into<ElementId>, state: &Entity<InputState>) -> Self

Creates a field around the caller's retained editing state.

pub fn from_control(id: impl Into<ElementId>, control: C) -> Self

Creates a field around any supported text, choice or number control.

pub fn label(mut self, value: impl Into<SharedString>) -> Self

Sets the visible label and the control's accessible name.

pub fn description(mut self, value: impl Into<SharedString>) -> Self

Sets supporting text below the editor.

pub fn error(mut self, value: impl Into<SharedString>) -> Self

Shows validation feedback and marks the editor invalid.

pub fn required(mut self, value: bool) -> Self

Marks the field as required in its visible and accessible label.

pub fn disabled(mut self, value: bool) -> Self

Disables interaction and applies the disabled appearance.

pub fn orientation(mut self, value: FieldOrientation) -> Self

Places the label above or beside the editor.

pub fn field_group(cx: &App) -> Div

Stacks related fields with Nova spacing.

pub fn field_content(cx: &App) -> Div

Stacks a field editor and supporting content.

pub fn field_title(cx: &App) -> Div

Creates a themed field heading.

pub fn field_separator(cx: &App) -> Div

Creates a horizontal divider between field sections.

Installed source, 235 lines
//! Nova field layout with explicit input state, label focus and validation messages.

use super::{
    input::{Input, InputState, Textarea},
    theme::UiTheme,
};
use gpui_kit::{
    AnyElement, App, Div, ElementId, Entity, FocusHandle, FontWeight, InteractiveElement as _,
    IntoElement, MouseButton, ParentElement as _, RenderOnce, Role, SharedString,
    StatefulInteractiveElement as _, Styled, Window, div, prelude::FluentBuilder as _, px,
};

/// A control that can receive a field's shared name, disabled and invalid state.
pub trait FieldControl: 'static {
    /// Returns the control's real keyboard focus target.
    fn field_focus_handle(&self, cx: &App) -> FocusHandle;
    /// Applies the field state and erases the rendered control type.
    fn into_field_control(self, label: SharedString, disabled: bool, invalid: bool) -> AnyElement;
}

impl FieldControl for Input {
    fn field_focus_handle(&self, cx: &App) -> FocusHandle {
        Input::field_focus_handle(self, cx)
    }

    fn into_field_control(self, label: SharedString, disabled: bool, invalid: bool) -> AnyElement {
        self.aria_label(label)
            .disabled(disabled)
            .invalid(invalid)
            .into_any_element()
    }
}

impl FieldControl for Textarea {
    fn field_focus_handle(&self, cx: &App) -> FocusHandle {
        Textarea::field_focus_handle(self, cx)
    }

    fn into_field_control(self, label: SharedString, disabled: bool, invalid: bool) -> AnyElement {
        self.aria_label(label)
            .disabled(disabled)
            .invalid(invalid)
            .into_any_element()
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
/// The placement of a field label relative to its editor.
pub enum FieldOrientation {
    #[default]
    /// Place the label above the editor.
    Vertical,
    /// Place the label beside the editor.
    Horizontal,
}

#[derive(IntoElement)]
/// A labeled control with application-owned validation feedback.
pub struct Field<C: FieldControl = Input> {
    id: ElementId,
    control: C,
    label: SharedString,
    description: Option<SharedString>,
    error: Option<SharedString>,
    required: bool,
    disabled: bool,
    orientation: FieldOrientation,
    style: gpui_kit::StyleRefinement,
}
impl Field<Input> {
    /// Creates a field around the caller's retained editing state.
    pub fn new(id: impl Into<ElementId>, state: &Entity<InputState>) -> Self {
        Field::from_control(id, Input::new(state))
    }
}
impl<C: FieldControl> Field<C> {
    /// Creates a field around any supported text, choice or number control.
    pub fn from_control(id: impl Into<ElementId>, control: C) -> Self {
        Self {
            id: id.into(),
            control,
            label: SharedString::default(),
            description: None,
            error: None,
            required: false,
            disabled: false,
            orientation: FieldOrientation::Vertical,
            style: Default::default(),
        }
    }
    /// Sets the visible label and the control's accessible name.
    pub fn label(mut self, value: impl Into<SharedString>) -> Self {
        self.label = value.into();
        self
    }
    /// Sets supporting text below the editor.
    pub fn description(mut self, value: impl Into<SharedString>) -> Self {
        self.description = Some(value.into());
        self
    }
    /// Shows validation feedback and marks the editor invalid.
    pub fn error(mut self, value: impl Into<SharedString>) -> Self {
        self.error = Some(value.into());
        self
    }
    /// Marks the field as required in its visible and accessible label.
    pub fn required(mut self, value: bool) -> Self {
        self.required = value;
        self
    }
    /// Disables interaction and applies the disabled appearance.
    pub fn disabled(mut self, value: bool) -> Self {
        self.disabled = value;
        self
    }
    /// Places the label above or beside the editor.
    pub fn orientation(mut self, value: FieldOrientation) -> Self {
        self.orientation = value;
        self
    }
}
impl<C: FieldControl> Styled for Field<C> {
    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
        &mut self.style
    }
}
impl<C: FieldControl> RenderOnce for Field<C> {
    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
        let t = UiTheme::read(cx).clone();
        let focus = self.control.field_focus_handle(cx);
        let label = if self.required {
            format!("{} (required)", self.label).into()
        } else {
            self.label.clone()
        };
        let label_view = div()
            .id((self.id.clone(), "label"))
            .flex()
            .items_center()
            .gap(t.space(1.))
            .font_weight(FontWeight::MEDIUM)
            .text_size(t.text(14.))
            .line_height(t.text(20.))
            .child(self.label)
            .when(self.required, |label| label.child(" *"))
            .when(!self.disabled, |label| {
                label
                    .cursor_pointer()
                    .on_mouse_down(MouseButton::Left, move |_, window, cx| {
                        focus.focus(window, cx)
                    })
            });
        let description = self.error.clone().or(self.description.clone());
        let control = self
            .control
            .into_field_control(label, self.disabled, self.error.is_some());
        let root = div()
            .id(self.id)
            .role(Role::Group)
            .w_full()
            .flex()
            .gap(t.space(2.))
            .font_family(t.fonts.body.clone())
            .text_color(t.colors.foreground)
            .when(self.orientation == FieldOrientation::Vertical, |root| {
                root.flex_col()
            })
            .when(self.orientation == FieldOrientation::Horizontal, |root| {
                root.items_center()
            })
            .child(label_view)
            .child(
                div()
                    .flex_1()
                    .min_w_0()
                    .flex()
                    .flex_col()
                    .gap(t.space(1.5))
                    .child(control)
                    .when_some(description, |root, description| {
                        root.child(
                            div()
                                .id("description")
                                .when(self.error.is_some(), |description| {
                                    description.role(Role::Alert)
                                })
                                .text_size(t.text(14.))
                                .line_height(t.text(20.))
                                .text_color(if self.error.is_some() {
                                    t.colors.destructive
                                } else {
                                    t.colors.muted_foreground
                                })
                                .child(description),
                        )
                    }),
            );
        super::theme::apply_style(root, &self.style)
    }
}

/// Stacks related fields with Nova spacing.
pub fn field_group(cx: &App) -> Div {
    div()
        .flex()
        .flex_col()
        .w_full()
        .gap(UiTheme::read(cx).space(7.))
}
/// Stacks a field editor and supporting content.
pub fn field_content(cx: &App) -> Div {
    div()
        .flex()
        .flex_col()
        .flex_1()
        .gap(UiTheme::read(cx).space(1.5))
}
/// Creates a themed field heading.
pub fn field_title(cx: &App) -> Div {
    let t = UiTheme::read(cx);
    div()
        .flex()
        .items_center()
        .gap(t.space(2.))
        .font_family(t.fonts.body.clone())
        .font_weight(FontWeight::MEDIUM)
        .text_size(t.text(14.))
        .line_height(t.text(20.))
        .text_color(t.colors.foreground)
}
/// Creates a horizontal divider between field sections.
pub fn field_separator(cx: &App) -> Div {
    div().w_full().h(px(1.)).bg(UiTheme::read(cx).colors.border)
}

Platform notes

Visual reference: shadcn/ui 4.19.0, Neutral Nova. Implementation: crates/ui/registry/field/field.rs, using GPUI Kit 0.6.4.

Field::new keeps the existing retained InputState API. Field::from_control composes the same label, required marker, description, disabled state and error treatment around Input, Select, Combobox, Autocomplete and NumberField. Each control supplies its real focus target and receives the same accessible name, disabled state and invalid state. Validation and values remain application-owned; there is no hidden form registry.

The Rust source and Usage example define the supported API. Browser DOM/CSS behavior, exact exit animations and cross-platform screen-reader parity are not implied by the native component. See migration qualification for the checks completed on this revision.