gpuicn Components

Component 39 of 60

Progress

Displays completion progress for a task.

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

Progress, 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 progress

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

// src/ui/mod.rs
pub mod theme;
pub mod progress;

// src/main.rs
mod ui;

Usage

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

use crate::ui::progress::Progress;
use gpui_kit::IntoElement;

fn example() -> impl IntoElement {
    Progress::new("upload").value(64.).label("Uploading…")
}

API reference

pub fn new(id: impl Into<ElementId>) -> Self

Creates a `Progress` with a stable caller-owned ID.

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

Sets the caller-owned value displayed by this control.

pub fn indeterminate(mut self) -> Self

Shows progress without a known completion value.

pub fn range(mut self, min: f64, max: f64) -> Self

Sets the minimum and maximum for the displayed value.

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

Sets the accessible label for progress.

Installed source, 127 lines
//! Nova-styled determinate and indeterminate Progress using GPUI Kit.

use gpui_kit::base::{Progress as BaseProgress, ProgressIndicator, ProgressTrack};
use gpui_kit::{
    Animation, AnimationExt as _, App, ElementId, IntoElement, ParentElement as _, RenderOnce,
    SharedString, Styled, Window, relative,
};

use super::theme::UiTheme;

#[derive(IntoElement)]
/// A Nova track for determinate or indeterminate progress.
pub struct Progress {
    style: gpui_kit::StyleRefinement,
    id: ElementId,
    value: Option<f64>,
    min: f64,
    max: f64,
    label: Option<SharedString>,
}

impl Progress {
    /// Creates a `Progress` with a stable caller-owned ID.
    pub fn new(id: impl Into<ElementId>) -> Self {
        Self {
            style: gpui_kit::StyleRefinement::default(),
            id: id.into(),
            value: None,
            min: 0.,
            max: 100.,
            label: None,
        }
    }
    /// Sets the caller-owned value displayed by this control.
    pub fn value(mut self, value: f64) -> Self {
        self.value = Some(value);
        self
    }
    /// Shows progress without a known completion value.
    pub fn indeterminate(mut self) -> Self {
        self.value = None;
        self
    }
    /// Sets the minimum and maximum for the displayed value.
    pub fn range(mut self, min: f64, max: f64) -> Self {
        self.min = min;
        self.max = max;
        self
    }
    /// Sets the accessible label for progress.
    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
        self.label = Some(label.into());
        self
    }
}

impl RenderOnce for Progress {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = UiTheme::read(cx).clone();
        let spacing = theme.spacing.unit;
        let progress = self.value.map(|value| ratio(value, self.min, self.max));
        let root = BaseProgress::new(self.id.clone())
            .value(progress.unwrap_or(0.) * 100.)
            .indeterminate(progress.is_none())
            .w_full()
            .flex()
            .flex_col()
            .gap(spacing * 1.5_f32);
        let mut root = super::theme::apply_style(root, &self.style);
        if let Some(label) = self.label {
            root = root.accessibility_label(label);
        }
        let indicator = ProgressIndicator::new()
            .w(relative(progress.unwrap_or(0.33)))
            .h_full()
            .rounded_full()
            .bg(theme.colors.primary);
        let indicator = if progress.is_none() && !theme.motion.reduced && !cx.reduce_motion() {
            indicator
                .with_animation(
                    (self.id, "indeterminate"),
                    Animation::new(std::time::Duration::from_millis(1400)).repeat(),
                    |bar, phase| bar.relative().left(relative(-0.33 + phase * 1.33)),
                )
                .into_any_element()
        } else {
            indicator.into_any_element()
        };
        root.child(
            ProgressTrack::new()
                .relative()
                .w_full()
                .h(spacing * 1_f32)
                .rounded_full()
                .overflow_hidden()
                .bg(theme.colors.muted)
                .child(indicator),
        )
    }
}

impl gpui_kit::Styled for Progress {
    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
        &mut self.style
    }
}

/// Normalizes a finite numeric value and clamps it to a valid range.
fn ratio(value: f64, min: f64, max: f64) -> f32 {
    assert!(
        value.is_finite() && min.is_finite() && max.is_finite() && min < max,
        "progress needs a finite value and an increasing finite range"
    );
    ((value.clamp(min, max) - min) / (max - min)) as f32
}

#[cfg(test)]
mod tests {
    use super::ratio;
    #[test]
    fn numeric_ranges_clamp_at_both_ends() {
        assert_eq!(ratio(-5., 10., 30.), 0.);
        assert_eq!(ratio(20., 10., 30.), 0.5);
        assert_eq!(ratio(40., 10., 30.), 1.);
    }
}

Platform notes

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

Kit supplies determinate or indeterminate progress semantics. gpuicn applies the Nova track and fill. The application supplies progress and its accessible label.

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.