diff --git a/Cargo.lock b/Cargo.lock index 82393cc7..6485720c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5427,7 +5427,6 @@ dependencies = [ "taffy", "unicode-segmentation", "usvg", - "uuid", "windows", "wry", ] diff --git a/README.md b/README.md index 7d6e7a48..07c3c19c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This is an example of build app by using GPUI. - [x] Button with Icon - [x] IconButton - [x] Glost / Outline Button + - [x] Loading - [x] Label - [x] Icon - [x] Checkbox @@ -58,8 +59,8 @@ This is an example of build app by using GPUI. - [x] Dockpanel - [x] Resizable - [x] Progress - - [ ] ProgressBar - - [ ] Loading + - [x] ProgressBar + - [x] Indicator - [ ] Skeleton - [ ] DatePicker - [ ] DateTimePicker diff --git a/assets/icons/loader-circle.svg b/assets/icons/loader-circle.svg new file mode 100644 index 00000000..12cb72f6 --- /dev/null +++ b/assets/icons/loader-circle.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/loader.svg b/assets/icons/loader.svg new file mode 100644 index 00000000..f6ff93fa --- /dev/null +++ b/assets/icons/loader.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 44061cbb..f1e87b85 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -9,9 +9,9 @@ use workspace::{dock::DockPosition, TitleBar, Workspace}; use std::sync::Arc; use ui::{ - button::ButtonSize, switch::{LabelSide, Switch}, theme::{ActiveTheme, Theme}, + Size, }; use crate::app_state::AppState; @@ -173,7 +173,7 @@ impl StoryWorkspace { appears_transparent: true, traffic_light_position: Some(point(px(9.0), px(9.0))), }), - window_min_size: Some(Size { + window_min_size: Some(gpui::Size { width: px(640.), height: px(480.), }), @@ -255,7 +255,7 @@ impl Render for StoryWorkspace { .mr_3() .child( Switch::new("theme-mode") - .size(ButtonSize::Small) + .size(Size::Small) .checked(cx.theme().mode.is_dark()) .label_side(LabelSide::Left) .label("Dark Mode") diff --git a/crates/story/src/button_story.rs b/crates/story/src/button_story.rs index 039dc2b0..38e2baf0 100644 --- a/crates/story/src/button_story.rs +++ b/crates/story/src/button_story.rs @@ -4,8 +4,8 @@ use gpui::{ }; use ui::{ - button::{Button, ButtonSize, ButtonStyle}, - h_flex, v_flex, Clickable, Disableable as _, Icon, IconName, Selectable, + button::{Button, ButtonStyle}, + h_flex, v_flex, Clickable, Disableable as _, Icon, IconName, Selectable, Size, }; use crate::section; @@ -96,21 +96,22 @@ impl Render for ButtonStory { Button::new("button-6", cx) .label("Primary Button") .style(ButtonStyle::Primary) - .size(ButtonSize::Small) + .size(Size::Small) + .loading(true) .on_click(Self::on_click), ) .child( Button::new("button-7", cx) .label("Secondary Button") .style(ButtonStyle::Secondary) - .size(ButtonSize::Small) + .size(Size::Small) .on_click(Self::on_click), ) .child( Button::new("button-8", cx) .label("Danger Button") .style(ButtonStyle::Danger) - .size(ButtonSize::Small) + .size(Size::Small) .on_click(Self::on_click), ), ) @@ -120,21 +121,22 @@ impl Render for ButtonStory { Button::new("button-xs-1", cx) .label("Primary Button") .style(ButtonStyle::Primary) - .size(ButtonSize::XSmall) + .size(Size::XSmall) .on_click(Self::on_click), ) .child( Button::new("button-xs-2", cx) .label("Secondary Button") .style(ButtonStyle::Secondary) - .size(ButtonSize::XSmall) + .size(Size::XSmall) + .loading(true) .on_click(Self::on_click), ) .child( Button::new("button-xs-3", cx) .label("Danger Button") .style(ButtonStyle::Danger) - .size(ButtonSize::XSmall) + .size(Size::XSmall) .on_click(Self::on_click), ), ), @@ -163,7 +165,8 @@ impl Render for ButtonStory { .label("Disabled Button") .style(ButtonStyle::Danger) .on_click(Self::on_click) - .disabled(true), + .disabled(true) + .loading(true), ), ) .child( @@ -195,7 +198,11 @@ impl Render for ButtonStory { .icon(IconName::Search) .style(ButtonStyle::Primary), ) - .child(Button::new("icon-button-1", cx).icon(IconName::Info)) + .child( + Button::new("icon-button-1", cx) + .icon(IconName::Info) + .loading(true), + ) .child( Button::new("icon-button-2", cx) .icon(IconName::Close) @@ -204,7 +211,7 @@ impl Render for ButtonStory { .child( Button::new("icon-button-3", cx) .icon(IconName::Search) - .size(ButtonSize::Small) + .size(Size::Small) .style(ButtonStyle::Primary), ) .child( @@ -223,29 +230,29 @@ impl Render for ButtonStory { .child( Button::new("icon-button-4", cx) .icon(IconName::Info) - .size(ButtonSize::Small), + .size(Size::Small), ) .child( Button::new("icon-button-5", cx) .icon(IconName::Close) - .size(ButtonSize::Small) + .size(Size::Small) .style(ButtonStyle::Danger), ) .child( Button::new("icon-button-6", cx) .icon(IconName::Search) - .size(ButtonSize::XSmall) + .size(Size::XSmall) .style(ButtonStyle::Primary), ) .child( Button::new("icon-button-7", cx) .icon(IconName::Info) - .size(ButtonSize::XSmall), + .size(Size::XSmall), ) .child( Button::new("icon-button-8", cx) .icon(IconName::Close) - .size(ButtonSize::XSmall) + .size(Size::XSmall) .style(ButtonStyle::Danger), ), ) diff --git a/crates/story/src/popover_story.rs b/crates/story/src/popover_story.rs index cb0bc9d3..dd11c90e 100644 --- a/crates/story/src/popover_story.rs +++ b/crates/story/src/popover_story.rs @@ -4,7 +4,7 @@ use gpui::{ ParentElement as _, Render, Styled as _, View, ViewContext, VisualContext, WindowContext, }; use ui::{ - button::{Button, ButtonSize}, + button::Button, divider::Divider, h_flex, input::TextInput, @@ -12,7 +12,7 @@ use ui::{ popup_menu::PopupMenu, prelude::FluentBuilder, switch::Switch, - v_flex, Clickable, IconName, + v_flex, Clickable, IconName, Size, }; actions!(popover_story, [Copy, Paste, Cut, SearchAll]); @@ -141,7 +141,7 @@ impl Render for PopoverStory { Button::new("info1", cx) .label("Yes") .width(px(80.)) - .size(ButtonSize::Small), + .size(Size::Small), ) .into_any() }) @@ -164,7 +164,7 @@ impl Render for PopoverStory { Button::new("info1", cx) .label("Yes") .width(px(80.)) - .size(ButtonSize::Small), + .size(Size::Small), ) .into_any() }) @@ -234,7 +234,7 @@ impl Render for PopoverStory { Button::new("info1", cx) .label("Yes") .width(px(80.)) - .size(ButtonSize::Small), + .size(Size::Small), ) .into_any() }) diff --git a/crates/story/src/progress_story.rs b/crates/story/src/progress_story.rs index 95792f48..95100126 100644 --- a/crates/story/src/progress_story.rs +++ b/crates/story/src/progress_story.rs @@ -1,8 +1,11 @@ use gpui::{ - div, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext, + div, px, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext, WindowContext, }; -use ui::{button::Button, h_flex, progress::Progress, v_flex, Clickable, IconName}; +use ui::{ + button::Button, h_flex, indicator::Indicator, progress::Progress, v_flex, Clickable, IconName, + Size, +}; pub struct ProgressStory { value: f32, @@ -74,5 +77,19 @@ impl Render for ProgressStory { })), ), ) + .child( + h_flex() + .gap_x_2() + .child(Indicator::new().size(Size::XSmall)) + .child(Indicator::new().size(Size::Small)) + .child(Indicator::new()) + .child( + Indicator::new() + .size(Size::Large) + .icon(IconName::LoaderCircle) + .color(ui::blue_500()), + ) + .child(Indicator::new().size(px(64.))), + ) } } diff --git a/crates/story/src/switch_story.rs b/crates/story/src/switch_story.rs index 85d6d8bc..ea0e48b2 100644 --- a/crates/story/src/switch_story.rs +++ b/crates/story/src/switch_story.rs @@ -4,12 +4,11 @@ use gpui::{ }; use ui::{ - button::ButtonSize, h_flex, label::Label, switch::{LabelSide, Switch}, theme::ActiveTheme, - v_flex, Disableable as _, StyledExt, + v_flex, Disableable as _, Size, StyledExt, }; #[derive(Default)] @@ -116,7 +115,7 @@ impl Render for SwitchStory { .items_start().child(title("Disabled Switchs")).child( h_flex().items_center() .gap_6() - .child(Switch::new("switch3").checked(self.switch3).label("Small Size").size(ButtonSize::Small).on_click(cx.listener(move |view, checked, cx| { + .child(Switch::new("switch3").checked(self.switch3).label("Small Size").size(Size::Small).on_click(cx.listener(move |view, checked, cx| { view.switch3 = *checked; cx.notify(); })), diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 04d0e31f..0ec2a811 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -24,7 +24,6 @@ usvg = { version = "0.41.0", default-features = false } taffy = "0.4.3" paste = "1" once_cell = "1.19.0" -uuid = "1.10.0" [lints] workspace = true diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index cb376664..f3b35422 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -1,7 +1,8 @@ use crate::{ h_flex, + indicator::Indicator, theme::{ActiveTheme, Colorize as _}, - Clickable, Disableable, Icon, Selectable, StyledExt, + Clickable, Disableable, Icon, Selectable, Size, StyledExt, }; use gpui::{ div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, FocusHandle, @@ -16,13 +17,6 @@ pub enum ButtonRounded { Large, } -#[derive(Clone, Copy)] -pub enum ButtonSize { - XSmall, - Small, - Medium, -} - #[derive(Clone, Copy)] pub enum ButtonStyle { Primary, @@ -45,9 +39,10 @@ pub struct Button { height: Option, style: ButtonStyle, rounded: ButtonRounded, - size: ButtonSize, + size: Size, tooltip: Option, on_click: Option>, + loading: bool, } impl Button { @@ -64,9 +59,10 @@ impl Button { width: None, height: None, rounded: ButtonRounded::Medium, - size: ButtonSize::Medium, + size: Size::Medium, tooltip: None, on_click: None, + loading: false, } } @@ -91,7 +87,7 @@ impl Button { label: impl Into, cx: &mut WindowContext, ) -> Self { - Self::new(id, cx).label(label).size(ButtonSize::Small) + Self::new(id, cx).label(label).size(Size::Small) } pub fn width(mut self, width: impl Into) -> Self { @@ -109,8 +105,8 @@ impl Button { self } - pub fn size(mut self, size: ButtonSize) -> Self { - self.size = size; + pub fn size(mut self, size: impl Into) -> Self { + self.size = size.into(); self } @@ -134,6 +130,11 @@ impl Button { self.style = style; self } + + pub fn loading(mut self, loading: bool) -> Self { + self.loading = loading; + self + } } impl Disableable for Button { @@ -180,15 +181,16 @@ impl RenderOnce for Button { .map(|this| { if self.label.is_none() { match self.size { - ButtonSize::XSmall => this.size_5(), - ButtonSize::Small => this.size_6(), - ButtonSize::Medium => this.size_8(), + Size::Size(px) => this.size(px), + Size::XSmall => this.size_5(), + Size::Small => this.size_6(), + Size::Large | Size::Medium => this.size_8(), } } else { match self.size { - ButtonSize::XSmall => this.px_1().py_1().h_5(), - ButtonSize::Small => this.px_3().py_2().h_6(), - ButtonSize::Medium => this.px_4().py_2().h_8(), + Size::XSmall => this.px_1().py_1().h_5(), + Size::Small => this.px_3().py_2().h_6(), + _ => this.px_4().py_2().h_8(), } } }) @@ -247,14 +249,19 @@ impl RenderOnce for Button { .justify_center() .gap_2() .text_color(text_color) - .when_some(self.icon, |this, icon| { - this.child(icon.size(self.size).text_color(text_color)) + .when(!self.loading, |this| { + this.when_some(self.icon, |this, icon| { + this.child(icon.size(self.size).text_color(text_color)) + }) + }) + .when(self.loading, |this| { + this.child(Indicator::new().size(self.size).color(text_color)) }) .when_some(self.label, |this, label| this.child(label)) .map(|this| match self.size { - ButtonSize::XSmall => this.text_xs(), - ButtonSize::Small => this.text_sm(), - ButtonSize::Medium => this.text_base(), + Size::XSmall => this.text_xs(), + Size::Small => this.text_sm(), + _ => this.text_base(), }) }) } diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 06239b65..45794598 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -1,6 +1,6 @@ -use crate::{button::ButtonSize, theme::ActiveTheme}; +use crate::{theme::ActiveTheme, Size}; use gpui::{ - prelude::FluentBuilder as _, svg, AnyElement, Hsla, IntoElement, Pixels, Render, RenderOnce, + prelude::FluentBuilder as _, svg, AnyElement, Hsla, IntoElement, Render, RenderOnce, SharedString, StyleRefinement, Styled, Svg, View, VisualContext, WindowContext, }; @@ -21,6 +21,8 @@ pub enum IconName { Search, Delete, CircleX, + Loader, + LoaderCircle, } impl IconName { @@ -41,6 +43,8 @@ impl IconName { IconName::Search => "icons/search.svg", IconName::Delete => "icons/delete.svg", IconName::CircleX => "icons/circle-x.svg", + IconName::Loader => "icons/loader.svg", + IconName::LoaderCircle => "icons/loader-circle.svg", } .into() } @@ -69,36 +73,12 @@ impl RenderOnce for IconName { } } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum IconSize { - XSmall, - Small, - Medium, - Size(Pixels), -} - -impl From for IconSize { - fn from(size: ButtonSize) -> Self { - match size { - ButtonSize::XSmall => IconSize::XSmall, - ButtonSize::Small => IconSize::Small, - ButtonSize::Medium => IconSize::Medium, - } - } -} - -impl From for IconSize { - fn from(size: Pixels) -> Self { - IconSize::Size(size) - } -} - #[derive(IntoElement)] pub struct Icon { base: Svg, path: SharedString, text_color: Option, - size: IconSize, + size: Size, } impl Default for Icon { @@ -107,7 +87,7 @@ impl Default for Icon { base: svg().flex_none().size_4(), path: "".into(), text_color: None, - size: IconSize::Medium, + size: Size::Medium, } } } @@ -135,7 +115,7 @@ impl Icon { /// /// Also can receive a `ButtonSize` to convert to `IconSize`, /// Or a `Pixels` to set a custom size: `px(30.)` - pub fn size(mut self, size: impl Into) -> Self { + pub fn size(mut self, size: impl Into) -> Self { self.size = size.into(); self @@ -145,6 +125,11 @@ impl Icon { pub fn view(self, cx: &mut WindowContext) -> View { cx.new_view(|_| self) } + + pub fn transform(mut self, transformation: gpui::Transformation) -> Self { + self.base = self.base.with_transformation(transformation); + self + } } impl Styled for Icon { @@ -165,10 +150,11 @@ impl RenderOnce for Icon { self.base .text_color(text_color) .map(|this| match self.size { - IconSize::Size(px) => this.size(px), - IconSize::XSmall => this.size_3(), - IconSize::Small => this.size_3p5(), - IconSize::Medium => this.size_4(), + Size::Size(px) => this.size(px), + Size::XSmall => this.size_3(), + Size::Small => this.size_3p5(), + Size::Medium => this.size_4(), + Size::Large => this.size_6(), }) .path(self.path) } @@ -189,10 +175,11 @@ impl Render for Icon { .size_4() .text_color(text_color) .map(|this| match self.size { - IconSize::Size(px) => this.size(px), - IconSize::XSmall => this.size_3(), - IconSize::Small => this.size_3p5(), - IconSize::Medium => this.size_4(), + Size::Size(px) => this.size(px), + Size::XSmall => this.size_3(), + Size::Small => this.size_3p5(), + Size::Medium => this.size_4(), + Size::Large => this.size_6(), }) .path(self.path.clone()) } diff --git a/crates/ui/src/indicator.rs b/crates/ui/src/indicator.rs new file mode 100644 index 00000000..8131d4f3 --- /dev/null +++ b/crates/ui/src/indicator.rs @@ -0,0 +1,59 @@ +use std::time::Duration; + +use crate::{theme::ActiveTheme, Icon, IconName, Size}; +use gpui::{ + div, ease_in_out, percentage, Animation, AnimationExt as _, Hsla, IntoElement, ParentElement, + RenderOnce, Styled as _, Transformation, +}; + +#[derive(IntoElement)] +pub struct Indicator { + size: Size, + icon: IconName, + speed: Duration, + color: Option, +} + +impl Indicator { + pub fn new() -> Self { + Self { + size: Size::Medium, + speed: Duration::from_secs_f64(0.8), + icon: IconName::Loader, + color: None, + } + } + + pub fn size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } + + pub fn icon(mut self, icon: IconName) -> Self { + self.icon = icon; + self + } + + pub fn color(mut self, color: Hsla) -> Self { + self.color = Some(color); + self + } +} + +impl RenderOnce for Indicator { + fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { + let color = self.color.unwrap_or_else(|| cx.theme().indicator); + div() + .child( + Icon::new(self.icon) + .size(self.size) + .text_color(color) + .with_animation( + "circle", + Animation::new(self.speed).repeat().with_easing(ease_in_out), + |this, delta| this.transform(Transformation::rotate(percentage(delta))), + ), + ) + .into_element() + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index e8a4aaec..965b59fe 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -4,6 +4,7 @@ mod disableable; mod event; mod focusable; mod icon; + mod scroll; mod scrollbar; mod selectable; @@ -15,6 +16,7 @@ pub mod button; pub mod checkbox; pub mod divider; pub mod dropdown; +pub mod indicator; pub mod input; pub mod label; pub mod list; @@ -35,7 +37,7 @@ pub use disableable::Disableable; pub use event::InterativeElementExt; pub use focusable::FocusableCycle; pub use selectable::{Selectable, Selection}; -pub use styled_ext::StyledExt; +pub use styled_ext::{Size, StyledExt}; pub use colors::*; pub use icon::*; diff --git a/crates/ui/src/progress.rs b/crates/ui/src/progress.rs index c7516552..43729c52 100644 --- a/crates/ui/src/progress.rs +++ b/crates/ui/src/progress.rs @@ -47,7 +47,7 @@ impl RenderOnce for Progress { }) .h_full() .w(relative_w) - .bg(cx.theme().primary), + .bg(cx.theme().progress_bar), ) .child( div() @@ -57,7 +57,7 @@ impl RenderOnce for Progress { }) .h_full() .flex_1() - .bg(cx.theme().primary.opacity(0.2)), + .bg(cx.theme().progress_bar.opacity(0.2)), ) } } diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index d39bd9c4..aad51b1a 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -1,5 +1,5 @@ use crate::theme::ActiveTheme; -use gpui::{hsla, point, px, BoxShadow, FocusHandle, Styled, WindowContext}; +use gpui::{hsla, point, px, BoxShadow, FocusHandle, Pixels, Styled, WindowContext}; use smallvec::{smallvec, SmallVec}; pub enum ElevationIndex { @@ -133,3 +133,19 @@ pub trait StyledExt: Styled + Sized { } impl StyledExt for E {} + +/// A size for elements. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Size { + Size(Pixels), + XSmall, + Small, + Medium, + Large, +} + +impl From for Size { + fn from(size: Pixels) -> Self { + Size::Size(size) + } +} diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 4c14f9b2..b79a75a8 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,8 +1,7 @@ use crate::{ - button::ButtonSize, stack::h_flex, theme::{ActiveTheme, Colorize}, - Disableable, + Disableable, Size, }; use gpui::{ div, prelude::FluentBuilder as _, Div, InteractiveElement, IntoElement, ParentElement as _, @@ -31,7 +30,7 @@ pub struct Switch { label: Option, label_side: LabelSide, on_click: Option, - size: ButtonSize, + size: Size, } impl Switch { @@ -46,7 +45,7 @@ impl Switch { label: None, on_click: None, label_side: LabelSide::Right, - size: ButtonSize::Medium, + size: Size::Medium, } } @@ -60,7 +59,8 @@ impl Switch { self } - pub fn size(mut self, size: ButtonSize) -> Self { + /// Only supported XSmall, Small, Medium + pub fn size(mut self, size: Size) -> Self { self.size = size; self } @@ -107,8 +107,8 @@ impl RenderOnce for Switch { .child( self.base .map(|this| match self.size { - ButtonSize::Medium => this.w_11().h_6().rounded_xl(), - ButtonSize::XSmall | ButtonSize::Small => this.w_8().h_4().rounded_lg(), + Size::XSmall | Size::Small => this.w_8().h_4().rounded_lg(), + _ => this.w_11().h_6().rounded_xl(), }) .flex() .items_center() @@ -125,15 +125,15 @@ impl RenderOnce for Switch { .rounded_full() .bg(toggle_bg) .map(|this| match self.size { - ButtonSize::Medium => this.w_5().h_5(), - ButtonSize::XSmall | ButtonSize::Small => this.w_3().h_3(), + Size::XSmall | Size::Small => this.w_3().h_3(), + _ => this.w_5().h_5(), }), ), ) .when_some(self.label, |this, label| { this.child(div().child(label).map(|this| match self.size { - ButtonSize::Medium => this.text_base(), - ButtonSize::XSmall | ButtonSize::Small => this.text_sm(), + Size::XSmall | Size::Small => this.text_sm(), + _ => this.text_base(), })) }) .when_some( diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index a27eb8d9..a9442922 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -301,6 +301,8 @@ pub struct Theme { pub tab_active: Hsla, pub tab_foreground: Hsla, pub tab_active_foreground: Hsla, + pub indicator: Hsla, + pub progress_bar: Hsla, } impl Global for Theme {} @@ -355,6 +357,8 @@ impl From for Theme { tab_active: colors.background, tab_foreground: colors.foreground, tab_active_foreground: colors.foreground, + indicator: colors.secondary_foreground, + progress_bar: colors.primary, } } }