button.rs

  1use std::marker::PhantomData;
  2use std::sync::Arc;
  3
  4use gpui3::rems;
  5use gpui3::{DefiniteLength, Hsla, Interactive, MouseButton, WindowContext};
  6
  7use crate::prelude::*;
  8use crate::settings::user_settings;
  9use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
 10
 11#[derive(Default, PartialEq, Clone, Copy)]
 12pub enum IconPosition {
 13    #[default]
 14    Left,
 15    Right,
 16}
 17
 18#[derive(Default, Copy, Clone, PartialEq)]
 19pub enum ButtonVariant {
 20    #[default]
 21    Ghost,
 22    Filled,
 23}
 24
 25pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + 'static + Send + Sync>;
 26
 27struct ButtonHandlers<S: 'static + Send + Sync> {
 28    click: Option<ClickHandler<S>>,
 29}
 30
 31impl<S: 'static + Send + Sync> Default for ButtonHandlers<S> {
 32    fn default() -> Self {
 33        Self { click: None }
 34    }
 35}
 36
 37#[derive(Element)]
 38pub struct Button<S: 'static + Send + Sync + Clone> {
 39    state_type: PhantomData<S>,
 40    label: SharedString,
 41    variant: ButtonVariant,
 42    state: InteractionState,
 43    icon: Option<Icon>,
 44    icon_position: Option<IconPosition>,
 45    width: Option<DefiniteLength>,
 46    handlers: ButtonHandlers<S>,
 47}
 48
 49impl<S: 'static + Send + Sync + Clone> Button<S> {
 50    pub fn new(label: impl Into<SharedString>) -> Self {
 51        Self {
 52            state_type: PhantomData,
 53            label: label.into(),
 54            variant: Default::default(),
 55            state: Default::default(),
 56            icon: None,
 57            icon_position: None,
 58            width: Default::default(),
 59            handlers: ButtonHandlers::default(),
 60        }
 61    }
 62
 63    pub fn ghost(label: impl Into<SharedString>) -> Self {
 64        Self::new(label).variant(ButtonVariant::Ghost)
 65    }
 66
 67    pub fn variant(mut self, variant: ButtonVariant) -> Self {
 68        self.variant = variant;
 69        self
 70    }
 71
 72    pub fn state(mut self, state: InteractionState) -> Self {
 73        self.state = state;
 74        self
 75    }
 76
 77    pub fn icon(mut self, icon: Icon) -> Self {
 78        self.icon = Some(icon);
 79        self
 80    }
 81
 82    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
 83        if self.icon.is_none() {
 84            panic!("An icon must be present if an icon_position is provided.");
 85        }
 86        self.icon_position = Some(icon_position);
 87        self
 88    }
 89
 90    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
 91        self.width = width;
 92        self
 93    }
 94
 95    pub fn on_click(mut self, handler: ClickHandler<S>) -> Self {
 96        self.handlers.click = Some(handler);
 97        self
 98    }
 99
100    fn background_color(&self, cx: &mut ViewContext<S>) -> Hsla {
101        let color = ThemeColor::new(cx);
102
103        match (self.variant, self.state) {
104            (ButtonVariant::Ghost, InteractionState::Enabled) => color.ghost_element,
105            (ButtonVariant::Ghost, InteractionState::Focused) => color.ghost_element,
106            (ButtonVariant::Ghost, InteractionState::Hovered) => color.ghost_element_hover,
107            (ButtonVariant::Ghost, InteractionState::Active) => color.ghost_element_active,
108            (ButtonVariant::Ghost, InteractionState::Disabled) => color.filled_element_disabled,
109            (ButtonVariant::Filled, InteractionState::Enabled) => color.filled_element,
110            (ButtonVariant::Filled, InteractionState::Focused) => color.filled_element,
111            (ButtonVariant::Filled, InteractionState::Hovered) => color.filled_element_hover,
112            (ButtonVariant::Filled, InteractionState::Active) => color.filled_element_active,
113            (ButtonVariant::Filled, InteractionState::Disabled) => color.filled_element_disabled,
114        }
115    }
116
117    fn label_color(&self) -> LabelColor {
118        match self.state {
119            InteractionState::Disabled => LabelColor::Disabled,
120            _ => Default::default(),
121        }
122    }
123
124    fn icon_color(&self) -> IconColor {
125        match self.state {
126            InteractionState::Disabled => IconColor::Disabled,
127            _ => Default::default(),
128        }
129    }
130
131    fn border_color(&self, cx: &WindowContext) -> Hsla {
132        let color = ThemeColor::new(cx);
133
134        match self.state {
135            InteractionState::Focused => color.border_focused,
136            _ => color.border_transparent,
137        }
138    }
139
140    fn render_label(&self) -> Label<S> {
141        Label::new(self.label.clone())
142            .size(LabelSize::Small)
143            .color(self.label_color())
144    }
145
146    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement<S>> {
147        self.icon.map(|i| IconElement::new(i).color(icon_color))
148    }
149
150    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
151        let icon_color = self.icon_color();
152        let border_color = self.border_color(cx);
153        let setting = user_settings();
154
155        let mut el = h_stack()
156            .p_1()
157            .text_size(ui_size(1.125))
158            .rounded_md()
159            .border()
160            .border_color(border_color)
161            .bg(self.background_color(cx))
162            .hover(|style| {
163                let color = ThemeColor::new(cx);
164
165                style.bg(match self.variant {
166                    ButtonVariant::Ghost => color.ghost_element_hover,
167                    ButtonVariant::Filled => color.filled_element_hover,
168                })
169            });
170
171        match (self.icon, self.icon_position) {
172            (Some(_), Some(IconPosition::Left)) => {
173                el = el
174                    .gap_1()
175                    .child(self.render_label())
176                    .children(self.render_icon(icon_color))
177            }
178            (Some(_), Some(IconPosition::Right)) => {
179                el = el
180                    .gap_1()
181                    .children(self.render_icon(icon_color))
182                    .child(self.render_label())
183            }
184            (_, _) => el = el.child(self.render_label()),
185        }
186
187        if let Some(width) = self.width {
188            el = el.w(width).justify_center();
189        }
190
191        if let Some(click_handler) = self.handlers.click.clone() {
192            el = el.on_mouse_down(MouseButton::Left, move |state, event, cx| {
193                click_handler(state, cx);
194            });
195        }
196
197        el
198    }
199}
200
201#[cfg(feature = "stories")]
202pub use stories::*;
203
204#[cfg(feature = "stories")]
205mod stories {
206    use gpui3::rems;
207    use strum::IntoEnumIterator;
208
209    use crate::{h_stack, v_stack, LabelColor, LabelSize, Story};
210
211    use super::*;
212
213    #[derive(Element)]
214    pub struct ButtonStory<S: 'static + Send + Sync + Clone> {
215        state_type: PhantomData<S>,
216    }
217
218    impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
219        pub fn new() -> Self {
220            Self {
221                state_type: PhantomData,
222            }
223        }
224
225        fn render(
226            &mut self,
227            _view: &mut S,
228            cx: &mut ViewContext<S>,
229        ) -> impl Element<ViewState = S> {
230            let states = InteractionState::iter();
231
232            Story::container(cx)
233                .child(Story::title_for::<_, Button<S>>(cx))
234                .child(
235                    div()
236                        .flex()
237                        .gap_8()
238                        .child(
239                            div()
240                                .child(Story::label(cx, "Ghost (Default)"))
241                                .child(h_stack().gap_2().children(states.clone().map(|state| {
242                                    v_stack()
243                                        .gap_1()
244                                        .child(
245                                            Label::new(state.to_string())
246                                                .color(LabelColor::Muted)
247                                                .size(LabelSize::Small),
248                                        )
249                                        .child(
250                                            Button::new("Label")
251                                                .variant(ButtonVariant::Ghost)
252                                                .state(state),
253                                        )
254                                })))
255                                .child(Story::label(cx, "Ghost – Left Icon"))
256                                .child(h_stack().gap_2().children(states.clone().map(|state| {
257                                    v_stack()
258                                        .gap_1()
259                                        .child(
260                                            Label::new(state.to_string())
261                                                .color(LabelColor::Muted)
262                                                .size(LabelSize::Small),
263                                        )
264                                        .child(
265                                            Button::new("Label")
266                                                .variant(ButtonVariant::Ghost)
267                                                .icon(Icon::Plus)
268                                                .icon_position(IconPosition::Left)
269                                                .state(state),
270                                        )
271                                })))
272                                .child(Story::label(cx, "Ghost – Right Icon"))
273                                .child(h_stack().gap_2().children(states.clone().map(|state| {
274                                    v_stack()
275                                        .gap_1()
276                                        .child(
277                                            Label::new(state.to_string())
278                                                .color(LabelColor::Muted)
279                                                .size(LabelSize::Small),
280                                        )
281                                        .child(
282                                            Button::new("Label")
283                                                .variant(ButtonVariant::Ghost)
284                                                .icon(Icon::Plus)
285                                                .icon_position(IconPosition::Right)
286                                                .state(state),
287                                        )
288                                }))),
289                        )
290                        .child(
291                            div()
292                                .child(Story::label(cx, "Filled"))
293                                .child(h_stack().gap_2().children(states.clone().map(|state| {
294                                    v_stack()
295                                        .gap_1()
296                                        .child(
297                                            Label::new(state.to_string())
298                                                .color(LabelColor::Muted)
299                                                .size(LabelSize::Small),
300                                        )
301                                        .child(
302                                            Button::new("Label")
303                                                .variant(ButtonVariant::Filled)
304                                                .state(state),
305                                        )
306                                })))
307                                .child(Story::label(cx, "Filled – Left Button"))
308                                .child(h_stack().gap_2().children(states.clone().map(|state| {
309                                    v_stack()
310                                        .gap_1()
311                                        .child(
312                                            Label::new(state.to_string())
313                                                .color(LabelColor::Muted)
314                                                .size(LabelSize::Small),
315                                        )
316                                        .child(
317                                            Button::new("Label")
318                                                .variant(ButtonVariant::Filled)
319                                                .icon(Icon::Plus)
320                                                .icon_position(IconPosition::Left)
321                                                .state(state),
322                                        )
323                                })))
324                                .child(Story::label(cx, "Filled – Right Button"))
325                                .child(h_stack().gap_2().children(states.clone().map(|state| {
326                                    v_stack()
327                                        .gap_1()
328                                        .child(
329                                            Label::new(state.to_string())
330                                                .color(LabelColor::Muted)
331                                                .size(LabelSize::Small),
332                                        )
333                                        .child(
334                                            Button::new("Label")
335                                                .variant(ButtonVariant::Filled)
336                                                .icon(Icon::Plus)
337                                                .icon_position(IconPosition::Right)
338                                                .state(state),
339                                        )
340                                }))),
341                        )
342                        .child(
343                            div()
344                                .child(Story::label(cx, "Fixed With"))
345                                .child(h_stack().gap_2().children(states.clone().map(|state| {
346                                    v_stack()
347                                        .gap_1()
348                                        .child(
349                                            Label::new(state.to_string())
350                                                .color(LabelColor::Muted)
351                                                .size(LabelSize::Small),
352                                        )
353                                        .child(
354                                            Button::new("Label")
355                                                .variant(ButtonVariant::Filled)
356                                                .state(state)
357                                                .width(Some(rems(6.).into())),
358                                        )
359                                })))
360                                .child(Story::label(cx, "Fixed With – Left Icon"))
361                                .child(h_stack().gap_2().children(states.clone().map(|state| {
362                                    v_stack()
363                                        .gap_1()
364                                        .child(
365                                            Label::new(state.to_string())
366                                                .color(LabelColor::Muted)
367                                                .size(LabelSize::Small),
368                                        )
369                                        .child(
370                                            Button::new("Label")
371                                                .variant(ButtonVariant::Filled)
372                                                .state(state)
373                                                .icon(Icon::Plus)
374                                                .icon_position(IconPosition::Left)
375                                                .width(Some(rems(6.).into())),
376                                        )
377                                })))
378                                .child(Story::label(cx, "Fixed With – Right Icon"))
379                                .child(h_stack().gap_2().children(states.clone().map(|state| {
380                                    v_stack()
381                                        .gap_1()
382                                        .child(
383                                            Label::new(state.to_string())
384                                                .color(LabelColor::Muted)
385                                                .size(LabelSize::Small),
386                                        )
387                                        .child(
388                                            Button::new("Label")
389                                                .variant(ButtonVariant::Filled)
390                                                .state(state)
391                                                .icon(Icon::Plus)
392                                                .icon_position(IconPosition::Right)
393                                                .width(Some(rems(6.).into())),
394                                        )
395                                }))),
396                        ),
397                )
398                .child(Story::label(cx, "Button with `on_click`"))
399                .child(
400                    Button::new("Label")
401                        .variant(ButtonVariant::Ghost)
402                        .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
403                )
404        }
405    }
406}