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>) + Send + Sync>;
 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            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
182            .active(|style| style.bg(self.variant.bg_color_active(cx)));
183
184        match (self.icon, self.icon_position) {
185            (Some(_), Some(IconPosition::Left)) => {
186                button = button
187                    .gap_1()
188                    .child(self.render_label(label_color))
189                    .children(self.render_icon(icon_color))
190            }
191            (Some(_), Some(IconPosition::Right)) => {
192                button = button
193                    .gap_1()
194                    .children(self.render_icon(icon_color))
195                    .child(self.render_label(label_color))
196            }
197            (_, _) => button = button.child(self.render_label(label_color)),
198        }
199
200        if let Some(width) = self.width {
201            button = button.w(width).justify_center();
202        }
203
204        if let Some(click_handler) = self.handlers.click.clone() {
205            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
206                click_handler(state, cx);
207            });
208        }
209
210        button
211    }
212}
213
214#[derive(Component)]
215pub struct ButtonGroup<V: 'static> {
216    buttons: Vec<Button<V>>,
217}
218
219impl<V: 'static> ButtonGroup<V> {
220    pub fn new(buttons: Vec<Button<V>>) -> Self {
221        Self { buttons }
222    }
223
224    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
225        let mut el = h_stack().text_ui();
226
227        for button in self.buttons {
228            el = el.child(button.render(_view, cx));
229        }
230
231        el
232    }
233}
234
235#[cfg(feature = "stories")]
236pub use stories::*;
237
238#[cfg(feature = "stories")]
239mod stories {
240    use super::*;
241    use crate::{h_stack, v_stack, Story, TextColor};
242    use gpui::{rems, Div, Render};
243    use strum::IntoEnumIterator;
244
245    pub struct ButtonStory;
246
247    impl Render for ButtonStory {
248        type Element = Div<Self>;
249
250        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
251            let states = InteractionState::iter();
252
253            Story::container(cx)
254                .child(Story::title_for::<_, Button<Self>>(cx))
255                .child(
256                    div()
257                        .flex()
258                        .gap_8()
259                        .child(
260                            div()
261                                .child(Story::label(cx, "Ghost (Default)"))
262                                .child(h_stack().gap_2().children(states.clone().map(|state| {
263                                    v_stack()
264                                        .gap_1()
265                                        .child(
266                                            Label::new(state.to_string()).color(TextColor::Muted),
267                                        )
268                                        .child(
269                                            Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
270                                        )
271                                })))
272                                .child(Story::label(cx, "Ghost – Left Icon"))
273                                .child(h_stack().gap_2().children(states.clone().map(|state| {
274                                    v_stack()
275                                        .gap_1()
276                                        .child(
277                                            Label::new(state.to_string()).color(TextColor::Muted),
278                                        )
279                                        .child(
280                                            Button::new("Label")
281                                                .variant(ButtonVariant::Ghost)
282                                                .icon(Icon::Plus)
283                                                .icon_position(IconPosition::Left), // .state(state),
284                                        )
285                                })))
286                                .child(Story::label(cx, "Ghost – Right Icon"))
287                                .child(h_stack().gap_2().children(states.clone().map(|state| {
288                                    v_stack()
289                                        .gap_1()
290                                        .child(
291                                            Label::new(state.to_string()).color(TextColor::Muted),
292                                        )
293                                        .child(
294                                            Button::new("Label")
295                                                .variant(ButtonVariant::Ghost)
296                                                .icon(Icon::Plus)
297                                                .icon_position(IconPosition::Right), // .state(state),
298                                        )
299                                }))),
300                        )
301                        .child(
302                            div()
303                                .child(Story::label(cx, "Filled"))
304                                .child(h_stack().gap_2().children(states.clone().map(|state| {
305                                    v_stack()
306                                        .gap_1()
307                                        .child(
308                                            Label::new(state.to_string()).color(TextColor::Muted),
309                                        )
310                                        .child(
311                                            Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
312                                        )
313                                })))
314                                .child(Story::label(cx, "Filled – Left Button"))
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(TextColor::Muted),
320                                        )
321                                        .child(
322                                            Button::new("Label")
323                                                .variant(ButtonVariant::Filled)
324                                                .icon(Icon::Plus)
325                                                .icon_position(IconPosition::Left), // .state(state),
326                                        )
327                                })))
328                                .child(Story::label(cx, "Filled – Right Button"))
329                                .child(h_stack().gap_2().children(states.clone().map(|state| {
330                                    v_stack()
331                                        .gap_1()
332                                        .child(
333                                            Label::new(state.to_string()).color(TextColor::Muted),
334                                        )
335                                        .child(
336                                            Button::new("Label")
337                                                .variant(ButtonVariant::Filled)
338                                                .icon(Icon::Plus)
339                                                .icon_position(IconPosition::Right), // .state(state),
340                                        )
341                                }))),
342                        )
343                        .child(
344                            div()
345                                .child(Story::label(cx, "Fixed With"))
346                                .child(h_stack().gap_2().children(states.clone().map(|state| {
347                                    v_stack()
348                                        .gap_1()
349                                        .child(
350                                            Label::new(state.to_string()).color(TextColor::Muted),
351                                        )
352                                        .child(
353                                            Button::new("Label")
354                                                .variant(ButtonVariant::Filled)
355                                                // .state(state)
356                                                .width(Some(rems(6.).into())),
357                                        )
358                                })))
359                                .child(Story::label(cx, "Fixed With – Left Icon"))
360                                .child(h_stack().gap_2().children(states.clone().map(|state| {
361                                    v_stack()
362                                        .gap_1()
363                                        .child(
364                                            Label::new(state.to_string()).color(TextColor::Muted),
365                                        )
366                                        .child(
367                                            Button::new("Label")
368                                                .variant(ButtonVariant::Filled)
369                                                // .state(state)
370                                                .icon(Icon::Plus)
371                                                .icon_position(IconPosition::Left)
372                                                .width(Some(rems(6.).into())),
373                                        )
374                                })))
375                                .child(Story::label(cx, "Fixed With – Right Icon"))
376                                .child(h_stack().gap_2().children(states.clone().map(|state| {
377                                    v_stack()
378                                        .gap_1()
379                                        .child(
380                                            Label::new(state.to_string()).color(TextColor::Muted),
381                                        )
382                                        .child(
383                                            Button::new("Label")
384                                                .variant(ButtonVariant::Filled)
385                                                // .state(state)
386                                                .icon(Icon::Plus)
387                                                .icon_position(IconPosition::Right)
388                                                .width(Some(rems(6.).into())),
389                                        )
390                                }))),
391                        ),
392                )
393                .child(Story::label(cx, "Button with `on_click`"))
394                .child(
395                    Button::new("Label")
396                        .variant(ButtonVariant::Ghost)
397                        .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
398                )
399        }
400    }
401}