use std::marker::PhantomData; use std::sync::Arc; use gpui3::rems; use gpui3::{DefiniteLength, Hsla, Interactive, MouseButton, WindowContext}; use crate::prelude::*; use crate::settings::user_settings; use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor, LabelSize}; #[derive(Default, PartialEq, Clone, Copy)] pub enum IconPosition { #[default] Left, Right, } #[derive(Default, Copy, Clone, PartialEq)] pub enum ButtonVariant { #[default] Ghost, Filled, } pub type ClickHandler = Arc) + 'static + Send + Sync>; struct ButtonHandlers { click: Option>, } impl Default for ButtonHandlers { fn default() -> Self { Self { click: None } } } #[derive(Element)] pub struct Button { state_type: PhantomData, label: SharedString, variant: ButtonVariant, state: InteractionState, icon: Option, icon_position: Option, width: Option, handlers: ButtonHandlers, } impl Button { pub fn new(label: impl Into) -> Self { Self { state_type: PhantomData, label: label.into(), variant: Default::default(), state: Default::default(), icon: None, icon_position: None, width: Default::default(), handlers: ButtonHandlers::default(), } } pub fn ghost(label: impl Into) -> Self { Self::new(label).variant(ButtonVariant::Ghost) } pub fn variant(mut self, variant: ButtonVariant) -> Self { self.variant = variant; self } pub fn state(mut self, state: InteractionState) -> Self { self.state = state; 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) -> Self { self.width = width; self } pub fn on_click(mut self, handler: ClickHandler) -> Self { self.handlers.click = Some(handler); self } fn background_color(&self, cx: &mut ViewContext) -> Hsla { let color = ThemeColor::new(cx); match (self.variant, self.state) { (ButtonVariant::Ghost, InteractionState::Enabled) => color.ghost_element, (ButtonVariant::Ghost, InteractionState::Focused) => color.ghost_element, (ButtonVariant::Ghost, InteractionState::Hovered) => color.ghost_element_hover, (ButtonVariant::Ghost, InteractionState::Active) => color.ghost_element_active, (ButtonVariant::Ghost, InteractionState::Disabled) => color.filled_element_disabled, (ButtonVariant::Filled, InteractionState::Enabled) => color.filled_element, (ButtonVariant::Filled, InteractionState::Focused) => color.filled_element, (ButtonVariant::Filled, InteractionState::Hovered) => color.filled_element_hover, (ButtonVariant::Filled, InteractionState::Active) => color.filled_element_active, (ButtonVariant::Filled, InteractionState::Disabled) => color.filled_element_disabled, } } fn label_color(&self) -> LabelColor { match self.state { InteractionState::Disabled => LabelColor::Disabled, _ => Default::default(), } } fn icon_color(&self) -> IconColor { match self.state { InteractionState::Disabled => IconColor::Disabled, _ => Default::default(), } } fn border_color(&self, cx: &WindowContext) -> Hsla { let color = ThemeColor::new(cx); match self.state { InteractionState::Focused => color.border_focused, _ => color.border_transparent, } } fn render_label(&self) -> Label { Label::new(self.label.clone()) .size(LabelSize::Small) .color(self.label_color()) } fn render_icon(&self, icon_color: IconColor) -> Option> { self.icon.map(|i| IconElement::new(i).color(icon_color)) } fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { let icon_color = self.icon_color(); let border_color = self.border_color(cx); let setting = user_settings(); let mut el = h_stack() .p_1() .text_size(ui_size(1.125)) .rounded_md() .border() .border_color(border_color) .bg(self.background_color(cx)) .hover(|style| { let color = ThemeColor::new(cx); style.bg(match self.variant { ButtonVariant::Ghost => color.ghost_element_hover, ButtonVariant::Filled => color.filled_element_hover, }) }); match (self.icon, self.icon_position) { (Some(_), Some(IconPosition::Left)) => { el = el .gap_1() .child(self.render_label()) .children(self.render_icon(icon_color)) } (Some(_), Some(IconPosition::Right)) => { el = el .gap_1() .children(self.render_icon(icon_color)) .child(self.render_label()) } (_, _) => el = el.child(self.render_label()), } if let Some(width) = self.width { el = el.w(width).justify_center(); } if let Some(click_handler) = self.handlers.click.clone() { el = el.on_mouse_down(MouseButton::Left, move |state, event, cx| { click_handler(state, cx); }); } el } } #[cfg(feature = "stories")] pub use stories::*; #[cfg(feature = "stories")] mod stories { use gpui3::rems; use strum::IntoEnumIterator; use crate::{h_stack, v_stack, LabelColor, LabelSize, Story}; use super::*; #[derive(Element)] pub struct ButtonStory { state_type: PhantomData, } impl ButtonStory { pub fn new() -> Self { Self { state_type: PhantomData, } } fn render( &mut self, _view: &mut S, cx: &mut ViewContext, ) -> impl Element { let states = InteractionState::iter(); Story::container(cx) .child(Story::title_for::<_, Button>(cx)) .child( div() .flex() .gap_8() .child( div() .child(Story::label(cx, "Ghost (Default)")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Ghost) .state(state), ) }))) .child(Story::label(cx, "Ghost – Left Icon")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Ghost) .icon(Icon::Plus) .icon_position(IconPosition::Left) .state(state), ) }))) .child(Story::label(cx, "Ghost – Right Icon")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Ghost) .icon(Icon::Plus) .icon_position(IconPosition::Right) .state(state), ) }))), ) .child( div() .child(Story::label(cx, "Filled")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Filled) .state(state), ) }))) .child(Story::label(cx, "Filled – Left Button")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Filled) .icon(Icon::Plus) .icon_position(IconPosition::Left) .state(state), ) }))) .child(Story::label(cx, "Filled – Right Button")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Filled) .icon(Icon::Plus) .icon_position(IconPosition::Right) .state(state), ) }))), ) .child( div() .child(Story::label(cx, "Fixed With")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Filled) .state(state) .width(Some(rems(6.).into())), ) }))) .child(Story::label(cx, "Fixed With – Left Icon")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Filled) .state(state) .icon(Icon::Plus) .icon_position(IconPosition::Left) .width(Some(rems(6.).into())), ) }))) .child(Story::label(cx, "Fixed With – Right Icon")) .child(h_stack().gap_2().children(states.clone().map(|state| { v_stack() .gap_1() .child( Label::new(state.to_string()) .color(LabelColor::Muted) .size(LabelSize::Small), ) .child( Button::new("Label") .variant(ButtonVariant::Filled) .state(state) .icon(Icon::Plus) .icon_position(IconPosition::Right) .width(Some(rems(6.).into())), ) }))), ), ) .child(Story::label(cx, "Button with `on_click`")) .child( Button::new("Label") .variant(ButtonVariant::Ghost) .on_click(Arc::new(|_view, _cx| println!("Button clicked."))), ) } } }