button.rs

  1use std::marker::PhantomData;
  2use std::rc::Rc;
  3
  4use gpui3::{DefiniteLength, Hsla, 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
 23// struct ButtonHandlers<V> {
 24//     click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
 25// }
 26
 27// impl<V> Default for ButtonHandlers<V> {
 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(mut self, handler: impl Fn(&mut S, &mut EventContext<S>) + 'static) -> Self {
 98    //     self.handlers.click = Some(Rc::new(handler));
 99    //     self
100    // }
101
102    fn background_color(&self, cx: &mut ViewContext<S>) -> Hsla {
103        let theme = theme(cx);
104        let system_color = SystemColor::new();
105
106        match (self.variant, self.state) {
107            (ButtonVariant::Ghost, InteractionState::Hovered) => {
108                theme.lowest.base.hovered.background
109            }
110            (ButtonVariant::Ghost, InteractionState::Active) => {
111                theme.lowest.base.pressed.background
112            }
113            (ButtonVariant::Filled, InteractionState::Enabled) => {
114                theme.lowest.on.default.background
115            }
116            (ButtonVariant::Filled, InteractionState::Hovered) => {
117                theme.lowest.on.hovered.background
118            }
119            (ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background,
120            (ButtonVariant::Filled, InteractionState::Disabled) => {
121                theme.lowest.on.disabled.background
122            }
123            _ => system_color.transparent,
124        }
125    }
126
127    fn label_color(&self) -> LabelColor {
128        match self.state {
129            InteractionState::Disabled => LabelColor::Disabled,
130            _ => Default::default(),
131        }
132    }
133
134    fn icon_color(&self) -> IconColor {
135        match self.state {
136            InteractionState::Disabled => IconColor::Disabled,
137            _ => Default::default(),
138        }
139    }
140
141    fn border_color(&self, cx: &WindowContext) -> Hsla {
142        let theme = theme(cx);
143        let system_color = SystemColor::new();
144
145        match self.state {
146            InteractionState::Focused => theme.lowest.accent.default.border,
147            _ => system_color.transparent,
148        }
149    }
150
151    fn render_label(&self) -> Label<S> {
152        Label::new(self.label.clone())
153            .size(LabelSize::Small)
154            .color(self.label_color())
155    }
156
157    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement<S>> {
158        self.icon.map(|i| IconElement::new(i).color(icon_color))
159    }
160
161    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
162        let theme = theme(cx);
163        let icon_color = self.icon_color();
164        let system_color = SystemColor::new();
165        let border_color = self.border_color(cx);
166
167        let mut el = h_stack()
168            .h_6()
169            .px_1()
170            .items_center()
171            .rounded_md()
172            .border()
173            .border_color(border_color)
174            .fill(self.background_color(cx));
175
176        match (self.icon, self.icon_position) {
177            (Some(_), Some(IconPosition::Left)) => {
178                el = el
179                    .gap_1()
180                    .child(self.render_label())
181                    .children(self.render_icon(icon_color))
182            }
183            (Some(_), Some(IconPosition::Right)) => {
184                el = el
185                    .gap_1()
186                    .children(self.render_icon(icon_color))
187                    .child(self.render_label())
188            }
189            (_, _) => el = el.child(self.render_label()),
190        }
191
192        if let Some(width) = self.width {
193            el = el.w(width).justify_center();
194        }
195
196        // if let Some(click_handler) = self.handlers.click.clone() {
197        //     el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| {
198        //         click_handler(view, cx);
199        //     });
200        // }
201
202        el
203    }
204}
205
206#[cfg(feature = "stories")]
207pub use stories::*;
208
209#[cfg(feature = "stories")]
210mod stories {
211    use gpui3::rems;
212    use strum::IntoEnumIterator;
213
214    use crate::{h_stack, v_stack, LabelColor, LabelSize, Story};
215
216    use super::*;
217
218    #[derive(Element)]
219    pub struct ButtonStory<S: 'static + Send + Sync + Clone> {
220        state_type: PhantomData<S>,
221    }
222
223    impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
224        pub fn new() -> Self {
225            Self {
226                state_type: PhantomData,
227            }
228        }
229
230        fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
231            let states = InteractionState::iter();
232
233            Story::container(cx)
234                .child(Story::title_for::<_, Button<S>>(cx))
235                .child(
236                    div()
237                        .flex()
238                        .gap_8()
239                        .child(
240                            div()
241                                .child(Story::label(cx, "Ghost (Default)"))
242                                .child(h_stack().gap_2().children(states.clone().map(|state| {
243                                    v_stack()
244                                        .gap_1()
245                                        .child(
246                                            Label::new(state.to_string())
247                                                .color(LabelColor::Muted)
248                                                .size(LabelSize::Small),
249                                        )
250                                        .child(
251                                            Button::new("Label")
252                                                .variant(ButtonVariant::Ghost)
253                                                .state(state),
254                                        )
255                                })))
256                                .child(Story::label(cx, "Ghost – Left Icon"))
257                                .child(h_stack().gap_2().children(states.clone().map(|state| {
258                                    v_stack()
259                                        .gap_1()
260                                        .child(
261                                            Label::new(state.to_string())
262                                                .color(LabelColor::Muted)
263                                                .size(LabelSize::Small),
264                                        )
265                                        .child(
266                                            Button::new("Label")
267                                                .variant(ButtonVariant::Ghost)
268                                                .icon(Icon::Plus)
269                                                .icon_position(IconPosition::Left)
270                                                .state(state),
271                                        )
272                                })))
273                                .child(Story::label(cx, "Ghost – Right Icon"))
274                                .child(h_stack().gap_2().children(states.clone().map(|state| {
275                                    v_stack()
276                                        .gap_1()
277                                        .child(
278                                            Label::new(state.to_string())
279                                                .color(LabelColor::Muted)
280                                                .size(LabelSize::Small),
281                                        )
282                                        .child(
283                                            Button::new("Label")
284                                                .variant(ButtonVariant::Ghost)
285                                                .icon(Icon::Plus)
286                                                .icon_position(IconPosition::Right)
287                                                .state(state),
288                                        )
289                                }))),
290                        )
291                        .child(
292                            div()
293                                .child(Story::label(cx, "Filled"))
294                                .child(h_stack().gap_2().children(states.clone().map(|state| {
295                                    v_stack()
296                                        .gap_1()
297                                        .child(
298                                            Label::new(state.to_string())
299                                                .color(LabelColor::Muted)
300                                                .size(LabelSize::Small),
301                                        )
302                                        .child(
303                                            Button::new("Label")
304                                                .variant(ButtonVariant::Filled)
305                                                .state(state),
306                                        )
307                                })))
308                                .child(Story::label(cx, "Filled – Left Button"))
309                                .child(h_stack().gap_2().children(states.clone().map(|state| {
310                                    v_stack()
311                                        .gap_1()
312                                        .child(
313                                            Label::new(state.to_string())
314                                                .color(LabelColor::Muted)
315                                                .size(LabelSize::Small),
316                                        )
317                                        .child(
318                                            Button::new("Label")
319                                                .variant(ButtonVariant::Filled)
320                                                .icon(Icon::Plus)
321                                                .icon_position(IconPosition::Left)
322                                                .state(state),
323                                        )
324                                })))
325                                .child(Story::label(cx, "Filled – Right Button"))
326                                .child(h_stack().gap_2().children(states.clone().map(|state| {
327                                    v_stack()
328                                        .gap_1()
329                                        .child(
330                                            Label::new(state.to_string())
331                                                .color(LabelColor::Muted)
332                                                .size(LabelSize::Small),
333                                        )
334                                        .child(
335                                            Button::new("Label")
336                                                .variant(ButtonVariant::Filled)
337                                                .icon(Icon::Plus)
338                                                .icon_position(IconPosition::Right)
339                                                .state(state),
340                                        )
341                                }))),
342                        )
343                        .child(
344                            div()
345                                .child(Story::label(cx, "Fixed With"))
346                                .child(h_stack().gap_2().children(states.clone().map(|state| {
347                                    v_stack()
348                                        .gap_1()
349                                        .child(
350                                            Label::new(state.to_string())
351                                                .color(LabelColor::Muted)
352                                                .size(LabelSize::Small),
353                                        )
354                                        .child(
355                                            Button::new("Label")
356                                                .variant(ButtonVariant::Filled)
357                                                .state(state)
358                                                .width(Some(rems(6.).into())),
359                                        )
360                                })))
361                                .child(Story::label(cx, "Fixed With – Left Icon"))
362                                .child(h_stack().gap_2().children(states.clone().map(|state| {
363                                    v_stack()
364                                        .gap_1()
365                                        .child(
366                                            Label::new(state.to_string())
367                                                .color(LabelColor::Muted)
368                                                .size(LabelSize::Small),
369                                        )
370                                        .child(
371                                            Button::new("Label")
372                                                .variant(ButtonVariant::Filled)
373                                                .state(state)
374                                                .icon(Icon::Plus)
375                                                .icon_position(IconPosition::Left)
376                                                .width(Some(rems(6.).into())),
377                                        )
378                                })))
379                                .child(Story::label(cx, "Fixed With – Right Icon"))
380                                .child(h_stack().gap_2().children(states.clone().map(|state| {
381                                    v_stack()
382                                        .gap_1()
383                                        .child(
384                                            Label::new(state.to_string())
385                                                .color(LabelColor::Muted)
386                                                .size(LabelSize::Small),
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").variant(ButtonVariant::Ghost), // NOTE: There currently appears to be a bug in GPUI2 where only the last event handler will fire.
402                                                                        // So adding additional buttons with `on_click`s after this one will cause this `on_click` to not fire.
403                                                                        // .on_click(|_view, _cx| println!("Button clicked.")),
404                )
405        }
406    }
407}