button.rs

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