button.rs

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