button.rs

  1use std::rc::Rc;
  2
  3use gpui::{
  4    DefiniteLength, Div, Hsla, MouseButton, MouseDownEvent, RenderOnce, StatefulInteractiveElement,
  5    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
 67#[derive(RenderOnce)]
 68pub struct Button {
 69    disabled: bool,
 70    click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
 71    icon: Option<Icon>,
 72    icon_position: Option<IconPosition>,
 73    label: SharedString,
 74    variant: ButtonVariant,
 75    width: Option<DefiniteLength>,
 76    color: Option<TextColor>,
 77}
 78
 79impl Component for Button {
 80    type Rendered = gpui::Stateful<Div>;
 81
 82    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
 83        let (icon_color, label_color) = match (self.disabled, self.color) {
 84            (true, _) => (TextColor::Disabled, TextColor::Disabled),
 85            (_, None) => (TextColor::Default, TextColor::Default),
 86            (_, Some(color)) => (TextColor::from(color), color),
 87        };
 88
 89        let mut button = h_stack()
 90            .id(SharedString::from(format!("{}", self.label)))
 91            .relative()
 92            .p_1()
 93            .text_ui()
 94            .rounded_md()
 95            .bg(self.variant.bg_color(cx))
 96            .cursor_pointer()
 97            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
 98            .active(|style| style.bg(self.variant.bg_color_active(cx)));
 99
100        match (self.icon, self.icon_position) {
101            (Some(_), Some(IconPosition::Left)) => {
102                button = button
103                    .gap_1()
104                    .child(self.render_label(label_color))
105                    .children(self.render_icon(icon_color))
106            }
107            (Some(_), Some(IconPosition::Right)) => {
108                button = button
109                    .gap_1()
110                    .children(self.render_icon(icon_color))
111                    .child(self.render_label(label_color))
112            }
113            (_, _) => button = button.child(self.render_label(label_color)),
114        }
115
116        if let Some(width) = self.width {
117            button = button.w(width).justify_center();
118        }
119
120        if let Some(click_handler) = self.click_handler.clone() {
121            button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
122                click_handler(event, cx);
123            });
124        }
125
126        button
127    }
128}
129
130impl Button {
131    pub fn new(label: impl Into<SharedString>) -> Self {
132        Self {
133            disabled: false,
134            click_handler: None,
135            icon: None,
136            icon_position: None,
137            label: label.into(),
138            variant: Default::default(),
139            width: Default::default(),
140            color: None,
141        }
142    }
143
144    pub fn ghost(label: impl Into<SharedString>) -> Self {
145        Self::new(label).variant(ButtonVariant::Ghost)
146    }
147
148    pub fn variant(mut self, variant: ButtonVariant) -> Self {
149        self.variant = variant;
150        self
151    }
152
153    pub fn icon(mut self, icon: Icon) -> Self {
154        self.icon = Some(icon);
155        self
156    }
157
158    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
159        if self.icon.is_none() {
160            panic!("An icon must be present if an icon_position is provided.");
161        }
162        self.icon_position = Some(icon_position);
163        self
164    }
165
166    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
167        self.width = width;
168        self
169    }
170
171    pub fn on_click(
172        mut self,
173        handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
174    ) -> Self {
175        self.click_handler = Some(Rc::new(handler));
176        self
177    }
178
179    pub fn disabled(mut self, disabled: bool) -> Self {
180        self.disabled = disabled;
181        self
182    }
183
184    pub fn color(mut self, color: Option<TextColor>) -> Self {
185        self.color = color;
186        self
187    }
188
189    pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
190        if self.disabled {
191            TextColor::Disabled
192        } else if let Some(color) = color {
193            color
194        } else {
195            Default::default()
196        }
197    }
198
199    fn render_label(&self, color: TextColor) -> Label {
200        Label::new(self.label.clone())
201            .color(color)
202            .line_height_style(LineHeightStyle::UILabel)
203    }
204
205    fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
206        self.icon.map(|i| IconElement::new(i).color(icon_color))
207    }
208}
209
210#[derive(RenderOnce)]
211pub struct ButtonGroup {
212    buttons: Vec<Button>,
213}
214
215impl Component for ButtonGroup {
216    type Rendered = Div;
217
218    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
219        let mut group = h_stack();
220
221        for button in self.buttons.into_iter() {
222            group = group.child(button.render(cx));
223        }
224
225        group
226    }
227}
228
229impl ButtonGroup {
230    pub fn new(buttons: Vec<Button>) -> Self {
231        Self { buttons }
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;
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>(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(|_, cx| println!("Button clicked.")),
398                )
399        }
400    }
401}