button.rs

  1use std::marker::PhantomData;
  2use std::sync::Arc;
  3
  4use gpui2::{div, DefiniteLength, Hsla, MouseButton, WindowContext};
  5
  6use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor};
  7use crate::{prelude::*, LineHeightStyle};
  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
 23impl ButtonVariant {
 24    pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
 25        let theme = theme(cx);
 26
 27        match self {
 28            ButtonVariant::Ghost => theme.ghost_element,
 29            ButtonVariant::Filled => theme.filled_element,
 30        }
 31    }
 32
 33    pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
 34        let theme = theme(cx);
 35
 36        match self {
 37            ButtonVariant::Ghost => theme.ghost_element_hover,
 38            ButtonVariant::Filled => theme.filled_element_hover,
 39        }
 40    }
 41
 42    pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
 43        let theme = theme(cx);
 44
 45        match self {
 46            ButtonVariant::Ghost => theme.ghost_element_active,
 47            ButtonVariant::Filled => theme.filled_element_active,
 48        }
 49    }
 50}
 51
 52pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + 'static + Send + Sync>;
 53
 54struct ButtonHandlers<S: 'static + Send + Sync> {
 55    click: Option<ClickHandler<S>>,
 56}
 57
 58impl<S: 'static + Send + Sync> Default for ButtonHandlers<S> {
 59    fn default() -> Self {
 60        Self { click: None }
 61    }
 62}
 63
 64#[derive(Element)]
 65pub struct Button<S: 'static + Send + Sync> {
 66    state_type: PhantomData<S>,
 67    disabled: bool,
 68    handlers: ButtonHandlers<S>,
 69    icon: Option<Icon>,
 70    icon_position: Option<IconPosition>,
 71    label: SharedString,
 72    variant: ButtonVariant,
 73    width: Option<DefiniteLength>,
 74}
 75
 76impl<S: 'static + Send + Sync> Button<S> {
 77    pub fn new(label: impl Into<SharedString>) -> Self {
 78        Self {
 79            state_type: PhantomData,
 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<S>) -> 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<S> {
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<S>> {
150        self.icon.map(|i| IconElement::new(i).color(icon_color))
151    }
152
153    pub fn render(
154        &mut self,
155        _view: &mut S,
156        cx: &mut ViewContext<S>,
157    ) -> impl Element<S> {
158        let icon_color = self.icon_color();
159
160        let mut button = h_stack()
161            .relative()
162            .id(SharedString::from(format!("{}", self.label)))
163            .p_1()
164            .text_size(ui_size(cx, 1.))
165            .rounded_md()
166            .bg(self.variant.bg_color(cx))
167            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
168            .active(|style| style.bg(self.variant.bg_color_active(cx)));
169
170        match (self.icon, self.icon_position) {
171            (Some(_), Some(IconPosition::Left)) => {
172                button = button
173                    .gap_1()
174                    .child(self.render_label())
175                    .children(self.render_icon(icon_color))
176            }
177            (Some(_), Some(IconPosition::Right)) => {
178                button = button
179                    .gap_1()
180                    .children(self.render_icon(icon_color))
181                    .child(self.render_label())
182            }
183            (_, _) => button = button.child(self.render_label()),
184        }
185
186        if let Some(width) = self.width {
187            button = button.w(width).justify_center();
188        }
189
190        if let Some(click_handler) = self.handlers.click.clone() {
191            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
192                click_handler(state, cx);
193            });
194        }
195
196        button
197    }
198}
199
200#[derive(Element)]
201pub struct ButtonGroup<S: 'static + Send + Sync> {
202    state_type: PhantomData<S>,
203    buttons: Vec<Button<S>>,
204}
205
206impl<S: 'static + Send + Sync> ButtonGroup<S> {
207    pub fn new(buttons: Vec<Button<S>>) -> Self {
208        Self {
209            state_type: PhantomData,
210            buttons,
211        }
212    }
213
214    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<S> {
215        let mut el = h_stack().text_size(ui_size(cx, 1.));
216
217        for button in &mut self.buttons {
218            el = el.child(button.render(_view, cx));
219        }
220
221        el
222    }
223}
224
225#[cfg(feature = "stories")]
226pub use stories::*;
227
228#[cfg(feature = "stories")]
229mod stories {
230    use gpui2::rems;
231    use strum::IntoEnumIterator;
232
233    use crate::{h_stack, v_stack, LabelColor, Story};
234
235    use super::*;
236
237    #[derive(Element)]
238    pub struct ButtonStory<S: 'static + Send + Sync + Clone> {
239        state_type: PhantomData<S>,
240    }
241
242    impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
243        pub fn new() -> Self {
244            Self {
245                state_type: PhantomData,
246            }
247        }
248
249        fn render(
250            &mut self,
251            _view: &mut S,
252            cx: &mut ViewContext<S>,
253        ) -> impl Element<S> {
254            let states = InteractionState::iter();
255
256            Story::container(cx)
257                .child(Story::title_for::<_, Button<S>>(cx))
258                .child(
259                    div()
260                        .flex()
261                        .gap_8()
262                        .child(
263                            div()
264                                .child(Story::label(cx, "Ghost (Default)"))
265                                .child(h_stack().gap_2().children(states.clone().map(|state| {
266                                    v_stack()
267                                        .gap_1()
268                                        .child(
269                                            Label::new(state.to_string()).color(LabelColor::Muted),
270                                        )
271                                        .child(
272                                            Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
273                                        )
274                                })))
275                                .child(Story::label(cx, "Ghost – Left Icon"))
276                                .child(h_stack().gap_2().children(states.clone().map(|state| {
277                                    v_stack()
278                                        .gap_1()
279                                        .child(
280                                            Label::new(state.to_string()).color(LabelColor::Muted),
281                                        )
282                                        .child(
283                                            Button::new("Label")
284                                                .variant(ButtonVariant::Ghost)
285                                                .icon(Icon::Plus)
286                                                .icon_position(IconPosition::Left), // .state(state),
287                                        )
288                                })))
289                                .child(Story::label(cx, "Ghost – Right Icon"))
290                                .child(h_stack().gap_2().children(states.clone().map(|state| {
291                                    v_stack()
292                                        .gap_1()
293                                        .child(
294                                            Label::new(state.to_string()).color(LabelColor::Muted),
295                                        )
296                                        .child(
297                                            Button::new("Label")
298                                                .variant(ButtonVariant::Ghost)
299                                                .icon(Icon::Plus)
300                                                .icon_position(IconPosition::Right), // .state(state),
301                                        )
302                                }))),
303                        )
304                        .child(
305                            div()
306                                .child(Story::label(cx, "Filled"))
307                                .child(h_stack().gap_2().children(states.clone().map(|state| {
308                                    v_stack()
309                                        .gap_1()
310                                        .child(
311                                            Label::new(state.to_string()).color(LabelColor::Muted),
312                                        )
313                                        .child(
314                                            Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
315                                        )
316                                })))
317                                .child(Story::label(cx, "Filled – Left Button"))
318                                .child(h_stack().gap_2().children(states.clone().map(|state| {
319                                    v_stack()
320                                        .gap_1()
321                                        .child(
322                                            Label::new(state.to_string()).color(LabelColor::Muted),
323                                        )
324                                        .child(
325                                            Button::new("Label")
326                                                .variant(ButtonVariant::Filled)
327                                                .icon(Icon::Plus)
328                                                .icon_position(IconPosition::Left), // .state(state),
329                                        )
330                                })))
331                                .child(Story::label(cx, "Filled – Right Button"))
332                                .child(h_stack().gap_2().children(states.clone().map(|state| {
333                                    v_stack()
334                                        .gap_1()
335                                        .child(
336                                            Label::new(state.to_string()).color(LabelColor::Muted),
337                                        )
338                                        .child(
339                                            Button::new("Label")
340                                                .variant(ButtonVariant::Filled)
341                                                .icon(Icon::Plus)
342                                                .icon_position(IconPosition::Right), // .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()).color(LabelColor::Muted),
354                                        )
355                                        .child(
356                                            Button::new("Label")
357                                                .variant(ButtonVariant::Filled)
358                                                // .state(state)
359                                                .width(Some(rems(6.).into())),
360                                        )
361                                })))
362                                .child(Story::label(cx, "Fixed With – Left 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::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()).color(LabelColor::Muted),
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}