Skip to main content

ui/components/button/
button.rs

1use crate::component_prelude::*;
2use gpui::{AnyElement, AnyView, DefiniteLength};
3use ui_macros::RegisterComponent;
4
5use crate::traits::animation_ext::CommonAnimationExt;
6use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, Label};
7use crate::{
8    Color, DynamicSpacing, ElevationIndex, KeyBinding, KeybindingPosition, TintColor, prelude::*,
9};
10
11/// An element that creates a button with a label and optional icons.
12///
13/// Common buttons:
14/// - Label, Icon + Label: [`Button`] (this component)
15/// - Icon only: [`IconButton`]
16/// - Custom: [`ButtonLike`]
17///
18/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use
19/// [`ButtonLike`] directly.
20///
21/// # Examples
22///
23/// **A button with a label**, is typically used in scenarios such as a form, where the button's label
24/// indicates what action will be performed when the button is clicked.
25///
26/// ```
27/// use ui::prelude::*;
28///
29/// Button::new("button_id", "Click me!")
30///     .on_click(|event, window, cx| {
31///         // Handle click event
32///     });
33/// ```
34///
35/// **A toggleable button**, is typically used in scenarios such as a toolbar,
36/// where the button's state indicates whether a feature is enabled or not, or
37/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu.
38///
39/// ```
40/// use ui::prelude::*;
41///
42/// Button::new("button_id", "Click me!")
43///     .start_icon(Icon::new(IconName::Check))
44///     .toggle_state(true)
45///     .on_click(|event, window, cx| {
46///         // Handle click event
47///     });
48/// ```
49///
50/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method.
51///
52/// ```
53/// use ui::prelude::*;
54/// use ui::TintColor;
55///
56/// Button::new("button_id", "Click me!")
57///     .toggle_state(true)
58///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
59///     .on_click(|event, window, cx| {
60///         // Handle click event
61///     });
62/// ```
63/// This will create a button with a blue tinted background when selected.
64///
65/// **A full-width button**, is typically used in scenarios such as the bottom of a modal or form, where it occupies the entire width of its container.
66/// The button's content, including text and icons, is centered by default.
67///
68/// ```
69/// use ui::prelude::*;
70///
71/// let button = Button::new("button_id", "Click me!")
72///     .full_width()
73///     .on_click(|event, window, cx| {
74///         // Handle click event
75///     });
76/// ```
77///
78#[derive(IntoElement, Documented, RegisterComponent)]
79pub struct Button {
80    base: ButtonLike,
81    label: SharedString,
82    label_color: Option<Color>,
83    label_size: Option<LabelSize>,
84    selected_label: Option<SharedString>,
85    selected_label_color: Option<Color>,
86    start_icon: Option<Icon>,
87    end_icon: Option<Icon>,
88    key_binding: Option<KeyBinding>,
89    key_binding_position: KeybindingPosition,
90    alpha: Option<f32>,
91    truncate: bool,
92    loading: bool,
93}
94
95impl Button {
96    /// Creates a new [`Button`] with a specified identifier and label.
97    ///
98    /// This is the primary constructor for a [`Button`] component. It initializes
99    /// the button with the provided identifier and label text, setting all other
100    /// properties to their default values, which can be customized using the
101    /// builder pattern methods provided by this struct.
102    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
103        Self {
104            base: ButtonLike::new(id),
105            label: label.into(),
106            label_color: None,
107            label_size: None,
108            selected_label: None,
109            selected_label_color: None,
110            start_icon: None,
111            end_icon: None,
112            key_binding: None,
113            key_binding_position: KeybindingPosition::default(),
114            alpha: None,
115            truncate: false,
116            loading: false,
117        }
118    }
119
120    /// Sets the color of the button's label.
121    pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
122        self.label_color = label_color.into();
123        self
124    }
125
126    /// Defines the size of the button's label.
127    pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
128        self.label_size = label_size.into();
129        self
130    }
131
132    /// Sets the label used when the button is in a selected state.
133    pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
134        self.selected_label = label.into().map(Into::into);
135        self
136    }
137
138    /// Sets the label color used when the button is in a selected state.
139    pub fn selected_label_color(mut self, color: impl Into<Option<Color>>) -> Self {
140        self.selected_label_color = color.into();
141        self
142    }
143
144    /// Sets an icon to display at the start (left) of the button label.
145    ///
146    /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
147    pub fn start_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
148        self.start_icon = icon.into();
149        self
150    }
151
152    /// Sets an icon to display at the end (right) of the button label.
153    ///
154    /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
155    pub fn end_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
156        self.end_icon = icon.into();
157        self
158    }
159
160    /// Display the keybinding that triggers the button action.
161    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
162        self.key_binding = key_binding.into();
163        self
164    }
165
166    /// Sets the position of the keybinding relative to the button label.
167    ///
168    /// This method allows you to specify where the keybinding should be displayed
169    /// in relation to the button's label.
170    pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
171        self.key_binding_position = position;
172        self
173    }
174
175    /// Sets the alpha property of the color of label.
176    pub fn alpha(mut self, alpha: f32) -> Self {
177        self.alpha = Some(alpha);
178        self
179    }
180
181    /// Truncates overflowing labels with an ellipsis (`…`) if needed.
182    ///
183    /// Buttons with static labels should _never_ be truncated, ensure
184    /// this is only used when the label is dynamic and may overflow.
185    pub fn truncate(mut self, truncate: bool) -> Self {
186        self.truncate = truncate;
187        self
188    }
189
190    /// Displays a rotating loading spinner in place of the `start_icon`.
191    ///
192    /// When `loading` is `true`, any `start_icon` is ignored. and a rotating
193    pub fn loading(mut self, loading: bool) -> Self {
194        self.loading = loading;
195        self
196    }
197
198    /// Convenience for a Tailwind-style solid "primary" button: solid
199    /// `palette::primary(600)` background with white text.
200    ///
201    /// Equivalent to `.style(ButtonStyle::Tinted(TintColor::Accent))`.
202    pub fn primary(mut self) -> Self {
203        self.base = self.base.style(ButtonStyle::Tinted(TintColor::Accent));
204        self.label_color = Some(Color::Custom(gpui::white()));
205        self
206    }
207
208    /// Convenience for a Tailwind-style solid "danger" button: solid
209    /// `palette::danger(600)` background with white text.
210    ///
211    /// Equivalent to `.style(ButtonStyle::Tinted(TintColor::Error))`.
212    pub fn danger(mut self) -> Self {
213        self.base = self.base.style(ButtonStyle::Tinted(TintColor::Error));
214        self.label_color = Some(Color::Custom(gpui::white()));
215        self
216    }
217
218    /// Convenience for a Tailwind-style "soft" button: faint
219    /// `palette::primary(50)` background with `palette::primary(700)` text.
220    ///
221    /// Implemented as an additive background override on top of the
222    /// `Tinted(Accent)` style, so it does not require a new `ButtonStyle`
223    /// variant.
224    pub fn soft(mut self) -> Self {
225        self.base = self
226            .base
227            .style(ButtonStyle::Tinted(TintColor::Accent))
228            .background_override(palette::primary(50));
229        self.label_color = Some(Color::Custom(palette::primary(700)));
230        self
231    }
232
233    /// shadcn/ui variant alias (recommended vocabulary).
234    ///
235    /// Maps to existing [`ButtonStyle`] / convenience builders without
236    /// renaming legacy `.primary()` / `.danger()` / `.soft()` call sites.
237    /// For link-style buttons use [`ButtonLink`] instead.
238    pub fn variant(mut self, variant: ButtonVariant) -> Self {
239        match variant {
240            ButtonVariant::Default => self.primary(),
241            ButtonVariant::Destructive => self.danger(),
242            ButtonVariant::Outline => {
243                self.base = self.base.style(ButtonStyle::Outlined);
244                self.label_color = None;
245                self
246            }
247            ButtonVariant::Secondary => {
248                self.base = self.base.style(ButtonStyle::Filled);
249                self.label_color = None;
250                self
251            }
252            ButtonVariant::Ghost => {
253                self.base = self.base.style(ButtonStyle::Transparent);
254                self.label_color = None;
255                self
256            }
257        }
258    }
259
260    /// shadcn/ui size alias. Icon-only sizing uses [`IconButton`], not this API.
261    pub fn shadcn_size(mut self, size: ButtonSizeAlias) -> Self {
262        let mapped = match size {
263            ButtonSizeAlias::Sm => ButtonSize::Compact,
264            ButtonSizeAlias::Default => ButtonSize::Default,
265            ButtonSizeAlias::Lg => ButtonSize::Medium,
266            ButtonSizeAlias::Icon => ButtonSize::None,
267        };
268        self.base = self.base.size(mapped);
269        self
270    }
271}
272
273/// shadcn/ui button variants (additive alias over [`ButtonStyle`]).
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
275pub enum ButtonVariant {
276    #[default]
277    Default,
278    Destructive,
279    Outline,
280    Secondary,
281    Ghost,
282}
283
284/// shadcn/ui button sizes (additive alias over [`ButtonSize`]).
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
286pub enum ButtonSizeAlias {
287    Sm,
288    #[default]
289    Default,
290    Lg,
291    Icon,
292}
293
294impl Toggleable for Button {
295    /// Sets the selected state of the button.
296    ///
297    /// # Examples
298    ///
299    /// Create a toggleable button that changes appearance when selected:
300    ///
301    /// ```
302    /// use ui::prelude::*;
303    /// use ui::TintColor;
304    ///
305    /// let selected = true;
306    ///
307    /// Button::new("toggle_button", "Toggle Me")
308    ///     .start_icon(Icon::new(IconName::Check))
309    ///     .toggle_state(selected)
310    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
311    ///     .on_click(|event, window, cx| {
312    ///         // Toggle the selected state
313    ///     });
314    /// ```
315    fn toggle_state(mut self, selected: bool) -> Self {
316        self.base = self.base.toggle_state(selected);
317        self
318    }
319}
320
321impl SelectableButton for Button {
322    /// Sets the style for the button in a selected state.
323    ///
324    /// # Examples
325    ///
326    /// Customize the selected appearance of a button:
327    ///
328    /// ```
329    /// use ui::prelude::*;
330    /// use ui::TintColor;
331    ///
332    /// Button::new("styled_button", "Styled Button")
333    ///     .toggle_state(true)
334    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent));
335    /// ```
336    fn selected_style(mut self, style: ButtonStyle) -> Self {
337        self.base = self.base.selected_style(style);
338        self
339    }
340}
341
342impl Disableable for Button {
343    /// Disables the button, preventing interaction and changing its appearance.
344    ///
345    /// When disabled, the button's icon and label will use `Color::Disabled`.
346    ///
347    /// # Examples
348    ///
349    /// Create a disabled button:
350    ///
351    /// ```
352    /// use ui::prelude::*;
353    ///
354    /// Button::new("disabled_button", "Can't Click Me")
355    ///     .disabled(true);
356    /// ```
357    fn disabled(mut self, disabled: bool) -> Self {
358        self.base = self.base.disabled(disabled);
359        self
360    }
361}
362
363impl Clickable for Button {
364    fn on_click(
365        mut self,
366        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
367    ) -> Self {
368        self.base = self.base.on_click(handler);
369        self
370    }
371
372    fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
373        self.base = self.base.cursor_style(cursor_style);
374        self
375    }
376}
377
378impl FixedWidth for Button {
379    /// Sets a fixed width for the button.
380    ///
381    /// # Examples
382    ///
383    /// Create a button with a fixed width of 100 pixels:
384    ///
385    /// ```
386    /// use ui::prelude::*;
387    ///
388    /// Button::new("fixed_width_button", "Fixed Width")
389    ///     .width(px(100.0));
390    /// ```
391    fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
392        self.base = self.base.width(width);
393        self
394    }
395
396    /// Makes the button take up the full width of its container.
397    ///
398    /// # Examples
399    ///
400    /// Create a button that takes up the full width of its container:
401    ///
402    /// ```
403    /// use ui::prelude::*;
404    ///
405    /// Button::new("full_width_button", "Full Width")
406    ///     .full_width();
407    /// ```
408    fn full_width(mut self) -> Self {
409        self.base = self.base.full_width();
410        self
411    }
412}
413
414impl ButtonCommon for Button {
415    fn id(&self) -> &ElementId {
416        self.base.id()
417    }
418
419    /// Sets the visual style of the button.
420    fn style(mut self, style: ButtonStyle) -> Self {
421        self.base = self.base.style(style);
422        self
423    }
424
425    /// Sets the size of the button.
426    fn size(mut self, size: ButtonSize) -> Self {
427        self.base = self.base.size(size);
428        self
429    }
430
431    /// Sets a tooltip that appears on hover.
432    ///
433    /// # Examples
434    ///
435    /// Add a tooltip to a button:
436    ///
437    /// ```
438    /// use ui::{Tooltip, prelude::*};
439    ///
440    /// Button::new("tooltip_button", "Hover Me")
441    ///     .tooltip(Tooltip::text("This is a tooltip"));
442    /// ```
443    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
444        self.base = self.base.tooltip(tooltip);
445        self
446    }
447
448    fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
449        self.base = self.base.tab_index(tab_index);
450        self
451    }
452
453    fn layer(mut self, elevation: ElevationIndex) -> Self {
454        self.base = self.base.layer(elevation);
455        self
456    }
457
458    fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
459        self.base = self.base.track_focus(focus_handle);
460        self
461    }
462}
463
464impl RenderOnce for Button {
465    #[allow(refining_impl_trait)]
466    fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
467        let is_disabled = self.base.disabled;
468        let is_selected = self.base.selected;
469        let button_style = self.base.style;
470
471        let label = self
472            .selected_label
473            .filter(|_| is_selected)
474            .unwrap_or(self.label);
475
476        let label_color = if is_disabled {
477            Color::Disabled
478        } else if is_selected {
479            self.selected_label_color.unwrap_or(Color::Selected)
480        } else if let Some(color) = self.label_color {
481            color
482        } else if matches!(button_style, ButtonStyle::Tinted(_)) {
483            // Solid Tinted backgrounds (primary/danger/etc.) need light text
484            // for contrast — applies to every `.style(Tinted(..))` caller, not
485            // just the `.primary()`/`.danger()` sugar.
486            Color::Custom(gpui::white())
487        } else {
488            Color::default()
489        };
490
491        self.base.child(
492            h_flex()
493                .when(self.truncate, |this| this.min_w_0().overflow_hidden())
494                .gap(DynamicSpacing::Base04.rems(cx))
495                .when(self.loading, |this| {
496                    this.child(
497                        Icon::new(IconName::LoadCircle)
498                            .size(IconSize::Small)
499                            .color(Color::Muted)
500                            .with_rotate_animation(2),
501                    )
502                })
503                .when(!self.loading, |this| {
504                    this.when_some(self.start_icon, |this, icon| {
505                        this.child(if is_disabled {
506                            icon.color(Color::Disabled)
507                        } else if matches!(button_style, ButtonStyle::Tinted(_)) {
508                            icon.color(Color::Custom(gpui::white()))
509                        } else {
510                            icon
511                        })
512                    })
513                })
514                .child(
515                    h_flex()
516                        .when(self.truncate, |this| this.min_w_0().overflow_hidden())
517                        .when(
518                            self.key_binding_position == KeybindingPosition::Start,
519                            |this| this.flex_row_reverse(),
520                        )
521                        .gap(DynamicSpacing::Base06.rems(cx))
522                        .justify_between()
523                        .child(
524                            Label::new(label)
525                                .color(label_color)
526                                .size(self.label_size.unwrap_or_default())
527                                .when_some(self.alpha, |this, alpha| this.alpha(alpha))
528                                .when(self.truncate, |this| this.truncate()),
529                        )
530                        .children(self.key_binding),
531                )
532                .when_some(self.end_icon, |this, icon| {
533                    this.child(if is_disabled {
534                        icon.color(Color::Disabled)
535                    } else {
536                        icon
537                    })
538                }),
539        )
540    }
541}
542
543impl Component for Button {
544    fn scope() -> ComponentScope {
545        ComponentScope::Input
546    }
547
548    fn sort_name() -> &'static str {
549        "ButtonA"
550    }
551
552    fn description() -> Option<&'static str> {
553        Some("A button triggers an event or action.")
554    }
555
556    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
557        Some(
558            v_flex()
559                .gap_6()
560                .children(vec![
561                    example_group_with_title(
562                        "Button Styles",
563                        vec![
564                            single_example(
565                                "Default",
566                                Button::new("default", "Default").into_any_element(),
567                            ),
568                            single_example(
569                                "Filled",
570                                Button::new("filled", "Filled")
571                                    .style(ButtonStyle::Filled)
572                                    .into_any_element(),
573                            ),
574                            single_example(
575                                "Subtle",
576                                Button::new("outline", "Subtle")
577                                    .style(ButtonStyle::Subtle)
578                                    .into_any_element(),
579                            ),
580                            single_example(
581                                "Tinted",
582                                Button::new("tinted_accent_style", "Accent")
583                                    .style(ButtonStyle::Tinted(TintColor::Accent))
584                                    .into_any_element(),
585                            ),
586                            single_example(
587                                "Transparent",
588                                Button::new("transparent", "Transparent")
589                                    .style(ButtonStyle::Transparent)
590                                    .into_any_element(),
591                            ),
592                        ],
593                    ),
594                    example_group_with_title(
595                        "Primary / Danger / Soft",
596                        vec![
597                            single_example(
598                                "Primary",
599                                Button::new("primary", "Primary")
600                                    .primary()
601                                    .into_any_element(),
602                            ),
603                            single_example(
604                                "Danger",
605                                Button::new("danger", "Danger").danger().into_any_element(),
606                            ),
607                            single_example(
608                                "Soft",
609                                Button::new("soft", "Soft").soft().into_any_element(),
610                            ),
611                        ],
612                    ),
613                    example_group_with_title(
614                        "Variant \u{d7} Size Matrix",
615                        vec![
616                            single_example(
617                                "Large",
618                                h_flex()
619                                    .gap_2()
620                                    .child(
621                                        Button::new("matrix_primary_lg", "Primary")
622                                            .primary()
623                                            .size(ButtonSize::Large),
624                                    )
625                                    .child(
626                                        Button::new("matrix_danger_lg", "Danger")
627                                            .danger()
628                                            .size(ButtonSize::Large),
629                                    )
630                                    .child(
631                                        Button::new("matrix_soft_lg", "Soft")
632                                            .soft()
633                                            .size(ButtonSize::Large),
634                                    )
635                                    .into_any_element(),
636                            ),
637                            single_example(
638                                "Medium",
639                                h_flex()
640                                    .gap_2()
641                                    .child(
642                                        Button::new("matrix_primary_md", "Primary")
643                                            .primary()
644                                            .size(ButtonSize::Medium),
645                                    )
646                                    .child(
647                                        Button::new("matrix_danger_md", "Danger")
648                                            .danger()
649                                            .size(ButtonSize::Medium),
650                                    )
651                                    .child(
652                                        Button::new("matrix_soft_md", "Soft")
653                                            .soft()
654                                            .size(ButtonSize::Medium),
655                                    )
656                                    .into_any_element(),
657                            ),
658                            single_example(
659                                "Compact",
660                                h_flex()
661                                    .gap_2()
662                                    .child(
663                                        Button::new("matrix_primary_cp", "Primary")
664                                            .primary()
665                                            .size(ButtonSize::Compact),
666                                    )
667                                    .child(
668                                        Button::new("matrix_danger_cp", "Danger")
669                                            .danger()
670                                            .size(ButtonSize::Compact),
671                                    )
672                                    .child(
673                                        Button::new("matrix_soft_cp", "Soft")
674                                            .soft()
675                                            .size(ButtonSize::Compact),
676                                    )
677                                    .into_any_element(),
678                            ),
679                        ],
680                    ),
681                    example_group_with_title(
682                        "Tint Styles",
683                        vec![
684                            single_example(
685                                "Accent",
686                                Button::new("tinted_accent", "Accent")
687                                    .style(ButtonStyle::Tinted(TintColor::Accent))
688                                    .into_any_element(),
689                            ),
690                            single_example(
691                                "Error",
692                                Button::new("tinted_negative", "Error")
693                                    .style(ButtonStyle::Tinted(TintColor::Error))
694                                    .into_any_element(),
695                            ),
696                            single_example(
697                                "Warning",
698                                Button::new("tinted_warning", "Warning")
699                                    .style(ButtonStyle::Tinted(TintColor::Warning))
700                                    .into_any_element(),
701                            ),
702                            single_example(
703                                "Success",
704                                Button::new("tinted_positive", "Success")
705                                    .style(ButtonStyle::Tinted(TintColor::Success))
706                                    .into_any_element(),
707                            ),
708                        ],
709                    ),
710                    example_group_with_title(
711                        "Special States",
712                        vec![
713                            single_example(
714                                "Default",
715                                Button::new("default_state", "Default").into_any_element(),
716                            ),
717                            single_example(
718                                "Disabled",
719                                Button::new("disabled", "Disabled")
720                                    .disabled(true)
721                                    .into_any_element(),
722                            ),
723                            single_example(
724                                "Selected",
725                                Button::new("selected", "Selected")
726                                    .toggle_state(true)
727                                    .into_any_element(),
728                            ),
729                        ],
730                    ),
731                    example_group_with_title(
732                        "Buttons with Icons",
733                        vec![
734                            single_example(
735                                "Start Icon",
736                                Button::new("icon_start", "Start Icon")
737                                    .start_icon(Icon::new(IconName::Check))
738                                    .into_any_element(),
739                            ),
740                            single_example(
741                                "End Icon",
742                                Button::new("icon_end", "End Icon")
743                                    .end_icon(Icon::new(IconName::Check))
744                                    .into_any_element(),
745                            ),
746                            single_example(
747                                "Both Icons",
748                                Button::new("both_icons", "Both Icons")
749                                    .start_icon(Icon::new(IconName::Check))
750                                    .end_icon(Icon::new(IconName::ChevronDown))
751                                    .into_any_element(),
752                            ),
753                            single_example(
754                                "Icon Color",
755                                Button::new("icon_color", "Icon Color")
756                                    .start_icon(Icon::new(IconName::Check).color(Color::Accent))
757                                    .into_any_element(),
758                            ),
759                        ],
760                    ),
761                ])
762                .into_any_element(),
763        )
764    }
765}