button: Improve Button to support combine outline with other variants. (#714)

<img width="1385" alt="image"
src="https://github.com/user-attachments/assets/240df695-bfd5-4680-89a2-2640db206b24"
/>
This commit is contained in:
Jason Lee 2025-03-12 11:57:25 +08:00 committed by GitHub
parent 89170eea1d
commit 6da7de5299
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 242 additions and 107 deletions

View file

@ -4,7 +4,7 @@ use gpui::{
}; };
use gpui_component::{ use gpui_component::{
accordion::Accordion, accordion::Accordion,
button::{Button, ButtonGroup, ButtonVariants as _}, button::{Button, ButtonGroup},
checkbox::Checkbox, checkbox::Checkbox,
h_flex, h_flex,
switch::Switch, switch::Switch,

View file

@ -4,10 +4,9 @@ use gpui::{
}; };
use gpui_component::{ use gpui_component::{
black,
button::{Button, ButtonCustomVariant, ButtonGroup, ButtonVariants as _, DropdownButton}, button::{Button, ButtonCustomVariant, ButtonGroup, ButtonVariants as _, DropdownButton},
checkbox::Checkbox, checkbox::Checkbox,
green_500, green_600, green_800, green_900, green_950, h_flex, v_flex, white, ActiveTheme, green_600, green_700, green_800, green_950, h_flex, v_flex, white, ActiveTheme,
Disableable as _, Icon, IconName, Selectable as _, Sizable as _, Theme, Disableable as _, Icon, IconName, Selectable as _, Sizable as _, Theme,
}; };
@ -73,6 +72,33 @@ impl Render for ButtonStory {
let compact = self.compact; let compact = self.compact;
let toggle_multiple = self.toggle_multiple; let toggle_multiple = self.toggle_multiple;
let custom_variant = ButtonCustomVariant::new(cx)
.color(if cx.theme().mode.is_dark() {
green_800()
} else {
green_600()
})
.foreground(if cx.theme().mode.is_dark() {
white()
} else {
white()
})
.border(if cx.theme().mode.is_dark() {
green_800()
} else {
green_700()
})
.hover(if cx.theme().mode.is_dark() {
green_800()
} else {
green_600()
})
.active(if cx.theme().mode.is_dark() {
green_950()
} else {
green_700()
});
v_flex() v_flex()
.on_action(cx.listener(|this, _: &Disabled, _, _| this.disabled = !this.disabled)) .on_action(cx.listener(|this, _: &Disabled, _, _| this.disabled = !this.disabled))
.on_action(cx.listener(|this, _: &Loading, _, _| this.loading = !this.loading)) .on_action(cx.listener(|this, _: &Loading, _, _| this.loading = !this.loading))
@ -165,16 +191,6 @@ impl Render for ButtonStory {
.when(compact, |this| this.compact()) .when(compact, |this| this.compact())
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child(
Button::new("button-5")
.outline()
.label("Outline Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
)
.child( .child(
Button::new("button-5-ghost") Button::new("button-5-ghost")
.ghost() .ghost()
@ -207,34 +223,7 @@ impl Render for ButtonStory {
) )
.child( .child(
Button::new("button-6-custom") Button::new("button-6-custom")
.custom( .custom(custom_variant)
ButtonCustomVariant::new(cx)
.color(if cx.theme().mode.is_dark() {
green_900()
} else {
green_500()
})
.foreground(if cx.theme().mode.is_dark() {
white()
} else {
black()
})
.border(if cx.theme().mode.is_dark() {
green_800()
} else {
green_600()
})
.hover(if cx.theme().mode.is_dark() {
green_800()
} else {
green_500()
})
.active(if cx.theme().mode.is_dark() {
green_950()
} else {
green_600()
}),
)
.label("Custom Button") .label("Custom Button")
.disabled(disabled) .disabled(disabled)
.selected(selected) .selected(selected)
@ -328,6 +317,87 @@ impl Render for ButtonStory {
), ),
), ),
) )
.child(
h_flex().gap_6().child(
section("Outline Button", cx)
.child(
Button::new("button-outline-1")
.primary()
.outline()
.label("Primary Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
)
.child(
Button::new("button-outline-2")
.outline()
.label("Secondary Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
)
.child(
Button::new("button-outline-4")
.danger()
.outline()
.label("Danger Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
)
.child(
Button::new("button-outline-6-custom")
.outline()
.custom(custom_variant)
.label("Custom Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
)
.child(
Button::new("button-outline-5-ghost")
.ghost()
.outline()
.label("Ghost Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
)
.child(
Button::new("button-outline-5-link")
.link()
.outline()
.label("Link Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
)
.child(
Button::new("button-outline-5-text")
.text()
.outline()
.label("Text Button")
.disabled(disabled)
.selected(selected)
.loading(loading)
.when(compact, |this| this.compact())
.on_click(Self::on_click),
),
),
)
.child( .child(
h_flex() h_flex()
.gap_6() .gap_6()

View file

@ -6,7 +6,7 @@ use gpui::{
ParentElement, Pixels, Render, ScrollHandle, SharedString, Size, Styled, Window, ParentElement, Pixels, Render, ScrollHandle, SharedString, Size, Styled, Window,
}; };
use gpui_component::{ use gpui_component::{
button::{Button, ButtonGroup, ButtonVariants as _}, button::{Button, ButtonGroup},
divider::Divider, divider::Divider,
gray_100, gray_800, h_flex, gray_100, gray_800, h_flex,
label::Label, label::Label,

View file

@ -46,11 +46,6 @@ pub trait ButtonVariants: Sized {
self.with_variant(ButtonVariant::Danger) self.with_variant(ButtonVariant::Danger)
} }
/// With the outline style for the Button.
fn outline(self) -> Self {
self.with_variant(ButtonVariant::Outline)
}
/// With the ghost style for the Button. /// With the ghost style for the Button.
fn ghost(self) -> Self { fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost) self.with_variant(ButtonVariant::Ghost)
@ -121,7 +116,6 @@ pub enum ButtonVariant {
Primary, Primary,
Secondary, Secondary,
Danger, Danger,
Outline,
Ghost, Ghost,
Link, Link,
Text, Text,
@ -160,6 +154,7 @@ pub struct Button {
pub(crate) selected: bool, pub(crate) selected: bool,
variant: ButtonVariant, variant: ButtonVariant,
rounded: ButtonRounded, rounded: ButtonRounded,
outline: bool,
border_corners: Corners<bool>, border_corners: Corners<bool>,
border_edges: Edges<bool>, border_edges: Edges<bool>,
size: Size, size: Size,
@ -196,11 +191,18 @@ impl Button {
stop_propagation: true, stop_propagation: true,
loading: false, loading: false,
compact: false, compact: false,
outline: false,
children: Vec::new(), children: Vec::new(),
loading_icon: None, loading_icon: None,
} }
} }
/// Set the outline style of the Button.
pub fn outline(mut self) -> Self {
self.outline = true;
self
}
/// Set the border radius of the Button. /// Set the border radius of the Button.
pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self { pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self {
self.rounded = rounded.into(); self.rounded = rounded.into();
@ -321,7 +323,7 @@ impl InteractiveElement for Button {
impl RenderOnce for Button { impl RenderOnce for Button {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let style: ButtonVariant = self.variant; let style: ButtonVariant = self.variant;
let normal_style = style.normal(cx); let normal_style = style.normal(self.outline, cx);
let icon_size = match self.size { let icon_size = match self.size {
Size::Size(v) => Size::Size(v * 0.75), Size::Size(v) => Size::Size(v * 0.75),
_ => self.size, _ => self.size,
@ -382,7 +384,7 @@ impl RenderOnce for Button {
.when(self.border_edges.bottom, |this| this.border_b_1()) .when(self.border_edges.bottom, |this| this.border_b_1())
.text_color(normal_style.fg) .text_color(normal_style.fg)
.when(self.selected, |this| { .when(self.selected, |this| {
let selected_style = style.selected(cx); let selected_style = style.selected(self.outline, cx);
this.bg(selected_style.bg) this.bg(selected_style.bg)
.border_color(selected_style.border) .border_color(selected_style.border)
.text_color(selected_style.fg) .text_color(selected_style.fg)
@ -392,13 +394,13 @@ impl RenderOnce for Button {
.bg(normal_style.bg) .bg(normal_style.bg)
.when(normal_style.underline, |this| this.text_decoration_1()) .when(normal_style.underline, |this| this.text_decoration_1())
.hover(|this| { .hover(|this| {
let hover_style = style.hovered(cx); let hover_style = style.hovered(self.outline, cx);
this.bg(hover_style.bg) this.bg(hover_style.bg)
.border_color(hover_style.border) .border_color(hover_style.border)
.text_color(crate::red_400()) .text_color(crate::red_400())
}) })
.active(|this| { .active(|this| {
let active_style = style.active(cx); let active_style = style.active(self.outline, cx);
this.bg(active_style.bg) this.bg(active_style.bg)
.border_color(active_style.border) .border_color(active_style.border)
.text_color(active_style.fg) .text_color(active_style.fg)
@ -420,7 +422,7 @@ impl RenderOnce for Button {
}, },
) )
.when(self.disabled, |this| { .when(self.disabled, |this| {
let disabled_style = style.disabled(cx); let disabled_style = style.disabled(self.outline, cx);
this.cursor_not_allowed() this.cursor_not_allowed()
.bg(disabled_style.bg) .bg(disabled_style.bg)
.text_color(disabled_style.fg) .text_color(disabled_style.fg)
@ -471,38 +473,56 @@ struct ButtonVariantStyle {
} }
impl ButtonVariant { impl ButtonVariant {
fn bg_color(&self, cx: &mut App) -> Hsla { fn bg_color(&self, outline: bool, cx: &mut App) -> Hsla {
if outline {
return cx.theme().background;
}
match self { match self {
ButtonVariant::Primary => cx.theme().primary, ButtonVariant::Primary => cx.theme().primary,
ButtonVariant::Secondary => cx.theme().secondary, ButtonVariant::Secondary => cx.theme().secondary,
ButtonVariant::Danger => cx.theme().danger, ButtonVariant::Danger => cx.theme().danger,
ButtonVariant::Outline ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
| ButtonVariant::Ghost cx.theme().transparent
| ButtonVariant::Link }
| ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Custom(colors) => colors.color, ButtonVariant::Custom(colors) => colors.color,
} }
} }
fn text_color(&self, cx: &mut App) -> Hsla { fn text_color(&self, outline: bool, cx: &mut App) -> Hsla {
match self { match self {
ButtonVariant::Primary => cx.theme().primary_foreground, ButtonVariant::Primary => {
ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => { if outline {
cx.theme().secondary_foreground cx.theme().primary
} else {
cx.theme().primary_foreground
}
}
ButtonVariant::Secondary | ButtonVariant::Ghost => cx.theme().secondary_foreground,
ButtonVariant::Danger => {
if outline {
cx.theme().danger
} else {
cx.theme().danger_foreground
}
} }
ButtonVariant::Danger => cx.theme().danger_foreground,
ButtonVariant::Link => cx.theme().link, ButtonVariant::Link => cx.theme().link,
ButtonVariant::Text => cx.theme().foreground, ButtonVariant::Text => cx.theme().foreground,
ButtonVariant::Custom(colors) => colors.foreground, ButtonVariant::Custom(colors) => {
if outline {
colors.color
} else {
colors.foreground
}
}
} }
} }
fn border_color(&self, cx: &mut App) -> Hsla { fn border_color(&self, _outline: bool, cx: &mut App) -> Hsla {
match self { match self {
ButtonVariant::Primary => cx.theme().primary, ButtonVariant::Primary => cx.theme().primary,
ButtonVariant::Secondary => cx.theme().border, ButtonVariant::Secondary => cx.theme().border,
ButtonVariant::Danger => cx.theme().danger, ButtonVariant::Danger => cx.theme().danger,
ButtonVariant::Outline => cx.theme().border,
ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => { ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
cx.theme().transparent cx.theme().transparent
} }
@ -517,7 +537,7 @@ impl ButtonVariant {
} }
} }
fn shadow(&self, _: &App) -> bool { fn shadow(&self, _outline: bool, _: &App) -> bool {
match self { match self {
ButtonVariant::Primary | ButtonVariant::Secondary | ButtonVariant::Danger => true, ButtonVariant::Primary | ButtonVariant::Secondary | ButtonVariant::Danger => true,
ButtonVariant::Custom(c) => c.shadow, ButtonVariant::Custom(c) => c.shadow,
@ -525,12 +545,12 @@ impl ButtonVariant {
} }
} }
fn normal(&self, cx: &mut App) -> ButtonVariantStyle { fn normal(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
let bg = self.bg_color(cx); let bg = self.bg_color(outline, cx);
let border = self.border_color(cx); let border = self.border_color(outline, cx);
let fg = self.text_color(cx); let fg = self.text_color(outline, cx);
let underline = self.underline(cx); let underline = self.underline(cx);
let shadow = self.shadow(cx); let shadow = self.shadow(outline, cx);
ButtonVariantStyle { ButtonVariantStyle {
bg, bg,
@ -541,11 +561,23 @@ impl ButtonVariant {
} }
} }
fn hovered(&self, cx: &mut App) -> ButtonVariantStyle { fn hovered(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
let bg = match self { let bg = match self {
ButtonVariant::Primary => cx.theme().primary_hover, ButtonVariant::Primary => {
ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_hover, if outline {
ButtonVariant::Danger => cx.theme().danger_hover, cx.theme().secondary_hover
} else {
cx.theme().primary_hover
}
}
ButtonVariant::Secondary => cx.theme().secondary_hover,
ButtonVariant::Danger => {
if outline {
cx.theme().secondary_hover
} else {
cx.theme().danger_hover
}
}
ButtonVariant::Ghost => { ButtonVariant::Ghost => {
if cx.theme().mode.is_dark() { if cx.theme().mode.is_dark() {
cx.theme().secondary.lighten(0.1).opacity(0.8) cx.theme().secondary.lighten(0.1).opacity(0.8)
@ -555,15 +587,23 @@ impl ButtonVariant {
} }
ButtonVariant::Link => cx.theme().transparent, ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Custom(colors) => colors.hover, ButtonVariant::Custom(colors) => {
if outline {
cx.theme().secondary_hover
} else {
colors.hover
}
}
}; };
let border = self.border_color(cx);
let border = self.border_color(outline, cx);
let fg = match self { let fg = match self {
ButtonVariant::Link => cx.theme().link_hover, ButtonVariant::Link => cx.theme().link_hover,
_ => self.text_color(cx), _ => self.text_color(outline, cx),
}; };
let underline = self.underline(cx); let underline = self.underline(cx);
let shadow = self.shadow(cx); let shadow = self.shadow(outline, cx);
ButtonVariantStyle { ButtonVariantStyle {
bg, bg,
@ -574,10 +614,16 @@ impl ButtonVariant {
} }
} }
fn active(&self, cx: &mut App) -> ButtonVariantStyle { fn active(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
let bg = match self { let bg = match self {
ButtonVariant::Primary => cx.theme().primary_active, ButtonVariant::Primary => {
ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_active, if outline {
cx.theme().primary_active.opacity(0.1)
} else {
cx.theme().primary_active
}
}
ButtonVariant::Secondary => cx.theme().secondary_active,
ButtonVariant::Ghost => { ButtonVariant::Ghost => {
if cx.theme().mode.is_dark() { if cx.theme().mode.is_dark() {
cx.theme().secondary.lighten(0.2).opacity(0.8) cx.theme().secondary.lighten(0.2).opacity(0.8)
@ -585,19 +631,31 @@ impl ButtonVariant {
cx.theme().secondary.darken(0.2).opacity(0.8) cx.theme().secondary.darken(0.2).opacity(0.8)
} }
} }
ButtonVariant::Danger => cx.theme().danger_active, ButtonVariant::Danger => {
if outline {
cx.theme().danger_active.opacity(0.1)
} else {
cx.theme().danger_active
}
}
ButtonVariant::Link => cx.theme().transparent, ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Custom(colors) => colors.active, ButtonVariant::Custom(colors) => {
if outline {
colors.active.opacity(0.1)
} else {
colors.active
}
}
}; };
let border = self.border_color(cx); let border = self.border_color(outline, cx);
let fg = match self { let fg = match self {
ButtonVariant::Link => cx.theme().link_active, ButtonVariant::Link => cx.theme().link_active,
ButtonVariant::Text => cx.theme().foreground.opacity(0.7), ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
_ => self.text_color(cx), _ => self.text_color(outline, cx),
}; };
let underline = self.underline(cx); let underline = self.underline(cx);
let shadow = self.shadow(cx); let shadow = self.shadow(outline, cx);
ButtonVariantStyle { ButtonVariantStyle {
bg, bg,
@ -608,26 +666,24 @@ impl ButtonVariant {
} }
} }
fn selected(&self, cx: &mut App) -> ButtonVariantStyle { fn selected(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
let bg = match self { let bg = match self {
ButtonVariant::Primary => cx.theme().primary_active, ButtonVariant::Primary => cx.theme().primary_active,
ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => { ButtonVariant::Secondary | ButtonVariant::Ghost => cx.theme().secondary_active,
cx.theme().secondary_active
}
ButtonVariant::Danger => cx.theme().danger_active, ButtonVariant::Danger => cx.theme().danger_active,
ButtonVariant::Link => cx.theme().transparent, ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Custom(colors) => colors.active, ButtonVariant::Custom(colors) => colors.active,
}; };
let border = self.border_color(cx); let border = self.border_color(outline, cx);
let fg = match self { let fg = match self {
ButtonVariant::Link => cx.theme().link_active, ButtonVariant::Link => cx.theme().link_active,
ButtonVariant::Text => cx.theme().foreground.opacity(0.7), ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
_ => self.text_color(cx), _ => self.text_color(outline, cx),
}; };
let underline = self.underline(cx); let underline = self.underline(cx);
let shadow = self.shadow(cx); let shadow = self.shadow(outline, cx);
ButtonVariantStyle { ButtonVariantStyle {
bg, bg,
@ -638,12 +694,11 @@ impl ButtonVariant {
} }
} }
fn disabled(&self, cx: &mut App) -> ButtonVariantStyle { fn disabled(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
let bg = match self { let bg = match self {
ButtonVariant::Link ButtonVariant::Link | ButtonVariant::Ghost | ButtonVariant::Text => {
| ButtonVariant::Ghost cx.theme().transparent
| ButtonVariant::Outline }
| ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Primary => cx.theme().primary.opacity(0.15), ButtonVariant::Primary => cx.theme().primary.opacity(0.15),
ButtonVariant::Danger => cx.theme().danger.opacity(0.15), ButtonVariant::Danger => cx.theme().danger.opacity(0.15),
ButtonVariant::Secondary => cx.theme().secondary.opacity(1.5), ButtonVariant::Secondary => cx.theme().secondary.opacity(1.5),
@ -656,9 +711,10 @@ impl ButtonVariant {
_ => cx.theme().secondary_foreground.opacity(0.5).grayscale(), _ => cx.theme().secondary_foreground.opacity(0.5).grayscale(),
}; };
let border = match self { let (bg, border) = if outline {
ButtonVariant::Outline => cx.theme().border.opacity(0.5), (cx.theme().transparent, cx.theme().border.opacity(0.5))
_ => bg, } else {
(bg, bg)
}; };
let underline = self.underline(cx); let underline = self.underline(cx);

View file

@ -19,7 +19,8 @@ pub struct ButtonGroup {
pub(super) disabled: bool, pub(super) disabled: bool,
// The button props // The button props
pub(super) compact: Option<bool>, pub(super) compact: bool,
pub(super) outline: bool,
pub(super) variant: Option<ButtonVariant>, pub(super) variant: Option<ButtonVariant>,
pub(super) size: Option<Size>, pub(super) size: Option<Size>,
@ -42,7 +43,8 @@ impl ButtonGroup {
id: id.into(), id: id.into(),
variant: None, variant: None,
size: None, size: None,
compact: None, compact: false,
outline: false,
multiple: false, multiple: false,
disabled: false, disabled: false,
on_click: None, on_click: None,
@ -69,7 +71,13 @@ impl ButtonGroup {
/// With the compact mode for the ButtonGroup. /// With the compact mode for the ButtonGroup.
pub fn compact(mut self) -> Self { pub fn compact(mut self) -> Self {
self.compact = Some(true); self.compact = true;
self
}
/// With the outline mode for the ButtonGroup.
pub fn outline(mut self) -> Self {
self.outline = true;
self self
} }
@ -193,7 +201,8 @@ impl RenderOnce for ButtonGroup {
.stop_propagation(false) .stop_propagation(false)
.when_some(self.size, |this, size| this.with_size(size)) .when_some(self.size, |this, size| this.with_size(size))
.when_some(self.variant, |this, variant| this.with_variant(variant)) .when_some(self.variant, |this, variant| this.with_variant(variant))
.when_some(self.compact, |this, _| this.compact()) .when(self.compact, |this| this.compact())
.when(self.outline, |this| this.outline())
.on_click(move |_, _, _| { .on_click(move |_, _, _| {
state.set(Some(child_index)); state.set(Some(child_index));
}); });