button.rs

  1use std::sync::Arc;
  2
  3use gpui::{
  4    DefiniteLength, Div, Hsla, MouseButton, RenderOnce, Stateful, 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<V: 'static> {
 14    Button(Button<V>),
 15    IconButton(IconButton<V>),
 16}
 17
 18impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
 19    fn from(value: Button<V>) -> Self {
 20        Self::Button(value)
 21    }
 22}
 23
 24impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
 25    fn from(value: IconButton<V>) -> 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 type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
 68
 69struct ButtonHandlers<V: 'static> {
 70    click: Option<ClickHandler<V>>,
 71}
 72
 73unsafe impl<S> Send for ButtonHandlers<S> {}
 74unsafe impl<S> Sync for ButtonHandlers<S> {}
 75
 76impl<V: 'static> Default for ButtonHandlers<V> {
 77    fn default() -> Self {
 78        Self { click: None }
 79    }
 80}
 81
 82#[derive(RenderOnce)]
 83pub struct Button<V: 'static> {
 84    disabled: bool,
 85    handlers: ButtonHandlers<V>,
 86    icon: Option<Icon>,
 87    icon_position: Option<IconPosition>,
 88    label: SharedString,
 89    variant: ButtonVariant,
 90    width: Option<DefiniteLength>,
 91    color: Option<TextColor>,
 92}
 93
 94impl<V: 'static> Component<V> for Button<V> {
 95    type Rendered = Stateful<V, Div<V>>;
 96
 97    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
 98        let _view: &mut V = view;
 99        let (icon_color, label_color) = match (self.disabled, self.color) {
100            (true, _) => (TextColor::Disabled, TextColor::Disabled),
101            (_, None) => (TextColor::Default, TextColor::Default),
102            (_, Some(color)) => (TextColor::from(color), color),
103        };
104
105        let mut button = h_stack()
106            .id(SharedString::from(format!("{}", self.label)))
107            .relative()
108            .p_1()
109            .text_ui()
110            .rounded_md()
111            .bg(self.variant.bg_color(cx))
112            .cursor_pointer()
113            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
114            .active(|style| style.bg(self.variant.bg_color_active(cx)));
115
116        match (self.icon, self.icon_position) {
117            (Some(_), Some(IconPosition::Left)) => {
118                button = button
119                    .gap_1()
120                    .child(self.render_label(label_color))
121                    .children(self.render_icon(icon_color))
122            }
123            (Some(_), Some(IconPosition::Right)) => {
124                button = button
125                    .gap_1()
126                    .children(self.render_icon(icon_color))
127                    .child(self.render_label(label_color))
128            }
129            (_, _) => button = button.child(self.render_label(label_color)),
130        }
131
132        if let Some(width) = self.width {
133            button = button.w(width).justify_center();
134        }
135
136        if let Some(click_handler) = self.handlers.click.clone() {
137            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
138                click_handler(state, cx);
139            });
140        }
141
142        button
143    }
144}
145
146impl<V: 'static> Button<V> {
147    pub fn new(label: impl Into<SharedString>) -> Self {
148        Self {
149            disabled: false,
150            handlers: ButtonHandlers::default(),
151            icon: None,
152            icon_position: None,
153            label: label.into(),
154            variant: Default::default(),
155            width: Default::default(),
156            color: None,
157        }
158    }
159
160    pub fn ghost(label: impl Into<SharedString>) -> Self {
161        Self::new(label).variant(ButtonVariant::Ghost)
162    }
163
164    pub fn variant(mut self, variant: ButtonVariant) -> Self {
165        self.variant = variant;
166        self
167    }
168
169    pub fn icon(mut self, icon: Icon) -> Self {
170        self.icon = Some(icon);
171        self
172    }
173
174    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
175        if self.icon.is_none() {
176            panic!("An icon must be present if an icon_position is provided.");
177        }
178        self.icon_position = Some(icon_position);
179        self
180    }
181
182    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
183        self.width = width;
184        self
185    }
186
187    pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
188        self.handlers.click = Some(handler);
189        self
190    }
191
192    pub fn disabled(mut self, disabled: bool) -> Self {
193        self.disabled = disabled;
194        self
195    }
196
197    pub fn color(mut self, color: Option<TextColor>) -> Self {
198        self.color = color;
199        self
200    }
201
202    pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
203        if self.disabled {
204            TextColor::Disabled
205        } else if let Some(color) = color {
206            color
207        } else {
208            Default::default()
209        }
210    }
211
212    fn render_label(&self, color: TextColor) -> Label {
213        Label::new(self.label.clone())
214            .color(color)
215            .line_height_style(LineHeightStyle::UILabel)
216    }
217
218    fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
219        self.icon.map(|i| IconElement::new(i).color(icon_color))
220    }
221
222    pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
223        let (icon_color, label_color) = match (self.disabled, self.color) {
224            (true, _) => (TextColor::Disabled, TextColor::Disabled),
225            (_, None) => (TextColor::Default, TextColor::Default),
226            (_, Some(color)) => (TextColor::from(color), color),
227        };
228
229        let mut button = h_stack()
230            .id(SharedString::from(format!("{}", self.label)))
231            .relative()
232            .p_1()
233            .text_ui()
234            .rounded_md()
235            .bg(self.variant.bg_color(cx))
236            .cursor_pointer()
237            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
238            .active(|style| style.bg(self.variant.bg_color_active(cx)));
239
240        match (self.icon, self.icon_position) {
241            (Some(_), Some(IconPosition::Left)) => {
242                button = button
243                    .gap_1()
244                    .child(self.render_label(label_color))
245                    .children(self.render_icon(icon_color))
246            }
247            (Some(_), Some(IconPosition::Right)) => {
248                button = button
249                    .gap_1()
250                    .children(self.render_icon(icon_color))
251                    .child(self.render_label(label_color))
252            }
253            (_, _) => button = button.child(self.render_label(label_color)),
254        }
255
256        if let Some(width) = self.width {
257            button = button.w(width).justify_center();
258        }
259
260        if let Some(click_handler) = self.handlers.click.clone() {
261            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
262                click_handler(state, cx);
263            });
264        }
265
266        button
267    }
268}
269
270#[derive(RenderOnce)]
271pub struct ButtonGroup<V: 'static> {
272    buttons: Vec<Button<V>>,
273}
274
275impl<V: 'static> Component<V> for ButtonGroup<V> {
276    type Rendered = Div<V>;
277
278    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
279        let mut group = h_stack();
280
281        for button in self.buttons.into_iter() {
282            group = group.child(button.render(view, cx));
283        }
284
285        group
286    }
287}
288
289impl<V: 'static> ButtonGroup<V> {
290    pub fn new(buttons: Vec<Button<V>>) -> Self {
291        Self { buttons }
292    }
293}
294
295#[cfg(feature = "stories")]
296pub use stories::*;
297
298#[cfg(feature = "stories")]
299mod stories {
300    use super::*;
301    use crate::{h_stack, v_stack, Story, TextColor};
302    use gpui::{rems, Div, Render};
303    use strum::IntoEnumIterator;
304
305    pub struct ButtonStory;
306
307    impl Render<Self> for ButtonStory {
308        type Element = Div<Self>;
309
310        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
311            let states = InteractionState::iter();
312
313            Story::container(cx)
314                .child(Story::title_for::<_, Button<Self>>(cx))
315                .child(
316                    div()
317                        .flex()
318                        .gap_8()
319                        .child(
320                            div()
321                                .child(Story::label(cx, "Ghost (Default)"))
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(TextColor::Muted),
327                                        )
328                                        .child(
329                                            Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
330                                        )
331                                })))
332                                .child(Story::label(cx, "Ghost – Left Icon"))
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::Ghost)
342                                                .icon(Icon::Plus)
343                                                .icon_position(IconPosition::Left), // .state(state),
344                                        )
345                                })))
346                                .child(Story::label(cx, "Ghost – Right 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::Ghost)
356                                                .icon(Icon::Plus)
357                                                .icon_position(IconPosition::Right), // .state(state),
358                                        )
359                                }))),
360                        )
361                        .child(
362                            div()
363                                .child(Story::label(cx, "Filled"))
364                                .child(h_stack().gap_2().children(states.clone().map(|state| {
365                                    v_stack()
366                                        .gap_1()
367                                        .child(
368                                            Label::new(state.to_string()).color(TextColor::Muted),
369                                        )
370                                        .child(
371                                            Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
372                                        )
373                                })))
374                                .child(Story::label(cx, "Filled – Left Button"))
375                                .child(h_stack().gap_2().children(states.clone().map(|state| {
376                                    v_stack()
377                                        .gap_1()
378                                        .child(
379                                            Label::new(state.to_string()).color(TextColor::Muted),
380                                        )
381                                        .child(
382                                            Button::new("Label")
383                                                .variant(ButtonVariant::Filled)
384                                                .icon(Icon::Plus)
385                                                .icon_position(IconPosition::Left), // .state(state),
386                                        )
387                                })))
388                                .child(Story::label(cx, "Filled – Right Button"))
389                                .child(h_stack().gap_2().children(states.clone().map(|state| {
390                                    v_stack()
391                                        .gap_1()
392                                        .child(
393                                            Label::new(state.to_string()).color(TextColor::Muted),
394                                        )
395                                        .child(
396                                            Button::new("Label")
397                                                .variant(ButtonVariant::Filled)
398                                                .icon(Icon::Plus)
399                                                .icon_position(IconPosition::Right), // .state(state),
400                                        )
401                                }))),
402                        )
403                        .child(
404                            div()
405                                .child(Story::label(cx, "Fixed With"))
406                                .child(h_stack().gap_2().children(states.clone().map(|state| {
407                                    v_stack()
408                                        .gap_1()
409                                        .child(
410                                            Label::new(state.to_string()).color(TextColor::Muted),
411                                        )
412                                        .child(
413                                            Button::new("Label")
414                                                .variant(ButtonVariant::Filled)
415                                                // .state(state)
416                                                .width(Some(rems(6.).into())),
417                                        )
418                                })))
419                                .child(Story::label(cx, "Fixed With – Left Icon"))
420                                .child(h_stack().gap_2().children(states.clone().map(|state| {
421                                    v_stack()
422                                        .gap_1()
423                                        .child(
424                                            Label::new(state.to_string()).color(TextColor::Muted),
425                                        )
426                                        .child(
427                                            Button::new("Label")
428                                                .variant(ButtonVariant::Filled)
429                                                // .state(state)
430                                                .icon(Icon::Plus)
431                                                .icon_position(IconPosition::Left)
432                                                .width(Some(rems(6.).into())),
433                                        )
434                                })))
435                                .child(Story::label(cx, "Fixed With – Right Icon"))
436                                .child(h_stack().gap_2().children(states.clone().map(|state| {
437                                    v_stack()
438                                        .gap_1()
439                                        .child(
440                                            Label::new(state.to_string()).color(TextColor::Muted),
441                                        )
442                                        .child(
443                                            Button::new("Label")
444                                                .variant(ButtonVariant::Filled)
445                                                // .state(state)
446                                                .icon(Icon::Plus)
447                                                .icon_position(IconPosition::Right)
448                                                .width(Some(rems(6.).into())),
449                                        )
450                                }))),
451                        ),
452                )
453                .child(Story::label(cx, "Button with `on_click`"))
454                .child(
455                    Button::new("Label")
456                        .variant(ButtonVariant::Ghost)
457                        .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
458                )
459        }
460    }
461}