gpuicn Components

Component 35 of 60

Number Field

A numeric input with step controls.

site/src/previews/number_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.

Number 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 number-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;
pub mod number_field;

// src/main.rs
mod ui;

Usage

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

use crate::ui::input::InputState;
use crate::ui::number_field::*;
use gpui_kit::{App, IntoElement, Window};

fn example(window: &mut Window, cx: &mut App) -> impl IntoElement {
    let state = window.use_keyed_state("quantity", cx, |window, cx| {
        InputState::new(window, cx)
            .default_value("3")
            .min(0.)
            .max(20.)
            .step(1.)
    });
    NumberField::new(&state).aria_label("Quantity")
}

API reference

pub fn new(state: &Entity<InputState>) -> Self

Configure range and step on the caller-owned `InputState`.

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

Sets the accessible name of the control.

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

Disables interaction and applies the disabled appearance.

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

Prevents user changes while preserving focus and reading.

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

Applies validation error colors and an invalid accessibility description.

Installed source, 140 lines
//! Nova presentation for GPUI Kit's numeric input and step actions.

use super::{
    field::FieldControl,
    input::Input,
    theme::{
        UiTheme,
        primitives::{control, disabled, focus},
    },
};
use gpui_icons::{LucideIcon, lucide};
use gpui_kit::base::{NumberInput, input::InputState};
pub use gpui_kit::base::{NumberInputEvent, NumberStep, StepAction};
use gpui_kit::{
    AnyElement, App, Entity, FocusHandle, Focusable as _, IntoElement, ParentElement as _,
    RenderOnce, SharedString, Styled, Window, prelude::FluentBuilder as _,
};

#[derive(IntoElement)]
/// A Kit numeric editor with Nova stepper controls.
pub struct NumberField {
    state: Entity<InputState>,
    style: gpui_kit::StyleRefinement,
    label: Option<SharedString>,
    disabled: bool,
    read_only: bool,
    invalid: bool,
}
impl NumberField {
    /// Configure range and step on the caller-owned `InputState`.
    pub fn new(state: &Entity<InputState>) -> Self {
        Self {
            state: state.clone(),
            style: Default::default(),
            label: None,
            disabled: false,
            read_only: false,
            invalid: false,
        }
    }
    /// Sets the accessible name of the control.
    pub fn aria_label(mut self, value: impl Into<SharedString>) -> Self {
        self.label = Some(value.into());
        self
    }
    /// Disables interaction and applies the disabled appearance.
    pub fn disabled(mut self, value: bool) -> Self {
        self.disabled = value;
        self
    }
    /// Prevents user changes while preserving focus and reading.
    pub fn read_only(mut self, value: bool) -> Self {
        self.read_only = value;
        self
    }
    /// Applies validation error colors and an invalid accessibility description.
    pub fn invalid(mut self, value: bool) -> Self {
        self.invalid = value;
        self
    }
}
impl Styled for NumberField {
    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
        &mut self.style
    }
}
impl RenderOnce for NumberField {
    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
        let t = UiTheme::read(cx).clone();
        let focused = self.state.focus_handle(cx).is_focused(window) && !self.disabled;
        let mut input = Input::new(&self.state)
            .bordered(false)
            .disabled(self.disabled)
            .read_only(self.read_only)
            .invalid(self.invalid);
        if let Some(label) = self.label {
            input = input.aria_label(label);
        }
        let mut root = NumberInput::new(&self.state)
            .disabled(self.disabled)
            .w_full()
            .h(t.space(8.))
            .map(|root| {
                focus::shown_for(
                    control::frame(root, &t, self.invalid),
                    &t,
                    focused,
                    self.invalid,
                )
            })
            .when(self.disabled, disabled::style)
            .decrement_button({
                let t = t.clone();
                move |button| {
                    button
                        .accessibility_label("Decrease value")
                        .size(t.space(8.))
                        .cursor_pointer()
                        .child(
                            lucide(LucideIcon::Minus)
                                .size(t.space(3.5))
                                .text_color(t.colors.foreground),
                        )
                }
            })
            .increment_button({
                let t = t.clone();
                move |button| {
                    button
                        .accessibility_label("Increase value")
                        .size(t.space(8.))
                        .cursor_pointer()
                        .child(
                            lucide(LucideIcon::Plus)
                                .size(t.space(3.5))
                                .text_color(t.colors.foreground),
                        )
                }
            })
            .input(input);
        if self.read_only {
            root = root.on_step(|_, _, _| {});
        }
        super::theme::apply_style(root, &self.style)
    }
}

impl FieldControl for NumberField {
    fn field_focus_handle(&self, cx: &App) -> FocusHandle {
        self.state.read(cx).focus_handle(cx)
    }

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

Platform notes

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

NumberField takes a Kit InputState configured with min, max and step. Kit NumberInput and step actions own parsing and numeric changes; gpuicn composes the shared Input and Nova stepper buttons. Disabled and read-only state guard editing and stepping. Pointer scrubbing is outside this API. The invalid builder applies the destructive border and accessibility description directly or through Field::from_control.

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.