button.rs

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