button.rs

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