button.rs

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