button.rs

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