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