progress: Add ProgressCircle. (#1918)

## Break Changes

- Renamed `bg` method to `color` for Progress.

<img width="1174" height="987" alt="image"
src="https://github.com/user-attachments/assets/cc6066a7-42f8-4e2a-b47f-a39de768a5e9"
/>
This commit is contained in:
Jason Lee
2026-01-12 15:08:03 +08:00
committed by GitHub
parent b0e1913bc9
commit 2af1007a6d
13 changed files with 780 additions and 118 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pause-icon lucide-pause"><rect x="14" y="3" width="5" height="18" rx="1"/><rect x="5" y="3" width="5" height="18" rx="1"/></svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-play-icon lucide-play"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/></svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -7,7 +7,9 @@ use gpui_component::{
ActiveTheme, Disableable as _, Icon, IconName, Selectable as _, Sizable as _, Theme,
button::{Button, ButtonCustomVariant, ButtonGroup, ButtonVariants as _},
checkbox::Checkbox,
h_flex, v_flex,
h_flex,
progress::ProgressCircle,
v_flex,
};
use serde::Deserialize;
@@ -338,6 +340,40 @@ impl Render for ButtonStory {
.on_click(Self::on_click),
),
)
.child(
section("With Progress").child(
h_flex()
.gap_4()
.child(
Button::new("progress-button-1")
.primary()
.large()
.icon(
ProgressCircle::new("circle-progress-1")
.color(cx.theme().primary_foreground)
.value(25.),
)
.label("Installing..."),
)
.child(
Button::new("progress-button-2")
.icon(ProgressCircle::new("circle-progress-1").value(35.))
.label("Installing..."),
)
.child(
Button::new("progress-button-3")
.small()
.icon(ProgressCircle::new("circle-progress-1").value(68.))
.label("Installing..."),
)
.child(
Button::new("progress-button-4")
.xsmall()
.icon(ProgressCircle::new("circle-progress-1").value(85.))
.label("Installing..."),
),
),
)
.child(
section("Outline Button")
.max_w_lg()

View File

@@ -1,16 +1,22 @@
use gpui::{
App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled,
App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Task,
Window, px,
};
use gpui_component::{
ActiveTheme, IconName, Sizable, button::Button, h_flex, progress::Progress, v_flex,
ActiveTheme, IconName, Sizable,
button::Button,
h_flex,
progress::{Progress, ProgressCircle},
v_flex,
};
use std::time::Duration;
use crate::section;
pub struct ProgressStory {
focus_handle: gpui::FocusHandle,
value: f32,
_task: Option<Task<()>>,
}
impl super::Story for ProgressStory {
@@ -35,13 +41,44 @@ impl ProgressStory {
fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
value: 50.,
value: 25.,
_task: None,
}
}
pub fn set_value(&mut self, value: f32) {
self.value = value;
}
fn start_animation(&mut self, cx: &mut Context<Self>) {
self.value = 0.;
self._task = Some(cx.spawn({
let entity = cx.entity();
async move |_, cx| {
loop {
cx.background_executor()
.timer(Duration::from_millis(15))
.await;
let mut need_break = false;
_ = entity.update(cx, |this, cx| {
this.value = (this.value + 2.).min(100.);
cx.notify();
if this.value >= 100. {
this._task = None;
need_break = true;
}
});
if need_break {
break;
}
}
}
}));
}
}
impl Focusable for ProgressStory {
@@ -56,52 +93,65 @@ impl Render for ProgressStory {
.items_center()
.gap_y_3()
.child(
section("Progress Bar").max_w_md().child(
v_flex()
.w_full()
.gap_3()
.justify_center()
.items_center()
.child(
h_flex()
.gap_2()
.child(Button::new("button-1").small().label("0%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(0.);
}),
))
.child(Button::new("button-2").small().label("25%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(25.);
}),
))
.child(Button::new("button-3").small().label("75%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(75.);
}),
))
.child(Button::new("button-4").small().label("100%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(100.);
}),
)),
)
.child(Progress::new("progress-1").value(self.value))
.child(
h_flex()
.gap_x_2()
.child(Button::new("button-5").icon(IconName::Minus).on_click(
cx.listener(|this, _, _, _| {
h_flex()
.w_full()
.gap_3()
.justify_between()
.child(
h_flex()
.gap_2()
.child(Button::new("button-1").small().label("0%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(0.);
}),
))
.child(Button::new("button-2").small().label("25%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(25.);
}),
))
.child(Button::new("button-3").small().label("75%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(75.);
}),
))
.child(Button::new("button-4").small().label("100%").on_click(
cx.listener(|this, _, _, _| {
this.set_value(100.);
}),
))
.child(
Button::new("circle-animation-button")
.small()
.icon(IconName::Play)
.on_click(cx.listener(|this, _, _, cx| {
this.start_animation(cx);
})),
),
)
.child(
h_flex()
.gap_2()
.child(
Button::new("circle-button-5")
.icon(IconName::Minus)
.on_click(cx.listener(|this, _, _, _| {
this.set_value((this.value - 1.).max(0.));
}),
))
.child(Button::new("button-6").icon(IconName::Plus).on_click(
cx.listener(|this, _, _, _| {
})),
)
.child(
Button::new("circle-button-6")
.icon(IconName::Plus)
.on_click(cx.listener(|this, _, _, _| {
this.set_value((this.value + 1.).min(100.));
}),
)),
),
),
})),
),
),
)
.child(
section("Progress Bar")
.max_w_md()
.child(Progress::new("progress-1").value(self.value)),
)
.child(
section("Custom Style").max_w_md().child(
@@ -109,10 +159,60 @@ impl Render for ProgressStory {
.value(32.)
.h(px(16.))
.rounded(px(2.))
.bg(cx.theme().green_light)
.color(cx.theme().green_light)
.border_2()
.border_color(cx.theme().green),
),
)
.child(
section("Circle Progress").max_w_md().child(
ProgressCircle::new("circle-progress-1")
.value(self.value)
.size_16(),
),
)
.child(
section("With size").max_w_md().child(
h_flex()
.gap_2()
.child(
ProgressCircle::new("circle-progress-1")
.value(self.value)
.large(),
)
.child(ProgressCircle::new("circle-progress-1").value(self.value))
.child(
ProgressCircle::new("circle-progress-1")
.value(self.value)
.small(),
)
.child(
ProgressCircle::new("circle-progress-1")
.value(self.value)
.xsmall(),
),
),
)
.child(
section("With Label").max_w_md().child(
h_flex()
.gap_2()
.child(
ProgressCircle::new("circle-progress-1")
.color(cx.theme().primary)
.value(self.value)
.size_4(),
)
.child("Downloading..."),
),
)
.child(
section("Circle with Color").max_w_md().child(
ProgressCircle::new("circle-progress-1")
.color(cx.theme().yellow)
.value(self.value)
.size_12(),
),
)
}
}

View File

@@ -2,7 +2,7 @@ use std::rc::Rc;
use crate::{
ActiveTheme, Colorize as _, Disableable, FocusableExt as _, Icon, IconName, Selectable,
Sizable, Size, StyleSized, StyledExt, h_flex, spinner::Spinner, tooltip::Tooltip,
Sizable, Size, StyleSized, StyledExt, button::ButtonIcon, h_flex, tooltip::Tooltip,
};
use gpui::{
Action, AnyElement, App, ClickEvent, Corners, Div, Edges, ElementId, Hsla, InteractiveElement,
@@ -179,7 +179,7 @@ pub struct Button {
id: ElementId,
base: Stateful<Div>,
style: StyleRefinement,
icon: Option<Icon>,
icon: Option<ButtonIcon>,
label: Option<SharedString>,
children: Vec<AnyElement>,
disabled: bool,
@@ -275,7 +275,7 @@ impl Button {
}
/// Set the icon of the button, if the Button have no label, the button well in Icon Button mode.
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
pub fn icon(mut self, icon: impl Into<ButtonIcon>) -> Self {
self.icon = Some(icon.into());
self
}
@@ -578,16 +578,11 @@ impl RenderOnce for Button {
Size::Small => this.gap_1(),
_ => this.gap_2(),
})
.when(!self.loading, |this| {
this.when_some(self.icon, |this, icon| {
this.child(icon.with_size(icon_size))
})
})
.when(self.loading, |this| {
.when_some(self.icon, |this, icon| {
this.child(
Spinner::new()
.with_size(self.size)
.when_some(self.loading_icon, |this, icon| this.icon(icon)),
icon.loading_icon(self.loading_icon)
.loading(self.loading)
.with_size(icon_size),
)
})
.when_some(self.label, |this, label| {

View File

@@ -0,0 +1,131 @@
use crate::{Icon, Sizable, Size, progress::ProgressCircle, spinner::Spinner};
use gpui::{App, IntoElement, RenderOnce, Window, prelude::FluentBuilder};
/// Button icon which can be an Icon, Spinner, or Progress use for `icon` method of Button.
#[doc(hidden)]
#[derive(IntoElement)]
pub struct ButtonIcon {
icon: ButtonIconVariant,
loading_icon: Option<Icon>,
loading: bool,
size: Size,
}
impl<T> From<T> for ButtonIcon
where
T: Into<ButtonIconVariant>,
{
fn from(icon: T) -> Self {
ButtonIcon::new(icon)
}
}
impl ButtonIcon {
/// Creates a new ButtonIcon with the given icon.
pub fn new(icon: impl Into<ButtonIconVariant>) -> Self {
Self {
icon: icon.into(),
loading_icon: None,
loading: false,
size: Size::Medium,
}
}
pub(crate) fn loading_icon(mut self, icon: Option<Icon>) -> Self {
self.loading_icon = icon;
self
}
pub(crate) fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
}
impl Sizable for ButtonIcon {
fn with_size(mut self, size: impl Into<crate::Size>) -> Self {
self.size = size.into();
self
}
}
/// Button icon which can be an Icon, Spinner, Progress, or ProgressCircle use for `icon` method of Button.
#[doc(hidden)]
#[derive(IntoElement)]
pub enum ButtonIconVariant {
Icon(Icon),
Spinner(Spinner),
Progress(ProgressCircle),
}
impl<T> From<T> for ButtonIconVariant
where
T: Into<Icon>,
{
fn from(icon: T) -> Self {
Self::Icon(icon.into())
}
}
impl From<Spinner> for ButtonIconVariant {
fn from(spinner: Spinner) -> Self {
Self::Spinner(spinner)
}
}
impl From<ProgressCircle> for ButtonIconVariant {
fn from(progress: ProgressCircle) -> Self {
Self::Progress(progress)
}
}
impl ButtonIconVariant {
/// Returns true if the ButtonIconKind is an Icon.
#[inline]
pub(crate) fn is_spinner(&self) -> bool {
matches!(self, Self::Spinner(_))
}
/// Returns true if the ButtonIconKind is a Progress or ProgressCircle.
#[inline]
pub(crate) fn is_progress(&self) -> bool {
matches!(self, Self::Progress(_))
}
}
impl Sizable for ButtonIconVariant {
fn with_size(self, size: impl Into<crate::Size>) -> Self {
match self {
Self::Icon(icon) => Self::Icon(icon.with_size(size)),
Self::Spinner(spinner) => Self::Spinner(spinner.with_size(size)),
Self::Progress(progress) => Self::Progress(progress.with_size(size)),
}
}
}
impl RenderOnce for ButtonIconVariant {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
match self {
Self::Icon(icon) => icon.into_any_element(),
Self::Spinner(spinner) => spinner.into_any_element(),
Self::Progress(progress) => progress.into_any_element(),
}
}
}
impl RenderOnce for ButtonIcon {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
if self.loading {
if self.icon.is_spinner() || self.icon.is_progress() {
self.icon.with_size(self.size).into_any_element()
} else {
Spinner::new()
.when_some(self.loading_icon, |this, icon| this.icon(icon))
.with_size(self.size)
.into_any_element()
}
} else {
self.icon.with_size(self.size).into_any_element()
}
}
}

View File

@@ -1,9 +1,11 @@
mod button;
mod button_group;
mod button_icon;
mod dropdown_button;
mod toggle;
pub use button::*;
pub use button_group::*;
pub(crate) use button_icon::*;
pub use dropdown_button::*;
pub use toggle::*;

View File

@@ -32,6 +32,9 @@ pub enum IconName {
Battery,
BatteryCharging,
BatteryFull,
BatteryLow,
BatteryMedium,
BatteryWarning,
Bell,
BookOpen,
Bot,
@@ -77,6 +80,7 @@ pub enum IconName {
LoaderCircle,
Map,
Maximize,
MemoryStick,
Menu,
Minimize,
Minus,
@@ -91,6 +95,8 @@ pub enum IconName {
PanelRight,
PanelRightClose,
PanelRightOpen,
Pause,
Play,
Plus,
Redo,
Redo2,
@@ -116,10 +122,6 @@ pub enum IconName {
WindowMaximize,
WindowMinimize,
WindowRestore,
MemoryStick,
BatteryLow,
BatteryMedium,
BatteryWarning,
}
impl IconName {
@@ -204,6 +206,8 @@ impl IconNamed for IconName {
Self::PanelRight => "icons/panel-right.svg",
Self::PanelRightClose => "icons/panel-right-close.svg",
Self::PanelRightOpen => "icons/panel-right-open.svg",
Self::Pause => "icons/pause.svg",
Self::Play => "icons/play.svg",
Self::Plus => "icons/plus.svg",
Self::Redo => "icons/redo.svg",
Self::Redo2 => "icons/redo-2.svg",

View File

@@ -0,0 +1,10 @@
mod progress;
mod progress_circle;
pub use progress::Progress;
pub use progress_circle::ProgressCircle;
/// Shared state for progress components.
pub(crate) struct ProgressState {
pub(crate) value: f32,
}

View File

@@ -1,4 +1,4 @@
use crate::{ActiveTheme, StyledExt};
use crate::{ActiveTheme, Sizable, Size, StyledExt};
use gpui::{
Animation, AnimationExt as _, App, ElementId, Hsla, InteractiveElement as _, IntoElement,
ParentElement, RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder, px,
@@ -6,13 +6,16 @@ use gpui::{
};
use std::time::Duration;
/// A Progress bar element.
use super::ProgressState;
/// A linear horizontal progress bar element.
#[derive(IntoElement)]
pub struct Progress {
id: ElementId,
style: StyleRefinement,
color: Option<Hsla>,
value: f32,
size: Size,
}
impl Progress {
@@ -22,12 +25,13 @@ impl Progress {
id: id.into(),
value: Default::default(),
color: None,
style: StyleRefinement::default().h(px(8.)).rounded(px(4.)),
style: StyleRefinement::default(),
size: Size::default(),
}
}
/// Set the color of the progress bar.
pub fn bg(mut self, color: impl Into<Hsla>) -> Self {
pub fn color(mut self, color: impl Into<Hsla>) -> Self {
self.color = Some(color.into());
self
}
@@ -47,18 +51,29 @@ impl Styled for Progress {
}
}
struct ProgressState {
value: f32,
impl Sizable for Progress {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
impl RenderOnce for Progress {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = self.color.unwrap_or(cx.theme().progress_bar);
let value = self.value;
let radius = self.style.corner_radii.clone();
let mut inner_style = StyleRefinement::default();
inner_style.corner_radii = radius;
let color = self.color.unwrap_or(cx.theme().progress_bar);
let value = self.value;
let (height, radius) = match self.size {
Size::XSmall => (px(4.), px(2.)),
Size::Small => (px(6.), px(3.)),
Size::Medium => (px(8.), px(4.)),
Size::Large => (px(10.), px(5.)),
Size::Size(s) => (s, s / 2.),
};
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| ProgressState { value });
let prev_value = state.read(cx).value;
@@ -68,6 +83,8 @@ impl RenderOnce for Progress {
.w_full()
.relative()
.rounded_full()
.h(height)
.rounded(radius)
.refine_style(&self.style)
.bg(color.opacity(0.2))
.child(
@@ -99,8 +116,7 @@ impl RenderOnce for Progress {
"progress-animation",
Animation::new(duration),
move |this, delta| {
let current_value =
prev_value + (value - prev_value) * delta;
let current_value = prev_value + (value - prev_value) * delta;
let relative_w = relative(match current_value {
v if v < 0. => 0.,
v if v > 100. => 1.,

View File

@@ -0,0 +1,187 @@
use crate::{ActiveTheme, PixelsExt, Sizable, Size, StyledExt};
use gpui::prelude::FluentBuilder as _;
use gpui::{
Animation, AnimationExt as _, App, ElementId, Hsla, InteractiveElement as _, IntoElement,
ParentElement, Pixels, RenderOnce, StyleRefinement, Styled, Window, canvas, px,
};
use gpui::{Bounds, div};
use std::f32::consts::TAU;
use std::time::Duration;
use super::ProgressState;
use crate::plot::shape::{Arc, ArcData};
/// A circular progress indicator element.
#[derive(IntoElement)]
pub struct ProgressCircle {
id: ElementId,
style: StyleRefinement,
color: Option<Hsla>,
value: f32,
size: Size,
}
impl ProgressCircle {
/// Create a new circular progress indicator.
pub fn new(id: impl Into<ElementId>) -> Self {
ProgressCircle {
id: id.into(),
value: Default::default(),
color: None,
style: StyleRefinement::default(),
size: Size::default(),
}
}
/// Set the color of the progress circle.
pub fn color(mut self, color: impl Into<Hsla>) -> Self {
self.color = Some(color.into());
self
}
/// Set the percentage value of the progress circle.
///
/// The value should be between 0.0 and 100.0.
pub fn value(mut self, value: f32) -> Self {
self.value = value.clamp(0., 100.);
self
}
fn render_circle(&self, current_value: f32, color: Hsla) -> impl IntoElement {
struct PrepaintState {
current_value: f32,
actual_inner_radius: f32,
actual_outer_radius: f32,
bounds: Bounds<Pixels>,
}
canvas(
{
let display_value = current_value;
move |bounds: Bounds<Pixels>, _window: &mut Window, _cx: &mut App| {
// Use 15% of width as stroke width, but max 5px
let stroke_width = (bounds.size.width * 0.15).min(px(5.));
// Calculate actual size from bounds
let actual_size = bounds.size.width.min(bounds.size.height);
let actual_radius = (actual_size.as_f32() - stroke_width.as_f32()) / 2.;
let actual_inner_radius = actual_radius - stroke_width.as_f32() / 2.;
let actual_outer_radius = actual_radius + stroke_width.as_f32() / 2.;
PrepaintState {
current_value: display_value,
actual_inner_radius,
actual_outer_radius,
bounds,
}
}
},
move |_bounds, prepaint, window: &mut Window, _cx: &mut App| {
// Draw background circle
let bg_arc_data = ArcData {
data: &(),
index: 0,
value: 100.,
start_angle: 0.,
end_angle: TAU,
pad_angle: 0.,
};
let bg_arc = Arc::new()
.inner_radius(prepaint.actual_inner_radius)
.outer_radius(prepaint.actual_outer_radius);
bg_arc.paint(
&bg_arc_data,
color.opacity(0.2),
None,
None,
&prepaint.bounds,
window,
);
// Draw progress arc
if prepaint.current_value > 0. {
let progress_angle = (prepaint.current_value / 100.) * TAU;
let progress_arc_data = ArcData {
data: &(),
index: 1,
value: prepaint.current_value,
start_angle: 0.,
end_angle: progress_angle,
pad_angle: 0.,
};
let progress_arc = Arc::new()
.inner_radius(prepaint.actual_inner_radius)
.outer_radius(prepaint.actual_outer_radius);
progress_arc.paint(
&progress_arc_data,
color,
None,
None,
&prepaint.bounds,
window,
);
}
},
)
.absolute()
.size_full()
}
}
impl Styled for ProgressCircle {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl Sizable for ProgressCircle {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
impl RenderOnce for ProgressCircle {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let value = self.value;
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| ProgressState { value });
let prev_value = state.read(cx).value;
let color = self.color.unwrap_or(cx.theme().progress_bar);
let has_changed = prev_value != value;
div()
.id(self.id.clone())
.flex()
.items_center()
.justify_center()
.map(|this| match self.size {
Size::XSmall => this.size_2(),
Size::Small => this.size_3(),
Size::Medium => this.size_4(),
Size::Large => this.size_5(),
Size::Size(s) => this.size(s * 0.75),
})
.refine_style(&self.style)
.map(|this| {
if has_changed {
this.with_animation(
format!("progress-circle-{}", prev_value),
Animation::new(Duration::from_secs_f64(0.15)),
move |this, delta| {
let animated_value = prev_value + (value - prev_value) * delta;
this.child(self.render_circle(animated_value, color))
},
)
.into_any_element()
} else {
this.child(self.render_circle(value, color))
.into_any_element()
}
})
}
}

View File

@@ -89,22 +89,138 @@ Button::new("btn").large().label("Large")
### With Icons
The `icon` method supports multiple types, allowing you to use different visual indicators:
- **[Icon] / [IconName]** - Static icons for actions and visual cues
- **[Spinner]** - Animated loading indicator for async operations
- **[ProgressCircle]** - Circular progress indicator showing completion percentage
All icon types automatically adapt to the button's size and can be customized with colors and other properties.
#### Icon Types
```rust
use gpui_component::{Icon, IconName};
// Icon before label
// Using IconName (simplest)
Button::new("btn")
.icon(IconName::Check)
.label("Confirm")
// Icon only
Button::new("btn")
.icon(IconName::Search)
// Custom icon size
// Using Icon with custom size
Button::new("btn")
.icon(Icon::new(IconName::Heart))
.label("Like")
// Icon only (no label)
Button::new("btn")
.icon(IconName::Search)
```
#### Spinner Icon
Use a [Spinner] to indicate loading or processing state:
```rust
use gpui_component::spinner::Spinner;
// Basic spinner
Button::new("btn")
.icon(Spinner::new())
.label("Loading...")
// Spinner with custom color
Button::new("btn")
.icon(Spinner::new().color(cx.theme().blue))
.label("Processing")
// Spinner with icon
Button::new("btn")
.icon(Spinner::new().icon(IconName::LoaderCircle))
.label("Syncing")
```
#### ProgressCircle Icon
Use a [ProgressCircle] to show progress percentage:
```rust
use gpui_component::progress::ProgressCircle;
// Basic progress circle
Button::new("btn")
.icon(ProgressCircle::new("install-progress").value(45.0))
.label("Installing...")
// Progress circle with custom color
Button::new("btn")
.primary()
.icon(
ProgressCircle::new("download-progress")
.value(75.0)
.color(cx.theme().primary_foreground)
)
.label("Downloading")
// Different sizes
Button::new("btn")
.small()
.icon(ProgressCircle::new("progress-1").value(60.0))
.label("Installing...")
Button::new("btn")
.large()
.icon(ProgressCircle::new("progress-2").value(80.0))
.label("Installing...")
```
#### Dynamic Icon Updates
Icons can be updated dynamically based on component state:
```rust
struct InstallButton {
progress: f32,
is_installing: bool,
}
impl InstallButton {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let button = Button::new("install-btn")
.label(if self.is_installing {
"Installing..."
} else {
"Install"
});
if self.is_installing {
button.icon(
ProgressCircle::new("install-progress")
.value(self.progress)
)
} else {
button.icon(IconName::Download)
}
}
}
```
#### Loading State with Icons
When a button is in loading state, it automatically handles icon transitions:
```rust
// If icon is already a Spinner or ProgressCircle, it will be shown during loading
Button::new("btn")
.icon(Spinner::new())
.label("Processing")
.loading(true) // Spinner will continue to show
// If icon is a regular Icon, it will be replaced with a Spinner during loading
Button::new("btn")
.icon(IconName::Save)
.label("Saving")
.loading(true) // Icon will be replaced with Spinner
```
### With a dropdown caret icon
@@ -211,3 +327,7 @@ Button::new("btn")
[ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html
[ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html
[Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html
[Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html
[ProgressCircle]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.ProgressCircle.html
[Icon]: https://docs.rs/gpui-component/latest/gpui_component/icon/struct.Icon.html
[IconName]: https://docs.rs/gpui-component/latest/gpui_component/icon/enum.IconName.html

View File

@@ -1,21 +1,24 @@
---
title: Progress
description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.
description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar or circular indicator.
---
# Progress
A linear progress bar component that visually represents the completion percentage of a task. The progress bar features smooth animations, customizable colors, and automatic styling that adapts to the current theme.
Progress components visually represent the completion percentage of a task. The library provides two variants:
## Import
- **[Progress](#progress)** - A linear horizontal progress bar
- **[ProgressCircle](#progresscircle)** - A circular progress indicator
Both components feature smooth animations, customizable colors, and automatic styling that adapts to the current theme.
## Progress
```rust
use gpui_component::progress::Progress;
use gpui_component::progress::{Progress, ProgressCircle};
```
## Usage
### Basic Progress Bar
### Usage
```rust
Progress::new("my-progress")
@@ -190,6 +193,92 @@ impl MultiStepProcess {
}
```
## ProgressCircle
A circular progress indicator component that displays progress as an arc around a circle. Perfect for compact spaces, button icons, or when you want a more modern, space-efficient progress display.
```rust
use gpui_component::progress::ProgressCircle;
```
### Basic ProgressCircle
```rust
ProgressCircle::new("my-progress-circle")
.value(50.0) // 50% complete
```
### Different Sizes
ProgressCircle supports different sizes through the `Sizable` trait:
```rust
// Extra small
ProgressCircle::new("progress-xs")
.value(25.0)
.xsmall()
// Small
ProgressCircle::new("progress-sm")
.value(50.0)
.small()
// Medium (default)
ProgressCircle::new("progress-md")
.value(75.0)
.medium()
// Large
ProgressCircle::new("progress-lg")
.value(100.0)
.large()
// Custom size
ProgressCircle::new("progress-custom")
.value(60.0)
.size(px(80.))
```
### Custom Colors
```rust
// Use theme colors (default)
ProgressCircle::new("progress-default")
.value(50.0)
// Custom color
ProgressCircle::new("progress-green")
.value(75.0)
.color(cx.theme().green)
// Different color variants
ProgressCircle::new("progress-blue")
.value(60.0)
.color(cx.theme().blue)
ProgressCircle::new("progress-yellow")
.value(40.0)
.color(cx.theme().yellow)
ProgressCircle::new("progress-red")
.value(80.0)
.color(cx.theme().red)
```
### With Labels
```rust
h_flex()
.gap_2()
.items_center()
.child(
ProgressCircle::new("download-progress")
.value(65.0)
.size_4()
)
.child("Downloading... 65%")
```
## Examples
### Task Progress with Status
@@ -329,33 +418,3 @@ The Progress component automatically adapts to the current theme:
// These colors adapt to light/dark theme automatically
Progress::new("themed-progress").value(75.0) // Uses theme colors
```
### Visual Properties
- **Height**: 8px by default
- **Border Radius**: Matches theme radius (up to half the height)
- **Background**: Semi-transparent theme progress bar color (20% opacity)
- **Fill**: Full opacity theme progress bar color
- **Animation**: Smooth transitions when value changes
- **Corners**: Rounded on completion, left-rounded during progress
## Behavior Notes
- Values less than 0 are clamped to 0%
- Values greater than 100 are clamped to 100%
- Progress bar fills from left to right
- Border radius adjusts based on completion state:
- Partial progress: Left side rounded only
- Complete progress: Both sides rounded
- Background color is always a semi-transparent version of the fill color
- Height and radius adapt to theme settings automatically
## Best Practices
1. **Always provide text indicators** alongside the visual progress bar
2. **Use meaningful labels** to describe what is progressing
3. **Update progress regularly** but not too frequently to avoid performance issues
4. **Consider showing ETA or completion time** for long-running tasks
5. **Provide cancel/pause options** for lengthy operations
6. **Show final status** when progress reaches 100%
7. **Handle error states** gracefully with appropriate messaging