button.rs

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