button.rs

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