button.rs

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