You've already forked gpui-component
mirror of
https://github.com/librekeys/gpui-component.git
synced 2026-04-14 08:46:29 -07:00
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:
1
crates/assets/assets/icons/pause.svg
Normal file
1
crates/assets/assets/icons/pause.svg
Normal 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 |
1
crates/assets/assets/icons/play.svg
Normal file
1
crates/assets/assets/icons/play.svg
Normal 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 |
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
131
crates/ui/src/button/button_icon.rs
Normal file
131
crates/ui/src/button/button_icon.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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",
|
||||
|
||||
10
crates/ui/src/progress/mod.rs
Normal file
10
crates/ui/src/progress/mod.rs
Normal 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,
|
||||
}
|
||||
@@ -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.,
|
||||
187
crates/ui/src/progress/progress_circle.rs
Normal file
187
crates/ui/src/progress/progress_circle.rs
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user