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            Default::default()
133        }
134    }
135
136    fn icon_color(&self) -> IconColor {
137        if self.disabled {
138            IconColor::Disabled
139        } else {
140            Default::default()
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").variant(ButtonVariant::Ghost), // .state(state),
276                                        )
277                                })))
278                                .child(Story::label(cx, "Ghost – Left Icon"))
279                                .child(h_stack().gap_2().children(states.clone().map(|state| {
280                                    v_stack()
281                                        .gap_1()
282                                        .child(
283                                            Label::new(state.to_string()).color(LabelColor::Muted),
284                                        )
285                                        .child(
286                                            Button::new("Label")
287                                                .variant(ButtonVariant::Ghost)
288                                                .icon(Icon::Plus)
289                                                .icon_position(IconPosition::Left), // .state(state),
290                                        )
291                                })))
292                                .child(Story::label(cx, "Ghost – Right Icon"))
293                                .child(h_stack().gap_2().children(states.clone().map(|state| {
294                                    v_stack()
295                                        .gap_1()
296                                        .child(
297                                            Label::new(state.to_string()).color(LabelColor::Muted),
298                                        )
299                                        .child(
300                                            Button::new("Label")
301                                                .variant(ButtonVariant::Ghost)
302                                                .icon(Icon::Plus)
303                                                .icon_position(IconPosition::Right), // .state(state),
304                                        )
305                                }))),
306                        )
307                        .child(
308                            div()
309                                .child(Story::label(cx, "Filled"))
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").variant(ButtonVariant::Filled), // .state(state),
318                                        )
319                                })))
320                                .child(Story::label(cx, "Filled – Left Button"))
321                                .child(h_stack().gap_2().children(states.clone().map(|state| {
322                                    v_stack()
323                                        .gap_1()
324                                        .child(
325                                            Label::new(state.to_string()).color(LabelColor::Muted),
326                                        )
327                                        .child(
328                                            Button::new("Label")
329                                                .variant(ButtonVariant::Filled)
330                                                .icon(Icon::Plus)
331                                                .icon_position(IconPosition::Left), // .state(state),
332                                        )
333                                })))
334                                .child(Story::label(cx, "Filled – Right Button"))
335                                .child(h_stack().gap_2().children(states.clone().map(|state| {
336                                    v_stack()
337                                        .gap_1()
338                                        .child(
339                                            Label::new(state.to_string()).color(LabelColor::Muted),
340                                        )
341                                        .child(
342                                            Button::new("Label")
343                                                .variant(ButtonVariant::Filled)
344                                                .icon(Icon::Plus)
345                                                .icon_position(IconPosition::Right), // .state(state),
346                                        )
347                                }))),
348                        )
349                        .child(
350                            div()
351                                .child(Story::label(cx, "Fixed With"))
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                                                .width(Some(rems(6.).into())),
363                                        )
364                                })))
365                                .child(Story::label(cx, "Fixed With – Left Icon"))
366                                .child(h_stack().gap_2().children(states.clone().map(|state| {
367                                    v_stack()
368                                        .gap_1()
369                                        .child(
370                                            Label::new(state.to_string()).color(LabelColor::Muted),
371                                        )
372                                        .child(
373                                            Button::new("Label")
374                                                .variant(ButtonVariant::Filled)
375                                                // .state(state)
376                                                .icon(Icon::Plus)
377                                                .icon_position(IconPosition::Left)
378                                                .width(Some(rems(6.).into())),
379                                        )
380                                })))
381                                .child(Story::label(cx, "Fixed With – Right Icon"))
382                                .child(h_stack().gap_2().children(states.clone().map(|state| {
383                                    v_stack()
384                                        .gap_1()
385                                        .child(
386                                            Label::new(state.to_string()).color(LabelColor::Muted),
387                                        )
388                                        .child(
389                                            Button::new("Label")
390                                                .variant(ButtonVariant::Filled)
391                                                // .state(state)
392                                                .icon(Icon::Plus)
393                                                .icon_position(IconPosition::Right)
394                                                .width(Some(rems(6.).into())),
395                                        )
396                                }))),
397                        ),
398                )
399                .child(Story::label(cx, "Button with `on_click`"))
400                .child(
401                    Button::new("Label")
402                        .variant(ButtonVariant::Ghost)
403                        .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
404                )
405        }
406    }
407}