button.rs

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