From e9d409a4b1772481ea7426fd9b3e70898fcebf9d Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 1 Aug 2024 19:54:35 +0800 Subject: [PATCH] Add Link style Button (#97) image - Update `primary`, `danger`, `outline`, `ghost`, `link` methods to as a builder method to just change style. - Add `compact` used to reduce padding. image --- assets/icons/menu.svg | 1 + crates/story/src/button_story.rs | 368 +++++++++++++++++++++++------- crates/story/src/popover_story.rs | 19 +- crates/ui/src/button.rs | 247 +++++++++++++------- crates/ui/src/checkbox.rs | 4 +- crates/ui/src/icon.rs | 2 + crates/ui/src/indicator.rs | 11 +- crates/ui/src/selectable.rs | 10 + crates/ui/src/theme.rs | 6 +- 9 files changed, 490 insertions(+), 178 deletions(-) create mode 100644 assets/icons/menu.svg diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg new file mode 100644 index 00000000..6598697f --- /dev/null +++ b/assets/icons/menu.svg @@ -0,0 +1 @@ + diff --git a/crates/story/src/button_story.rs b/crates/story/src/button_story.rs index 62173d2f..fc1784e9 100644 --- a/crates/story/src/button_story.rs +++ b/crates/story/src/button_story.rs @@ -5,18 +5,29 @@ use gpui::{ use ui::{ button::{Button, ButtonCustomStyle, ButtonStyle}, + checkbox::Checkbox, h_flex, theme::ActiveTheme, - v_flex, Clickable, Disableable as _, Icon, IconName, Selectable, Size, + v_flex, Clickable, Disableable as _, Icon, IconName, Selectable as _, Size, }; use crate::section; -pub struct ButtonStory {} +pub struct ButtonStory { + disabled: bool, + loading: bool, + selected: bool, + compact: bool, +} impl ButtonStory { pub fn view(cx: &mut WindowContext) -> View { - cx.new_view(|_| Self {}) + cx.new_view(|_| Self { + disabled: false, + loading: false, + selected: false, + compact: false, + }) } fn on_click(ev: &ClickEvent, _: &mut WindowContext) { @@ -26,8 +37,54 @@ impl ButtonStory { impl Render for ButtonStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let disabled = self.disabled; + let loading = self.loading; + let selected = self.selected; + let compact = self.compact; + v_flex() .gap_6() + .child( + h_flex() + .gap_3() + .child("State") + .child( + Checkbox::new("disabled-button") + .label("Disabled") + .checked(self.disabled) + .on_click(cx.listener(|view, _, cx| { + view.disabled = !view.disabled; + cx.notify(); + })), + ) + .child( + Checkbox::new("loading-button") + .label("Loading") + .checked(self.loading) + .on_click(cx.listener(|view, _, cx| { + view.loading = !view.loading; + cx.notify(); + })), + ) + .child( + Checkbox::new("selected-button") + .label("Selected") + .checked(self.selected) + .on_click(cx.listener(|view, _, cx| { + view.selected = !view.selected; + cx.notify(); + })), + ) + .child( + Checkbox::new("compact-button") + .label("Compact") + .checked(self.compact) + .on_click(cx.listener(|view, _, cx| { + view.compact = !view.compact; + cx.notify(); + })), + ), + ) .child( h_flex() .gap_6() @@ -35,45 +92,78 @@ impl Render for ButtonStory { section("Normal Button", cx) .child( Button::new("button-1", cx) + .primary() .label("Primary Button") - .style(ButtonStyle::Primary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( Button::new("button-2", cx) .label("Secondary Button") - .style(ButtonStyle::Secondary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( Button::new("button-4", cx) + .danger() .label("Danger Button") - .style(ButtonStyle::Danger) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( Button::new("button-5", cx) + .outline() .label("Outline Button") - .style(ButtonStyle::Outline) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( Button::new("button-5-ghost", cx) + .ghost() .label("Ghost Button") - .style(ButtonStyle::Ghost) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), + ) + .child( + Button::new("button-5-link", cx) + .link() + .label("Link Button") + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( Button::new("button-6-custom", cx) - .label("Custom Button") - .style(ButtonStyle::Custom( + .custom( ButtonCustomStyle::new(cx) .color(cx.theme().muted) .foreground(cx.theme().destructive) .border(cx.theme().scrollbar) .hover(cx.theme().tab_active_foreground) .active(cx.theme().selection), - )) + ) + .label("Custom Button") + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ), ) @@ -81,23 +171,33 @@ impl Render for ButtonStory { section("Button with Icon", cx) .child( Button::new("button-icon-1", cx) + .primary() .label("Confirm") .icon(IconName::Check) - .style(ButtonStyle::Primary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( Button::new("button-icon-2", cx) .label("Abort") .icon(IconName::Close) - .style(ButtonStyle::Secondary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( Button::new("button-icon-3", cx) .label("Maximize") .icon(Icon::new(IconName::Maximize)) - .style(ButtonStyle::Secondary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( @@ -111,6 +211,32 @@ impl Render for ButtonStory { .child(IconName::ChevronDown) .child(IconName::Eye), ) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), + ) + .child( + Button::new("button-icon-5-ghost", cx) + .style(ButtonStyle::Ghost) + .icon(IconName::Check) + .label("Confirm") + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), + ) + .child( + Button::new("button-icon-6-link", cx) + .style(ButtonStyle::Link) + .icon(IconName::Check) + .label("Link") + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ), ), @@ -126,6 +252,10 @@ impl Render for ButtonStory { .style(ButtonStyle::Primary) .size(Size::Small) .loading(true) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( @@ -133,6 +263,10 @@ impl Render for ButtonStory { .label("Secondary Button") .style(ButtonStyle::Secondary) .size(Size::Small) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( @@ -140,6 +274,43 @@ impl Render for ButtonStory { .label("Danger Button") .style(ButtonStyle::Danger) .size(Size::Small) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), + ) + .child( + Button::new("button-8-outline", cx) + .label("Outline Button") + .style(ButtonStyle::Outline) + .size(Size::Small) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), + ) + .child( + Button::new("button-8-ghost", cx) + .label("Ghost Button") + .style(ButtonStyle::Ghost) + .size(Size::Small) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), + ) + .child( + Button::new("button-8-link", cx) + .label("Link Button") + .style(ButtonStyle::Link) + .size(Size::Small) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ), ) @@ -150,6 +321,10 @@ impl Render for ButtonStory { .label("Primary Button") .style(ButtonStyle::Primary) .size(Size::XSmall) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( @@ -158,6 +333,10 @@ impl Render for ButtonStory { .style(ButtonStyle::Secondary) .size(Size::XSmall) .loading(true) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), ) .child( @@ -165,92 +344,103 @@ impl Render for ButtonStory { .label("Danger Button") .style(ButtonStyle::Danger) .size(Size::XSmall) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) .on_click(Self::on_click), - ), - ), - ) - .child( - h_flex() - .gap_6() - .child( - section("Disabled Button", cx) - .child( - Button::new("button-disabled1", cx) - .label("Disabled Button") - .style(ButtonStyle::Primary) - .on_click(Self::on_click) - .disabled(true), ) .child( - Button::new("button-disabled1", cx) - .label("Disabled Button") - .style(ButtonStyle::Secondary) - .on_click(Self::on_click) - .disabled(true), + Button::new("button-xs-3-ghost", cx) + .label("Ghost Button") + .style(ButtonStyle::Ghost) + .size(Size::XSmall) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), ) .child( - Button::new("button-disabled1", cx) - .label("Disabled Button") - .style(ButtonStyle::Danger) - .on_click(Self::on_click) - .disabled(true) - .loading(true), - ), - ) - .child( - section("Selected Style", cx) - .child( - Button::new("button-selected-1", cx) - .label("Selected Button") - .style(ButtonStyle::Primary) - .selected(true), + Button::new("button-xs-3-outline", cx) + .label("Outline Button") + .style(ButtonStyle::Outline) + .size(Size::XSmall) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), ) .child( - Button::new("button-selected-2", cx) - .label("Selected Button") - .style(ButtonStyle::Secondary) - .selected(true), - ) - .child( - Button::new("button-selected-3", cx) - .label("Selected Button") - .style(ButtonStyle::Danger) - .selected(true), + Button::new("button-xs-3-link", cx) + .label("Link Button") + .style(ButtonStyle::Link) + .size(Size::XSmall) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact) + .on_click(Self::on_click), ), ), ) .child( section("Icon Button", cx) .child( - Button::new("icon-button-0", cx) + Button::new("icon-button-primary", cx) .icon(IconName::Search) - .style(ButtonStyle::Primary), + .style(ButtonStyle::Primary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( - Button::new("icon-button-1", cx) + Button::new("icon-button-secondary", cx) .icon(IconName::Info) - .loading(true), + .loading(true) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( - Button::new("icon-button-2", cx) + Button::new("icon-button-danger", cx) .icon(IconName::Close) - .style(ButtonStyle::Danger), + .style(ButtonStyle::Danger) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( - Button::new("icon-button-3", cx) + Button::new("icon-button-small-primary", cx) .icon(IconName::Search) .size(Size::Small) - .style(ButtonStyle::Primary), + .style(ButtonStyle::Primary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( - Button::new("icon-button-0-outline", cx) + Button::new("icon-button-outline", cx) .icon(IconName::Search) - .style(ButtonStyle::Outline), + .style(ButtonStyle::Outline) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( - Button::new("icon-button-1", cx) - .icon(IconName::Info) - .style(ButtonStyle::Ghost), + Button::new("icon-button-ghost", cx) + .icon(IconName::ArrowLeft) + .style(ButtonStyle::Ghost) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ), ) .child( @@ -258,36 +448,60 @@ impl Render for ButtonStory { .child( Button::new("icon-button-4", cx) .icon(IconName::Info) - .size(Size::Small), + .size(Size::Small) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( Button::new("icon-button-5", cx) .icon(IconName::Close) .size(Size::Small) - .style(ButtonStyle::Danger), + .style(ButtonStyle::Danger) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( Button::new("icon-button-6", cx) .icon(IconName::Search) .size(Size::XSmall) - .style(ButtonStyle::Primary), + .style(ButtonStyle::Primary) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( Button::new("icon-button-7", cx) .icon(IconName::Info) - .size(Size::XSmall), + .size(Size::XSmall) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( Button::new("icon-button-8", cx) .icon(IconName::Close) .size(Size::XSmall) - .style(ButtonStyle::Danger), + .style(ButtonStyle::Danger) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ) .child( Button::new("icon-button-9", cx) .icon(IconName::Heart) .size(px(24.)) - .style(ButtonStyle::Ghost), + .style(ButtonStyle::Ghost) + .disabled(disabled) + .selected(selected) + .loading(loading) + .compact(compact), ), ) } diff --git a/crates/story/src/popover_story.rs b/crates/story/src/popover_story.rs index 46ab3c73..aac40fa7 100644 --- a/crates/story/src/popover_story.rs +++ b/crates/story/src/popover_story.rs @@ -12,7 +12,7 @@ use ui::{ popup_menu::PopupMenu, prelude::FluentBuilder, switch::Switch, - v_flex, Clickable, IconName, Size, + v_flex, Clickable as _, IconName, Size, }; actions!( @@ -49,7 +49,8 @@ impl Render for Form { .child("This is a form container.") .child(self.input1.clone()) .child( - Button::primary("submit", "Submit", cx) + Button::new("submit", cx) + .primary() .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), ) } @@ -143,7 +144,7 @@ impl Render for PopoverStory { .child( Button::new("info1", cx) .label("Yes") - .width(px(80.)) + .w(px(80.)) .size(Size::Small), ) .into_any() @@ -166,7 +167,7 @@ impl Render for PopoverStory { .child( Button::new("info1", cx) .label("Yes") - .width(px(80.)) + .w(px(80.)) .size(Size::Small), ) .into_any() @@ -180,7 +181,7 @@ impl Render for PopoverStory { .child( Popover::new("popup-menu") .when(window_mode, |this| this.window_mode()) - .trigger(Button::new("popup-menu-1", cx).icon(IconName::Info)) + .trigger(Button::new("popup-menu-1", cx).icon(IconName::Ellipsis)) .content(move |cx| { let focus_handle = focus_handle.clone(); PopupMenu::build(cx, |mut this, _cx| { @@ -213,9 +214,7 @@ impl Render for PopoverStory { .when(self.window_mode, |this| this.window_mode()) .anchor(AnchorCorner::BottomLeft) .trigger( - Button::new("pop", cx) - .label("Popup with Form") - .width(px(300.)), + Button::new("pop", cx).label("Popup with Form").w(px(300.)), ) .content(move |_| form.clone()), ) @@ -227,7 +226,7 @@ impl Render for PopoverStory { .trigger( Button::new("pop", cx) .label("Mouse Right Click") - .width(px(300.)), + .w(px(300.)), ) .content(|cx| { PopoverContent::new(cx, |cx| { @@ -238,7 +237,7 @@ impl Render for PopoverStory { .child( Button::new("info1", cx) .label("Yes") - .width(px(80.)) + .w(px(80.)) .size(Size::Small), ) .into_any() diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 5de165ff..7b75347f 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -5,9 +5,9 @@ use crate::{ Clickable, Disableable, Icon, Selectable, Size, }; use gpui::{ - div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, DefiniteLength, Div, ElementId, - FocusHandle, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, - RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext, + div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, Div, ElementId, FocusHandle, + Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, + SharedString, StatefulInteractiveElement as _, Styled, WindowContext, }; pub enum ButtonRounded { @@ -24,7 +24,7 @@ impl From for ButtonRounded { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct ButtonCustomStyle { color: Hsla, foreground: Hsla, @@ -70,16 +70,23 @@ impl ButtonCustomStyle { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum ButtonStyle { Primary, Secondary, Danger, Outline, Ghost, + Link, Custom(ButtonCustomStyle), } +impl ButtonStyle { + fn is_link(&self) -> bool { + matches!(self, Self::Link) + } +} + #[derive(IntoElement)] pub struct Button { pub base: Div, @@ -90,11 +97,10 @@ pub struct Button { children: Vec, disabled: bool, selected: bool, - width: Option, - height: Option, style: ButtonStyle, rounded: ButtonRounded, size: Size, + compact: bool, tooltip: Option, on_click: Option>, loading: bool, @@ -111,61 +117,65 @@ impl Button { disabled: false, selected: false, style: ButtonStyle::Secondary, - width: None, - height: None, rounded: ButtonRounded::Medium, size: Size::Medium, tooltip: None, on_click: None, loading: false, + compact: false, children: Vec::new(), } } - pub fn primary( - id: impl Into, - label: impl Into, - cx: &mut WindowContext, - ) -> Self { - Self::new(id, cx).label(label).style(ButtonStyle::Primary) - } - - pub fn danger( - id: impl Into, - label: impl Into, - cx: &mut WindowContext, - ) -> Self { - Self::new(id, cx).label(label).style(ButtonStyle::Danger) - } - - pub fn small( - id: impl Into, - label: impl Into, - cx: &mut WindowContext, - ) -> Self { - Self::new(id, cx).label(label).size(Size::Small) - } - - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); + /// With the primary style for the Button. + pub fn primary(mut self) -> Self { + self.style = ButtonStyle::Primary; self } - pub fn height(mut self, height: impl Into) -> Self { - self.height = Some(height.into()); + /// With the secondary style for the Button. + pub fn danger(mut self) -> Self { + self.style = ButtonStyle::Danger; self } + /// With the ghost style for the Button. + pub fn ghost(mut self) -> Self { + self.style = ButtonStyle::Ghost; + self + } + + /// With the outline style for the Button. + pub fn outline(mut self) -> Self { + self.style = ButtonStyle::Outline; + self + } + + /// With the link style for the Button. + pub fn link(mut self) -> Self { + self.style = ButtonStyle::Link; + self + } + + /// With the custom style for the Button. + pub fn custom(mut self, custom: ButtonCustomStyle) -> Self { + self.style = ButtonStyle::Custom(custom); + self + } + + /// Set the border radius of the Button. pub fn rounded(mut self, rounded: impl Into) -> Self { self.rounded = rounded.into(); self } + /// Set the ui::Size of the Button. pub fn size(mut self, size: impl Into) -> Self { self.size = size.into(); self } + /// Set label to the Button, if no label is set, the button will be in Icon Button mode. pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self @@ -177,20 +187,29 @@ impl Button { self } + /// Set the tooltip of the button. pub fn tooltip(mut self, tooltip: impl Into) -> Self { self.tooltip = Some(tooltip.into()); self } + /// Set the ButtonStyle pub fn style(mut self, style: ButtonStyle) -> Self { self.style = style; self } + /// Set true to show the loading indicator. pub fn loading(mut self, loading: bool) -> Self { self.loading = loading; self } + + /// Set the button to compact mode, then padding will be reduced. + pub fn compact(mut self, compact: bool) -> Self { + self.compact = compact; + self + } } impl Disableable for Button { @@ -243,9 +262,7 @@ impl RenderOnce for Button { .items_center() .justify_center() .cursor_pointer() - .when_some(self.width, |this, width| this.w(width)) - .when_some(self.height, |this, height| this.h(height)) - .map(|this| { + .when(!style.is_link(), |this| { if self.label.is_none() && self.children.is_empty() { // Icon Button match self.size { @@ -258,9 +275,17 @@ impl RenderOnce for Button { // Normal Button match self.size { Size::Size(size) => this.p(size * 0.2), - 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(), + Size::XSmall => this.h_5().p_1(), + Size::Small => this + .px_3() + .py_2() + .h_6() + .when(self.compact, |this| this.p_2()), + _ => this + .px_4() + .py_2() + .h_8() + .when(self.compact, |this| this.p_2()), } } }) @@ -271,22 +296,29 @@ impl RenderOnce for Button { ButtonRounded::Size(px) => this.rounded(px), ButtonRounded::None => this.rounded_none(), }) + .text_color(normal_style.fg) .when(self.selected, |this| { let selected_style = style.selected(cx); this.bg(selected_style.bg) .border_color(selected_style.border) + .text_color(selected_style.fg) }) .when(!self.disabled && !self.selected, |this| { - this.hover(|this| { - let hover_style = style.hovered(cx); - this.bg(hover_style.bg).border_color(hover_style.border) - }) - .active(|this| { - let active_style = style.active(cx); - this.bg(active_style.bg).border_color(active_style.border) - }) - .border_color(normal_style.border) - .bg(normal_style.bg) + this.border_color(normal_style.border) + .bg(normal_style.bg) + .when(normal_style.underline, |this| this.text_decoration_1()) + .hover(|this| { + let hover_style = style.hovered(cx); + this.bg(hover_style.bg) + .border_color(hover_style.border) + .text_color(crate::red_400()) + }) + .active(|this| { + let active_style = style.active(cx); + this.bg(active_style.bg) + .border_color(active_style.border) + .text_color(active_style.fg) + }) }) .when(focused, |this| this.border_color(cx.theme().ring)) .when_some( @@ -305,36 +337,29 @@ impl RenderOnce for Button { let disabled_style = style.disabled(cx); this.cursor_not_allowed() .bg(disabled_style.bg) + .text_color(disabled_style.fg) .border_color(disabled_style.border) }) .border_1() .child({ - let text_color = if self.disabled { - normal_style.fg.opacity(0.6) - } else { - normal_style.fg - }; - h_flex() + .id("label") .items_center() .justify_center() .gap_2() - .text_color(text_color) - .when(!self.loading, |this| { - this.when_some(self.icon, |this, icon| { - this.child(div().text_color(text_color).child(icon.size(icon_size))) - }) - }) - .when(self.loading, |this| { - this.child(Indicator::new().size(self.size).color(text_color)) - }) - .when_some(self.label, |this, label| this.child(label)) - .children(self.children) .map(|this| match self.size { Size::XSmall => this.text_xs(), Size::Small => this.text_sm(), _ => this.text_base(), }) + .when(!self.loading, |this| { + this.when_some(self.icon, |this, icon| this.child(icon.size(icon_size))) + }) + .when(self.loading, |this| { + this.child(Indicator::new().size(self.size)) + }) + .when_some(self.label, |this, label| this.child(label)) + .children(self.children) }) } } @@ -343,6 +368,7 @@ struct ButtonStyles { bg: Hsla, border: Hsla, fg: Hsla, + underline: bool, } impl ButtonStyle { @@ -353,6 +379,7 @@ impl ButtonStyle { ButtonStyle::Danger => cx.theme().destructive, ButtonStyle::Outline => cx.theme().transparent, ButtonStyle::Ghost => cx.theme().transparent, + ButtonStyle::Link => cx.theme().transparent, ButtonStyle::Custom(colors) => colors.color, } } @@ -364,6 +391,7 @@ impl ButtonStyle { ButtonStyle::Danger => cx.theme().destructive_foreground, ButtonStyle::Outline => cx.theme().secondary_foreground, ButtonStyle::Ghost => cx.theme().secondary_foreground, + ButtonStyle::Link => cx.theme().link, ButtonStyle::Custom(colors) => colors.foreground, } } @@ -375,16 +403,30 @@ impl ButtonStyle { ButtonStyle::Danger => cx.theme().destructive, ButtonStyle::Outline => cx.theme().border, ButtonStyle::Ghost => cx.theme().transparent, + ButtonStyle::Link => cx.theme().transparent, ButtonStyle::Custom(colors) => colors.border, } } + fn underline(&self, _: &WindowContext) -> bool { + match self { + ButtonStyle::Link => true, + _ => false, + } + } + fn normal(&self, cx: &WindowContext) -> ButtonStyles { let bg = self.bg_color(cx); let border = self.border_color(cx); let fg = self.text_color(cx); + let underline = self.underline(cx); - ButtonStyles { bg, border, fg } + ButtonStyles { + bg, + border, + fg, + underline, + } } fn hovered(&self, cx: &WindowContext) -> ButtonStyles { @@ -394,12 +436,22 @@ impl ButtonStyle { ButtonStyle::Danger => cx.theme().destructive_hover, ButtonStyle::Outline => cx.theme().secondary_hover, ButtonStyle::Ghost => cx.theme().secondary, + ButtonStyle::Link => cx.theme().transparent, ButtonStyle::Custom(colors) => colors.hover, }; let border = self.border_color(cx); - let fg = self.text_color(cx); + let fg = match self { + ButtonStyle::Link => cx.theme().link_hover, + _ => self.text_color(cx), + }; + let underline = self.underline(cx); - ButtonStyles { bg, border, fg } + ButtonStyles { + bg, + border, + fg, + underline, + } } fn active(&self, cx: &WindowContext) -> ButtonStyles { @@ -409,12 +461,22 @@ impl ButtonStyle { ButtonStyle::Danger => cx.theme().destructive_active, ButtonStyle::Outline => cx.theme().secondary_active, ButtonStyle::Ghost => cx.theme().secondary_active, + ButtonStyle::Link => cx.theme().transparent, ButtonStyle::Custom(colors) => colors.active, }; let border = self.border_color(cx); - let fg = self.text_color(cx); + let fg = match self { + ButtonStyle::Link => cx.theme().link_active, + _ => self.text_color(cx), + }; + let underline = self.underline(cx); - ButtonStyles { bg, border, fg } + ButtonStyles { + bg, + border, + fg, + underline, + } } fn selected(&self, cx: &WindowContext) -> ButtonStyles { @@ -424,19 +486,42 @@ impl ButtonStyle { ButtonStyle::Danger => cx.theme().destructive_active, ButtonStyle::Outline => cx.theme().secondary_active, ButtonStyle::Ghost => cx.theme().secondary_active, + ButtonStyle::Link => cx.theme().transparent, ButtonStyle::Custom(colors) => colors.active, }; let border = self.border_color(cx); - let fg = self.text_color(cx); + let fg = match self { + ButtonStyle::Link => cx.theme().link_active, + _ => self.text_color(cx), + }; + let underline = self.underline(cx); - ButtonStyles { bg, border, fg } + ButtonStyles { + bg, + border, + fg, + underline, + } } fn disabled(&self, cx: &WindowContext) -> ButtonStyles { - let bg = self.bg_color(cx).grayscale().opacity(0.9); - let border = self.border_color(cx).grayscale().opacity(0.9); - let fg = self.text_color(cx).grayscale(); + let bg = match self { + ButtonStyle::Link => cx.theme().transparent, + _ => cx.theme().secondary.darken(0.2).grayscale(), + }; + let fg = match self { + ButtonStyle::Link => cx.theme().link.grayscale(), + _ => cx.theme().secondary_foreground.darken(0.2).grayscale(), + }; - ButtonStyles { bg, border, fg } + let border = bg; + let underline = self.underline(cx); + + ButtonStyles { + bg, + border, + fg, + underline, + } } } diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 3dd0e9b1..58be8d68 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -39,8 +39,8 @@ impl Checkbox { self } - pub fn checked(mut self, checked: Selection) -> Self { - self.checked = checked; + pub fn checked(mut self, checked: impl Into) -> Self { + self.checked = checked.into(); self } diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index c5f7195a..645b6699 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -44,6 +44,7 @@ pub enum IconName { Sun, ThumbsDown, ThumbsUp, + Menu, } impl IconName { @@ -87,6 +88,7 @@ impl IconName { IconName::Sun => "icons/sun.svg", IconName::ThumbsDown => "icons/thumbs-down.svg", IconName::ThumbsUp => "icons/thumbs-up.svg", + IconName::Menu => "icons/menu.svg", } .into() } diff --git a/crates/ui/src/indicator.rs b/crates/ui/src/indicator.rs index f6b95492..7b3cb3cb 100644 --- a/crates/ui/src/indicator.rs +++ b/crates/ui/src/indicator.rs @@ -1,9 +1,9 @@ use std::time::Duration; -use crate::{theme::ActiveTheme, Icon, IconName, Size}; +use crate::{Icon, IconName, Size}; use gpui::{ - div, ease_in_out, percentage, Animation, AnimationExt as _, Hsla, IntoElement, ParentElement, - RenderOnce, Styled as _, Transformation, + div, ease_in_out, percentage, prelude::FluentBuilder as _, Animation, AnimationExt as _, Hsla, + IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, }; #[derive(IntoElement)] @@ -41,13 +41,12 @@ impl Indicator { } impl RenderOnce for Indicator { - fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { - let color = self.color.unwrap_or_else(|| cx.theme().indicator); + fn render(self, _: &mut gpui::WindowContext) -> impl IntoElement { div() .child( Icon::new(self.icon.clone()) .size(self.size) - .text_color(color) + .when_some(self.color, |this, color| this.text_color(color)) .with_animation( "circle", Animation::new(self.speed).repeat().with_easing(ease_in_out), diff --git a/crates/ui/src/selectable.rs b/crates/ui/src/selectable.rs index 2ed168d7..841363c0 100644 --- a/crates/ui/src/selectable.rs +++ b/crates/ui/src/selectable.rs @@ -20,6 +20,16 @@ pub enum Selection { Selected, } +impl From for Selection { + fn from(selected: bool) -> Self { + if selected { + Self::Selected + } else { + Self::Unselected + } + } +} + impl Display for Selection { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index aa4234e7..4206ca7a 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -293,7 +293,6 @@ pub struct Theme { pub tab_active: Hsla, pub tab_foreground: Hsla, pub tab_active_foreground: Hsla, - pub indicator: Hsla, pub progress_bar: Hsla, pub slider_bar: Hsla, pub slider_thumb: Hsla, @@ -308,6 +307,8 @@ pub struct Theme { pub table_active: Hsla, pub table_hover: Hsla, pub link: Hsla, + pub link_hover: Hsla, + pub link_active: Hsla, } impl Global for Theme {} @@ -369,7 +370,6 @@ 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, slider_bar: colors.primary, slider_thumb: colors.background, @@ -384,6 +384,8 @@ impl From for Theme { table_active: colors.list_active, table_hover: colors.list_active.opacity(0.6), link: colors.link, + link_hover: colors.link.lighten(0.2), + link_active: colors.link.darken(0.2), } } }