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
234impl Toggleable for Button {
235    /// Sets the selected state of the button.
236    ///
237    /// # Examples
238    ///
239    /// Create a toggleable button that changes appearance when selected:
240    ///
241    /// ```
242    /// use ui::prelude::*;
243    /// use ui::TintColor;
244    ///
245    /// let selected = true;
246    ///
247    /// Button::new("toggle_button", "Toggle Me")
248    ///     .start_icon(Icon::new(IconName::Check))
249    ///     .toggle_state(selected)
250    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
251    ///     .on_click(|event, window, cx| {
252    ///         // Toggle the selected state
253    ///     });
254    /// ```
255    fn toggle_state(mut self, selected: bool) -> Self {
256        self.base = self.base.toggle_state(selected);
257        self
258    }
259}
260
261impl SelectableButton for Button {
262    /// Sets the style for the button in a selected state.
263    ///
264    /// # Examples
265    ///
266    /// Customize the selected appearance of a button:
267    ///
268    /// ```
269    /// use ui::prelude::*;
270    /// use ui::TintColor;
271    ///
272    /// Button::new("styled_button", "Styled Button")
273    ///     .toggle_state(true)
274    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent));
275    /// ```
276    fn selected_style(mut self, style: ButtonStyle) -> Self {
277        self.base = self.base.selected_style(style);
278        self
279    }
280}
281
282impl Disableable for Button {
283    /// Disables the button, preventing interaction and changing its appearance.
284    ///
285    /// When disabled, the button's icon and label will use `Color::Disabled`.
286    ///
287    /// # Examples
288    ///
289    /// Create a disabled button:
290    ///
291    /// ```
292    /// use ui::prelude::*;
293    ///
294    /// Button::new("disabled_button", "Can't Click Me")
295    ///     .disabled(true);
296    /// ```
297    fn disabled(mut self, disabled: bool) -> Self {
298        self.base = self.base.disabled(disabled);
299        self
300    }
301}
302
303impl Clickable for Button {
304    fn on_click(
305        mut self,
306        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
307    ) -> Self {
308        self.base = self.base.on_click(handler);
309        self
310    }
311
312    fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
313        self.base = self.base.cursor_style(cursor_style);
314        self
315    }
316}
317
318impl FixedWidth for Button {
319    /// Sets a fixed width for the button.
320    ///
321    /// # Examples
322    ///
323    /// Create a button with a fixed width of 100 pixels:
324    ///
325    /// ```
326    /// use ui::prelude::*;
327    ///
328    /// Button::new("fixed_width_button", "Fixed Width")
329    ///     .width(px(100.0));
330    /// ```
331    fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
332        self.base = self.base.width(width);
333        self
334    }
335
336    /// Makes the button take up the full width of its container.
337    ///
338    /// # Examples
339    ///
340    /// Create a button that takes up the full width of its container:
341    ///
342    /// ```
343    /// use ui::prelude::*;
344    ///
345    /// Button::new("full_width_button", "Full Width")
346    ///     .full_width();
347    /// ```
348    fn full_width(mut self) -> Self {
349        self.base = self.base.full_width();
350        self
351    }
352}
353
354impl ButtonCommon for Button {
355    fn id(&self) -> &ElementId {
356        self.base.id()
357    }
358
359    /// Sets the visual style of the button.
360    fn style(mut self, style: ButtonStyle) -> Self {
361        self.base = self.base.style(style);
362        self
363    }
364
365    /// Sets the size of the button.
366    fn size(mut self, size: ButtonSize) -> Self {
367        self.base = self.base.size(size);
368        self
369    }
370
371    /// Sets a tooltip that appears on hover.
372    ///
373    /// # Examples
374    ///
375    /// Add a tooltip to a button:
376    ///
377    /// ```
378    /// use ui::{Tooltip, prelude::*};
379    ///
380    /// Button::new("tooltip_button", "Hover Me")
381    ///     .tooltip(Tooltip::text("This is a tooltip"));
382    /// ```
383    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
384        self.base = self.base.tooltip(tooltip);
385        self
386    }
387
388    fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
389        self.base = self.base.tab_index(tab_index);
390        self
391    }
392
393    fn layer(mut self, elevation: ElevationIndex) -> Self {
394        self.base = self.base.layer(elevation);
395        self
396    }
397
398    fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
399        self.base = self.base.track_focus(focus_handle);
400        self
401    }
402}
403
404impl RenderOnce for Button {
405    #[allow(refining_impl_trait)]
406    fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
407        let is_disabled = self.base.disabled;
408        let is_selected = self.base.selected;
409        let button_style = self.base.style;
410
411        let label = self
412            .selected_label
413            .filter(|_| is_selected)
414            .unwrap_or(self.label);
415
416        let label_color = if is_disabled {
417            Color::Disabled
418        } else if is_selected {
419            self.selected_label_color.unwrap_or(Color::Selected)
420        } else if let Some(color) = self.label_color {
421            color
422        } else if matches!(button_style, ButtonStyle::Tinted(_)) {
423            // Solid Tinted backgrounds (primary/danger/etc.) need light text
424            // for contrast — applies to every `.style(Tinted(..))` caller, not
425            // just the `.primary()`/`.danger()` sugar.
426            Color::Custom(gpui::white())
427        } else {
428            Color::default()
429        };
430
431        self.base.child(
432            h_flex()
433                .when(self.truncate, |this| this.min_w_0().overflow_hidden())
434                .gap(DynamicSpacing::Base04.rems(cx))
435                .when(self.loading, |this| {
436                    this.child(
437                        Icon::new(IconName::LoadCircle)
438                            .size(IconSize::Small)
439                            .color(Color::Muted)
440                            .with_rotate_animation(2),
441                    )
442                })
443                .when(!self.loading, |this| {
444                    this.when_some(self.start_icon, |this, icon| {
445                        this.child(if is_disabled {
446                            icon.color(Color::Disabled)
447                        } else if matches!(button_style, ButtonStyle::Tinted(_)) {
448                            icon.color(Color::Custom(gpui::white()))
449                        } else {
450                            icon
451                        })
452                    })
453                })
454                .child(
455                    h_flex()
456                        .when(self.truncate, |this| this.min_w_0().overflow_hidden())
457                        .when(
458                            self.key_binding_position == KeybindingPosition::Start,
459                            |this| this.flex_row_reverse(),
460                        )
461                        .gap(DynamicSpacing::Base06.rems(cx))
462                        .justify_between()
463                        .child(
464                            Label::new(label)
465                                .color(label_color)
466                                .size(self.label_size.unwrap_or_default())
467                                .when_some(self.alpha, |this, alpha| this.alpha(alpha))
468                                .when(self.truncate, |this| this.truncate()),
469                        )
470                        .children(self.key_binding),
471                )
472                .when_some(self.end_icon, |this, icon| {
473                    this.child(if is_disabled {
474                        icon.color(Color::Disabled)
475                    } else {
476                        icon
477                    })
478                }),
479        )
480    }
481}
482
483impl Component for Button {
484    fn scope() -> ComponentScope {
485        ComponentScope::Input
486    }
487
488    fn sort_name() -> &'static str {
489        "ButtonA"
490    }
491
492    fn description() -> Option<&'static str> {
493        Some("A button triggers an event or action.")
494    }
495
496    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
497        Some(
498            v_flex()
499                .gap_6()
500                .children(vec![
501                    example_group_with_title(
502                        "Button Styles",
503                        vec![
504                            single_example(
505                                "Default",
506                                Button::new("default", "Default").into_any_element(),
507                            ),
508                            single_example(
509                                "Filled",
510                                Button::new("filled", "Filled")
511                                    .style(ButtonStyle::Filled)
512                                    .into_any_element(),
513                            ),
514                            single_example(
515                                "Subtle",
516                                Button::new("outline", "Subtle")
517                                    .style(ButtonStyle::Subtle)
518                                    .into_any_element(),
519                            ),
520                            single_example(
521                                "Tinted",
522                                Button::new("tinted_accent_style", "Accent")
523                                    .style(ButtonStyle::Tinted(TintColor::Accent))
524                                    .into_any_element(),
525                            ),
526                            single_example(
527                                "Transparent",
528                                Button::new("transparent", "Transparent")
529                                    .style(ButtonStyle::Transparent)
530                                    .into_any_element(),
531                            ),
532                        ],
533                    ),
534                    example_group_with_title(
535                        "Primary / Danger / Soft",
536                        vec![
537                            single_example(
538                                "Primary",
539                                Button::new("primary", "Primary")
540                                    .primary()
541                                    .into_any_element(),
542                            ),
543                            single_example(
544                                "Danger",
545                                Button::new("danger", "Danger").danger().into_any_element(),
546                            ),
547                            single_example(
548                                "Soft",
549                                Button::new("soft", "Soft").soft().into_any_element(),
550                            ),
551                        ],
552                    ),
553                    example_group_with_title(
554                        "Variant \u{d7} Size Matrix",
555                        vec![
556                            single_example(
557                                "Large",
558                                h_flex()
559                                    .gap_2()
560                                    .child(
561                                        Button::new("matrix_primary_lg", "Primary")
562                                            .primary()
563                                            .size(ButtonSize::Large),
564                                    )
565                                    .child(
566                                        Button::new("matrix_danger_lg", "Danger")
567                                            .danger()
568                                            .size(ButtonSize::Large),
569                                    )
570                                    .child(
571                                        Button::new("matrix_soft_lg", "Soft")
572                                            .soft()
573                                            .size(ButtonSize::Large),
574                                    )
575                                    .into_any_element(),
576                            ),
577                            single_example(
578                                "Medium",
579                                h_flex()
580                                    .gap_2()
581                                    .child(
582                                        Button::new("matrix_primary_md", "Primary")
583                                            .primary()
584                                            .size(ButtonSize::Medium),
585                                    )
586                                    .child(
587                                        Button::new("matrix_danger_md", "Danger")
588                                            .danger()
589                                            .size(ButtonSize::Medium),
590                                    )
591                                    .child(
592                                        Button::new("matrix_soft_md", "Soft")
593                                            .soft()
594                                            .size(ButtonSize::Medium),
595                                    )
596                                    .into_any_element(),
597                            ),
598                            single_example(
599                                "Compact",
600                                h_flex()
601                                    .gap_2()
602                                    .child(
603                                        Button::new("matrix_primary_cp", "Primary")
604                                            .primary()
605                                            .size(ButtonSize::Compact),
606                                    )
607                                    .child(
608                                        Button::new("matrix_danger_cp", "Danger")
609                                            .danger()
610                                            .size(ButtonSize::Compact),
611                                    )
612                                    .child(
613                                        Button::new("matrix_soft_cp", "Soft")
614                                            .soft()
615                                            .size(ButtonSize::Compact),
616                                    )
617                                    .into_any_element(),
618                            ),
619                        ],
620                    ),
621                    example_group_with_title(
622                        "Tint Styles",
623                        vec![
624                            single_example(
625                                "Accent",
626                                Button::new("tinted_accent", "Accent")
627                                    .style(ButtonStyle::Tinted(TintColor::Accent))
628                                    .into_any_element(),
629                            ),
630                            single_example(
631                                "Error",
632                                Button::new("tinted_negative", "Error")
633                                    .style(ButtonStyle::Tinted(TintColor::Error))
634                                    .into_any_element(),
635                            ),
636                            single_example(
637                                "Warning",
638                                Button::new("tinted_warning", "Warning")
639                                    .style(ButtonStyle::Tinted(TintColor::Warning))
640                                    .into_any_element(),
641                            ),
642                            single_example(
643                                "Success",
644                                Button::new("tinted_positive", "Success")
645                                    .style(ButtonStyle::Tinted(TintColor::Success))
646                                    .into_any_element(),
647                            ),
648                        ],
649                    ),
650                    example_group_with_title(
651                        "Special States",
652                        vec![
653                            single_example(
654                                "Default",
655                                Button::new("default_state", "Default").into_any_element(),
656                            ),
657                            single_example(
658                                "Disabled",
659                                Button::new("disabled", "Disabled")
660                                    .disabled(true)
661                                    .into_any_element(),
662                            ),
663                            single_example(
664                                "Selected",
665                                Button::new("selected", "Selected")
666                                    .toggle_state(true)
667                                    .into_any_element(),
668                            ),
669                        ],
670                    ),
671                    example_group_with_title(
672                        "Buttons with Icons",
673                        vec![
674                            single_example(
675                                "Start Icon",
676                                Button::new("icon_start", "Start Icon")
677                                    .start_icon(Icon::new(IconName::Check))
678                                    .into_any_element(),
679                            ),
680                            single_example(
681                                "End Icon",
682                                Button::new("icon_end", "End Icon")
683                                    .end_icon(Icon::new(IconName::Check))
684                                    .into_any_element(),
685                            ),
686                            single_example(
687                                "Both Icons",
688                                Button::new("both_icons", "Both Icons")
689                                    .start_icon(Icon::new(IconName::Check))
690                                    .end_icon(Icon::new(IconName::ChevronDown))
691                                    .into_any_element(),
692                            ),
693                            single_example(
694                                "Icon Color",
695                                Button::new("icon_color", "Icon Color")
696                                    .start_icon(Icon::new(IconName::Check).color(Color::Accent))
697                                    .into_any_element(),
698                            ),
699                        ],
700                    ),
701                ])
702                .into_any_element(),
703        )
704    }
705}