button.rs

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