button.rs

  1use std::rc::Rc;
  2
  3use gpui2::geometry::DefiniteLength;
  4use gpui2::platform::MouseButton;
  5use gpui2::{EventContext, Hsla, Interactive, WindowContext};
  6
  7use crate::prelude::*;
  8use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
  9
 10#[derive(Default, PartialEq, Clone, Copy)]
 11pub enum IconPosition {
 12    #[default]
 13    Left,
 14    Right,
 15}
 16
 17#[derive(Default, Copy, Clone, PartialEq)]
 18pub enum ButtonVariant {
 19    #[default]
 20    Ghost,
 21    Filled,
 22}
 23
 24struct ButtonHandlers<V> {
 25    click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
 26}
 27
 28impl<V> Default for ButtonHandlers<V> {
 29    fn default() -> Self {
 30        Self { click: None }
 31    }
 32}
 33
 34#[derive(Element)]
 35pub struct Button<V: 'static> {
 36    label: String,
 37    variant: ButtonVariant,
 38    state: InteractionState,
 39    icon: Option<Icon>,
 40    icon_position: Option<IconPosition>,
 41    width: Option<DefiniteLength>,
 42    handlers: ButtonHandlers<V>,
 43}
 44
 45impl<V: 'static> Button<V> {
 46    pub fn new<L>(label: L) -> Self
 47    where
 48        L: Into<String>,
 49    {
 50        Self {
 51            label: label.into(),
 52            variant: Default::default(),
 53            state: Default::default(),
 54            icon: None,
 55            icon_position: None,
 56            width: Default::default(),
 57            handlers: ButtonHandlers::default(),
 58        }
 59    }
 60
 61    pub fn ghost<L>(label: L) -> Self
 62    where
 63        L: Into<String>,
 64    {
 65        Self::new(label).variant(ButtonVariant::Ghost)
 66    }
 67
 68    pub fn variant(mut self, variant: ButtonVariant) -> Self {
 69        self.variant = variant;
 70        self
 71    }
 72
 73    pub fn state(mut self, state: InteractionState) -> Self {
 74        self.state = state;
 75        self
 76    }
 77
 78    pub fn icon(mut self, icon: Icon) -> Self {
 79        self.icon = Some(icon);
 80        self
 81    }
 82
 83    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
 84        if self.icon.is_none() {
 85            panic!("An icon must be present if an icon_position is provided.");
 86        }
 87        self.icon_position = Some(icon_position);
 88        self
 89    }
 90
 91    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
 92        self.width = width;
 93        self
 94    }
 95
 96    pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext<V>) + 'static) -> Self {
 97        self.handlers.click = Some(Rc::new(handler));
 98        self
 99    }
100
101    fn background_color(&self, cx: &mut ViewContext<V>) -> Hsla {
102        let theme = theme(cx);
103        let system_color = SystemColor::new();
104
105        match (self.variant, self.state) {
106            (ButtonVariant::Ghost, InteractionState::Hovered) => {
107                theme.lowest.base.hovered.background
108            }
109            (ButtonVariant::Ghost, InteractionState::Active) => {
110                theme.lowest.base.pressed.background
111            }
112            (ButtonVariant::Filled, InteractionState::Enabled) => {
113                theme.lowest.on.default.background
114            }
115            (ButtonVariant::Filled, InteractionState::Hovered) => {
116                theme.lowest.on.hovered.background
117            }
118            (ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background,
119            (ButtonVariant::Filled, InteractionState::Disabled) => {
120                theme.lowest.on.disabled.background
121            }
122            _ => system_color.transparent,
123        }
124    }
125
126    fn label_color(&self) -> LabelColor {
127        match self.state {
128            InteractionState::Disabled => LabelColor::Disabled,
129            _ => Default::default(),
130        }
131    }
132
133    fn icon_color(&self) -> IconColor {
134        match self.state {
135            InteractionState::Disabled => IconColor::Disabled,
136            _ => Default::default(),
137        }
138    }
139
140    fn border_color(&self, cx: &WindowContext) -> Hsla {
141        let theme = theme(cx);
142        let system_color = SystemColor::new();
143
144        match self.state {
145            InteractionState::Focused => theme.lowest.accent.default.border,
146            _ => system_color.transparent,
147        }
148    }
149
150    fn render_label(&self) -> Label {
151        Label::new(self.label.clone())
152            .size(LabelSize::Small)
153            .color(self.label_color())
154    }
155
156    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
157        self.icon.map(|i| IconElement::new(i).color(icon_color))
158    }
159
160    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
161        let theme = theme(cx);
162        let icon_color = self.icon_color();
163        let system_color = SystemColor::new();
164        let border_color = self.border_color(cx);
165
166        let mut el = h_stack()
167            .h_6()
168            .px_1()
169            .items_center()
170            .rounded_md()
171            .border()
172            .border_color(border_color)
173            .fill(self.background_color(cx));
174
175        match (self.icon, self.icon_position) {
176            (Some(_), Some(IconPosition::Left)) => {
177                el = el
178                    .gap_1()
179                    .child(self.render_label())
180                    .children(self.render_icon(icon_color))
181            }
182            (Some(_), Some(IconPosition::Right)) => {
183                el = el
184                    .gap_1()
185                    .children(self.render_icon(icon_color))
186                    .child(self.render_label())
187            }
188            (_, _) => el = el.child(self.render_label()),
189        }
190
191        if let Some(width) = self.width {
192            el = el.w(width).justify_center();
193        }
194
195        if let Some(click_handler) = self.handlers.click.clone() {
196            el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| {
197                click_handler(view, cx);
198            });
199        }
200
201        el
202    }
203}