button.rs

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