button.rs

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