Add new Button and IconButton components (#3448)

This PR adds new `Button` and `IconButton` components built on top of
our new button abstractions.

Both of these buttons are built from the common `ButtonLike` base, and
implement the `ButtonCommon` (name TBD) trait in order to provide a
common interface.

There are still some visual tweaks that we'll need to make to the new
buttons, but those should be straightforward to make after we land this.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2023-11-29 17:41:44 -05:00 committed by GitHub
parent df5de47a78
commit b357ae4dc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 324 additions and 682 deletions

View file

@ -1,12 +1,10 @@
mod avatar;
mod button;
mod button2;
mod checkbox;
mod context_menu;
mod disclosure;
mod divider;
mod icon;
mod icon_button;
mod keybinding;
mod label;
mod list;
@ -19,13 +17,11 @@ mod stories;
pub use avatar::*;
pub use button::*;
pub use button2::*;
pub use checkbox::*;
pub use context_menu::*;
pub use disclosure::*;
pub use divider::*;
pub use icon::*;
pub use icon_button::*;
pub use keybinding::*;
pub use label::*;
pub use list::*;

View file

@ -1,228 +0,0 @@
use gpui::{
ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext,
};
use std::rc::Rc;
use crate::prelude::*;
use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
/// Provides the flexibility to use either a standard
/// button or an icon button in a given context.
pub enum ButtonOrIconButton {
Button(Button),
IconButton(IconButton),
}
impl From<Button> for ButtonOrIconButton {
fn from(value: Button) -> Self {
Self::Button(value)
}
}
impl From<IconButton> for ButtonOrIconButton {
fn from(value: IconButton) -> Self {
Self::IconButton(value)
}
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum IconPosition {
#[default]
Left,
Right,
}
#[derive(Default, Copy, Clone, PartialEq)]
pub enum ButtonVariant {
#[default]
Ghost,
Filled,
}
impl ButtonVariant {
pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
match self {
ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
ButtonVariant::Filled => cx.theme().colors().element_background,
}
}
pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
match self {
ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
ButtonVariant::Filled => cx.theme().colors().element_hover,
}
}
pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
match self {
ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
ButtonVariant::Filled => cx.theme().colors().element_active,
}
}
}
#[derive(IntoElement)]
pub struct Button {
disabled: bool,
click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
icon: Option<Icon>,
icon_position: Option<IconPosition>,
label: SharedString,
variant: ButtonVariant,
width: Option<DefiniteLength>,
color: Option<Color>,
}
impl RenderOnce for Button {
type Rendered = gpui::Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let (icon_color, label_color) = match (self.disabled, self.color) {
(true, _) => (Color::Disabled, Color::Disabled),
(_, None) => (Color::Default, Color::Default),
(_, Some(color)) => (Color::from(color), color),
};
let mut button = h_stack()
.id(SharedString::from(format!("{}", self.label)))
.relative()
.p_1()
.text_ui()
.rounded_md()
.bg(self.variant.bg_color(cx))
.cursor_pointer()
.hover(|style| style.bg(self.variant.bg_color_hover(cx)))
.active(|style| style.bg(self.variant.bg_color_active(cx)));
match (self.icon, self.icon_position) {
(Some(_), Some(IconPosition::Left)) => {
button = button
.gap_1()
.child(self.render_label(label_color))
.children(self.render_icon(icon_color))
}
(Some(_), Some(IconPosition::Right)) => {
button = button
.gap_1()
.children(self.render_icon(icon_color))
.child(self.render_label(label_color))
}
(_, _) => button = button.child(self.render_label(label_color)),
}
if let Some(width) = self.width {
button = button.w(width).justify_center();
}
if let Some(click_handler) = self.click_handler.clone() {
button = button.on_click(move |event, cx| {
click_handler(event, cx);
});
}
button
}
}
impl Button {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
disabled: false,
click_handler: None,
icon: None,
icon_position: None,
label: label.into(),
variant: Default::default(),
width: Default::default(),
color: None,
}
}
pub fn ghost(label: impl Into<SharedString>) -> Self {
Self::new(label).variant(ButtonVariant::Ghost)
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
if self.icon.is_none() {
panic!("An icon must be present if an icon_position is provided.");
}
self.icon_position = Some(icon_position);
self
}
pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
self.width = width;
self
}
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
self.click_handler = Some(Rc::new(handler));
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn color(mut self, color: Option<Color>) -> Self {
self.color = color;
self
}
pub fn label_color(&self, color: Option<Color>) -> Color {
if self.disabled {
Color::Disabled
} else if let Some(color) = color {
color
} else {
Default::default()
}
}
fn render_label(&self, color: Color) -> Label {
Label::new(self.label.clone())
.color(color)
.line_height_style(LineHeightStyle::UILabel)
}
fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
self.icon.map(|i| IconElement::new(i).color(icon_color))
}
}
#[derive(IntoElement)]
pub struct ButtonGroup {
buttons: Vec<Button>,
}
impl RenderOnce for ButtonGroup {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let mut group = h_stack();
for button in self.buttons.into_iter() {
group = group.child(button.render(cx));
}
group
}
}
impl ButtonGroup {
pub fn new(buttons: Vec<Button>) -> Self {
Self { buttons }
}
}

View file

@ -0,0 +1,91 @@
use gpui::AnyView;
use crate::prelude::*;
use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle};
#[derive(IntoElement)]
pub struct Button {
base: ButtonLike,
label: SharedString,
label_color: Option<Color>,
selected: bool,
}
impl Button {
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self {
base: ButtonLike::new(id),
label: label.into(),
label_color: None,
selected: false,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
self.label_color = label_color.into();
self
}
}
impl Clickable for Button {
fn on_click(
mut self,
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
) -> Self {
self.base = self.base.on_click(handler);
self
}
}
impl Disableable for Button {
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);
self
}
}
impl ButtonCommon for Button {
fn id(&self) -> &ElementId {
self.base.id()
}
fn style(mut self, style: ButtonStyle2) -> Self {
self.base = self.base.style(style);
self
}
fn size(mut self, size: ButtonSize2) -> Self {
self.base = self.base.size(size);
self
}
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.base = self.base.tooltip(tooltip);
self
}
}
impl RenderOnce for Button {
type Rendered = ButtonLike;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
let label_color = if self.base.disabled {
Color::Disabled
} else if self.selected {
Color::Selected
} else {
Color::Default
};
self.base.child(
Label::new(self.label)
.color(label_color)
.line_height_style(LineHeightStyle::UILabel),
)
}
}

View file

@ -1,31 +1,17 @@
use gpui::{
rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful,
StatefulInteractiveElement, WindowContext,
};
use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
use smallvec::SmallVec;
use crate::{h_stack, prelude::*};
use crate::h_stack;
use crate::prelude::*;
// 🚧 Heavily WIP 🚧
// #[derive(Default, PartialEq, Clone, Copy)]
// pub enum ButtonType2 {
// #[default]
// DefaultButton,
// IconButton,
// ButtonLike,
// SplitButton,
// ToggleButton,
// }
#[derive(Default, PartialEq, Clone, Copy)]
pub enum IconPosition2 {
#[default]
Before,
After,
pub trait ButtonCommon: Clickable + Disableable {
fn id(&self) -> &ElementId;
fn style(self, style: ButtonStyle2) -> Self;
fn size(self, size: ButtonSize2) -> Self;
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
}
#[derive(Default, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum ButtonStyle2 {
#[default]
Filled,
@ -34,7 +20,7 @@ pub enum ButtonStyle2 {
Transparent,
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct ButtonStyle {
pub background: Hsla,
pub border_color: Hsla,
@ -181,82 +167,11 @@ impl ButtonSize2 {
}
}
// pub struct Button {
// id: ElementId,
// icon: Option<Icon>,
// icon_color: Option<Color>,
// icon_position: Option<IconPosition2>,
// label: Option<Label>,
// label_color: Option<Color>,
// appearance: ButtonAppearance2,
// state: InteractionState,
// selected: bool,
// disabled: bool,
// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
// width: Option<DefiniteLength>,
// action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
// /// Used to pass down some content to the button
// /// to enable creating custom buttons.
// children: SmallVec<[AnyElement; 2]>,
// }
pub trait ButtonCommon: Clickable + Disableable {
fn id(&self) -> &ElementId;
fn style(self, style: ButtonStyle2) -> Self;
fn size(self, size: ButtonSize2) -> Self;
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
}
// pub struct LabelButton {
// // Base properties...
// id: ElementId,
// appearance: ButtonAppearance,
// state: InteractionState,
// disabled: bool,
// size: ButtonSize,
// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
// width: Option<DefiniteLength>,
// // Button-specific properties...
// label: Option<SharedString>,
// label_color: Option<Color>,
// icon: Option<Icon>,
// icon_color: Option<Color>,
// icon_position: Option<IconPosition>,
// // Define more fields for additional properties as needed
// }
// impl ButtonCommon for LabelButton {
// fn id(&self) -> &ElementId {
// &self.id
// }
// fn appearance(&mut self, appearance: ButtonAppearance) -> &mut Self {
// self.style= style;
// self
// }
// // implement methods from ButtonCommon trait...
// }
// impl LabelButton {
// pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
// Self {
// id: id.into(),
// label: Some(label.into()),
// // initialize other fields with default values...
// }
// }
// // ... Define other builder methods specific to Button type...
// }
// TODO: Icon Button
#[derive(IntoElement)]
pub struct ButtonLike {
id: ElementId,
style: ButtonStyle2,
disabled: bool,
pub(super) style: ButtonStyle2,
pub(super) disabled: bool,
size: ButtonSize2,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@ -325,6 +240,12 @@ impl ButtonCommon for ButtonLike {
}
}
impl ParentElement for ButtonLike {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
impl RenderOnce for ButtonLike {
type Rendered = Stateful<Div>;
@ -349,57 +270,3 @@ impl RenderOnce for ButtonLike {
.children(self.children)
}
}
impl ParentElement for ButtonLike {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
// pub struct ToggleButton {
// // based on either IconButton2 or Button, with additional 'selected: bool' property
// }
// impl ButtonCommon for ToggleButton {
// fn id(&self) -> &ElementId {
// &self.id
// }
// // ... Implement other methods from ButtonCommon trait with builder patterns...
// }
// impl ToggleButton {
// pub fn new() -> Self {
// // Initialize with default values
// Self {
// // ... initialize fields, possibly with defaults or required parameters...
// }
// }
// // ... Define other builder methods specific to ToggleButton type...
// }
// pub struct SplitButton {
// // Base properties...
// id: ElementId,
// // Button-specific properties, possibly including a DefaultButton
// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
// // More fields as necessary...
// }
// impl ButtonCommon for SplitButton {
// fn id(&self) -> &ElementId {
// &self.id
// }
// // ... Implement other methods from ButtonCommon trait with builder patterns...
// }
// impl SplitButton {
// pub fn new(id: impl Into<ElementId>) -> Self {
// Self {
// id: id.into(),
// // ... initialize other fields with default values...
// }
// }
// // ... Define other builder methods specific to SplitButton type...
// }

View file

@ -0,0 +1,102 @@
use gpui::{Action, AnyView};
use crate::prelude::*;
use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize};
#[derive(IntoElement)]
pub struct IconButton {
base: ButtonLike,
icon: Icon,
icon_size: IconSize,
icon_color: Color,
selected: bool,
}
impl IconButton {
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
Self {
base: ButtonLike::new(id),
icon,
icon_size: IconSize::default(),
icon_color: Color::Default,
selected: false,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn icon_size(mut self, icon_size: IconSize) -> Self {
self.icon_size = icon_size;
self
}
pub fn icon_color(mut self, icon_color: Color) -> Self {
self.icon_color = icon_color;
self
}
pub fn action(self, action: Box<dyn Action>) -> Self {
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
}
}
impl Clickable for IconButton {
fn on_click(
mut self,
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
) -> Self {
self.base = self.base.on_click(handler);
self
}
}
impl Disableable for IconButton {
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);
self
}
}
impl ButtonCommon for IconButton {
fn id(&self) -> &ElementId {
self.base.id()
}
fn style(mut self, style: ButtonStyle2) -> Self {
self.base = self.base.style(style);
self
}
fn size(mut self, size: ButtonSize2) -> Self {
self.base = self.base.size(size);
self
}
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.base = self.base.tooltip(tooltip);
self
}
}
impl RenderOnce for IconButton {
type Rendered = ButtonLike;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
let icon_color = if self.base.disabled {
Color::Disabled
} else if self.selected {
Color::Selected
} else {
self.icon_color
};
self.base.child(
IconElement::new(self.icon)
.size(self.icon_size)
.color(icon_color),
)
}
}

View file

@ -0,0 +1,7 @@
mod button;
mod button_like;
mod icon_button;
pub use button::*;
pub use button_like::*;
pub use icon_button::*;

View file

@ -39,8 +39,8 @@ impl RenderOnce for Disclosure {
false => Icon::ChevronRight,
},
)
.color(Color::Muted)
.size(IconSize::Small)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.when_some(self.on_toggle, move |this, on_toggle| {
this.on_click(move |event, cx| on_toggle(event, cx))
})

View file

@ -1,135 +0,0 @@
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
#[derive(IntoElement)]
pub struct IconButton {
id: ElementId,
icon: Icon,
color: Color,
size: IconSize,
variant: ButtonVariant,
disabled: bool,
selected: bool,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
}
impl RenderOnce for IconButton {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let icon_color = match (self.disabled, self.selected, self.color) {
(true, _, _) => Color::Disabled,
(false, true, _) => Color::Selected,
_ => self.color,
};
let (mut bg_color, bg_active_color) = match self.variant {
ButtonVariant::Filled => (
cx.theme().colors().element_background,
cx.theme().colors().element_active,
),
ButtonVariant::Ghost => (
cx.theme().colors().ghost_element_background,
cx.theme().colors().ghost_element_active,
),
};
if self.selected {
bg_color = cx.theme().colors().element_selected;
}
let mut button = h_stack()
.id(self.id.clone())
.justify_center()
.rounded_md()
.p_1()
.bg(bg_color)
.cursor_pointer()
// Nate: Trying to figure out the right places we want to show a
// hover state here. I think it is a bit heavy to have it on every
// place we use an icon button.
// .hover(|style| style.bg(bg_hover_color))
.active(|style| style.bg(bg_active_color))
.child(
IconElement::new(self.icon)
.size(self.size)
.color(icon_color),
);
if let Some(click_handler) = self.on_click {
button = button.on_click(move |event, cx| {
cx.stop_propagation();
click_handler(event, cx);
})
}
if let Some(tooltip) = self.tooltip {
if !self.selected {
button = button.tooltip(move |cx| tooltip(cx))
}
}
button
}
}
impl IconButton {
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
Self {
id: id.into(),
icon,
color: Color::default(),
size: Default::default(),
variant: ButtonVariant::default(),
selected: false,
disabled: false,
tooltip: None,
on_click: None,
}
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = icon;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn size(mut self, size: IconSize) -> Self {
self.size = size;
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.tooltip = Some(Box::new(tooltip));
self
}
pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self {
self.on_click = Some(Box::new(handler));
self
}
pub fn action(self, action: Box<dyn Action>) -> Self {
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
}
}

View file

@ -78,7 +78,7 @@ impl RenderOnce for ListHeader {
h_stack()
.gap_2()
.items_center()
.children(icons.into_iter().map(|i| i.color(Color::Muted))),
.children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
),
Some(ListHeaderMeta::Button(label)) => div().child(label),
Some(ListHeaderMeta::Text(label)) => div().child(label),

View file

@ -2,7 +2,7 @@ use gpui::{Div, Render};
use story::Story;
use crate::prelude::*;
use crate::{h_stack, Button, Icon, IconPosition};
use crate::{Button, ButtonStyle2};
pub struct ButtonStory;
@ -12,66 +12,11 @@ impl Render for ButtonStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
Story::container()
.child(Story::title_for::<Button>())
.child(
div()
.flex()
.gap_8()
.child(
div().child(Story::label("Ghost (Default)")).child(
h_stack()
.gap_2()
.child(Button::new("Label").variant(ButtonVariant::Ghost)),
),
)
.child(Story::label("Ghost Left Icon"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Left),
),
),
)
.child(Story::label("Ghost Right Icon"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Right),
),
)
.child(
div().child(Story::label("Filled")).child(
h_stack()
.gap_2()
.child(Button::new("Label").variant(ButtonVariant::Filled)),
),
)
.child(Story::label("Filled Left Button"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Left),
),
)
.child(Story::label("Filled Right Button"))
.child(
h_stack().gap_2().child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Right),
),
)
.child(Story::label("Button with `on_click`"))
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.on_click(|_, _cx| println!("Button clicked.")),
)
.child(Story::label("Default"))
.child(Button::new("default_filled", "Click me"))
.child(Story::label("Default (Subtle)"))
.child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle))
.child(Story::label("Default (Transparent)"))
.child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent))
}
}

View file

@ -8,6 +8,5 @@ pub use crate::clickable::*;
pub use crate::disableable::*;
pub use crate::fixed::*;
pub use crate::selectable::*;
pub use crate::StyledExt;
pub use crate::{ButtonVariant, Color};
pub use crate::{ButtonCommon, Color, StyledExt};
pub use theme::ActiveTheme;