Skip to main content

gpui_component/button/
button.rs

1use std::rc::Rc;
2
3use crate::ThemeStyled as _;
4use crate::{
5    ActiveTheme, Colorize as _, Disableable, Icon, Placement, RoleOverride, Selectable, Sizable,
6    Size, StyleSized, StyledExt,
7    button::ButtonIcon,
8    h_flex,
9    select::Caret,
10    tooltip::{ManagedTooltipExt as _, Tooltip},
11};
12use gpui::{
13    AnyElement, App, Background, ClickEvent, Corners, Edges, ElementId, Hsla, InteractiveElement,
14    Interactivity, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Role, SharedString,
15    StatefulInteractiveElement as _, StyleRefinement, Styled, Window, div,
16    prelude::FluentBuilder as _, relative, transparent_white,
17};
18
19#[derive(Default, Clone, Copy)]
20pub enum ButtonRounded {
21    None,
22    Small,
23    #[default]
24    Medium,
25    Large,
26    Size(Pixels),
27}
28
29impl From<Pixels> for ButtonRounded {
30    fn from(px: Pixels) -> Self {
31        ButtonRounded::Size(px)
32    }
33}
34
35#[derive(Clone, Copy, PartialEq, Eq, Debug)]
36pub struct ButtonCustomVariant {
37    color: Hsla,
38    foreground: Hsla,
39    shadow: bool,
40    hover: Hsla,
41    active: Hsla,
42}
43
44pub trait ButtonVariants: Sized {
45    fn with_variant(self, variant: ButtonVariant) -> Self;
46
47    /// With the primary style for the Button.
48    fn primary(self) -> Self {
49        self.with_variant(ButtonVariant::Primary)
50    }
51
52    /// With the secondary style for the Button.
53    fn secondary(self) -> Self {
54        self.with_variant(ButtonVariant::Secondary)
55    }
56
57    /// With the danger style for the Button.
58    fn danger(self) -> Self {
59        self.with_variant(ButtonVariant::Danger)
60    }
61
62    /// With the warning style for the Button.
63    fn warning(self) -> Self {
64        self.with_variant(ButtonVariant::Warning)
65    }
66
67    /// With the success style for the Button.
68    fn success(self) -> Self {
69        self.with_variant(ButtonVariant::Success)
70    }
71
72    /// With the info style for the Button.
73    fn info(self) -> Self {
74        self.with_variant(ButtonVariant::Info)
75    }
76
77    /// With the ghost style for the Button.
78    fn ghost(self) -> Self {
79        self.with_variant(ButtonVariant::Ghost)
80    }
81
82    /// With the link style for the Button.
83    fn link(self) -> Self {
84        self.with_variant(ButtonVariant::Link)
85    }
86
87    /// With the text style for the Button, it will no padding look like a normal text.
88    fn text(self) -> Self {
89        self.with_variant(ButtonVariant::Text)
90    }
91
92    /// With the custom style for the Button.
93    fn custom(self, style: ButtonCustomVariant) -> Self {
94        self.with_variant(ButtonVariant::Custom(style))
95    }
96}
97
98impl ButtonCustomVariant {
99    pub fn new(cx: &App) -> Self {
100        Self {
101            color: cx.theme().transparent,
102            foreground: cx.theme().foreground,
103            hover: cx.theme().transparent,
104            active: cx.theme().transparent,
105            shadow: false,
106        }
107    }
108
109    /// Set background color, default is transparent.
110    pub fn color(mut self, color: Hsla) -> Self {
111        self.color = color;
112        self
113    }
114
115    /// Set foreground color, default is theme foreground.
116    pub fn foreground(mut self, color: Hsla) -> Self {
117        self.foreground = color;
118        self
119    }
120
121    /// Set hover background color, default is transparent.
122    pub fn hover(mut self, color: Hsla) -> Self {
123        self.hover = color;
124        self
125    }
126
127    /// Set active background color, default is transparent.
128    pub fn active(mut self, color: Hsla) -> Self {
129        self.active = color;
130        self
131    }
132
133    /// Set shadow, default is false.
134    pub fn shadow(mut self, shadow: bool) -> Self {
135        self.shadow = shadow;
136        self
137    }
138}
139
140/// The variant of the Button.
141#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
142pub enum ButtonVariant {
143    #[default]
144    Default,
145    Primary,
146    Secondary,
147    Danger,
148    Info,
149    Success,
150    Warning,
151    Ghost,
152    Link,
153    Text,
154    Custom(ButtonCustomVariant),
155}
156
157impl ButtonVariant {
158    #[inline]
159    pub fn is_link(&self) -> bool {
160        matches!(self, Self::Link)
161    }
162
163    #[inline]
164    pub fn is_text(&self) -> bool {
165        matches!(self, Self::Text)
166    }
167
168    #[inline]
169    pub fn is_ghost(&self) -> bool {
170        matches!(self, Self::Ghost)
171    }
172
173    #[inline]
174    fn no_padding(&self) -> bool {
175        self.is_link() || self.is_text()
176    }
177
178    #[inline]
179    fn is_default(&self) -> bool {
180        matches!(self, Self::Default)
181    }
182}
183
184/// A Button element.
185#[derive(IntoElement)]
186pub struct Button {
187    id: ElementId,
188    base: gpui_base::Button,
189    icon: Option<ButtonIcon>,
190    label: Option<SharedString>,
191    /// The announced name, when the visible content is not it.
192    accessibility_label: Option<SharedString>,
193    children: Vec<AnyElement>,
194    disabled: bool,
195    pub(crate) selected: bool,
196    /// Held by the popover, menu or dropdown this button triggers, for as long
197    /// as it is open. Kept apart from `selected` because the two states mean
198    /// different things, even though they paint the same today.
199    open: bool,
200    toggled: Option<bool>,
201    role: RoleOverride,
202    variant: ButtonVariant,
203    rounded: ButtonRounded,
204    outline: bool,
205    border_corners: Corners<bool>,
206    border_edges: Edges<bool>,
207    dropdown_caret: bool,
208    hover_group: Option<SharedString>,
209    hover_group_held: bool,
210    size: Size,
211    content_style: StyleRefinement,
212    icon_size: Option<Size>,
213    compact: bool,
214    tooltip: Option<(
215        SharedString,
216        Option<(Rc<Box<dyn gpui::Action>>, Option<SharedString>)>,
217    )>,
218    tooltip_placement: Option<Placement>,
219    tooltip_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> gpui::AnyView>>,
220    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
221    on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
222    loading: bool,
223    loading_icon: Option<Icon>,
224    focus_ring_enabled: bool,
225
226    tab_index: isize,
227    tab_stop: bool,
228}
229
230impl From<Button> for AnyElement {
231    fn from(button: Button) -> Self {
232        button.into_any_element()
233    }
234}
235
236impl Button {
237    pub fn new(id: impl Into<ElementId>) -> Self {
238        let id = id.into();
239
240        Self {
241            id: id.clone(),
242            base: gpui_base::Button::new(id),
243            icon: None,
244            label: None,
245            accessibility_label: None,
246            children: Vec::new(),
247            disabled: false,
248            selected: false,
249            open: false,
250            toggled: None,
251            role: RoleOverride::default(),
252            variant: ButtonVariant::default(),
253            rounded: ButtonRounded::Medium,
254            border_corners: Corners {
255                top_left: true,
256                top_right: true,
257                bottom_right: true,
258                bottom_left: true,
259            },
260            border_edges: Edges::all(true),
261            size: Size::Medium,
262            content_style: StyleRefinement::default(),
263            icon_size: None,
264            tooltip: None,
265            tooltip_placement: None,
266            tooltip_builder: None,
267            on_click: None,
268            focus_ring_enabled: true,
269            on_hover: None,
270            loading: false,
271            compact: false,
272            outline: false,
273            loading_icon: None,
274            dropdown_caret: false,
275            hover_group: None,
276            hover_group_held: false,
277            tab_index: 0,
278            tab_stop: true,
279        }
280    }
281
282    /// Uses a behavior primitive supplied by a compound Base control.
283    pub(crate) fn with_base(mut self, base: gpui_base::Button) -> Self {
284        self.base = base;
285        self
286    }
287
288    pub(crate) fn variant(&self) -> ButtonVariant {
289        self.variant
290    }
291
292    /// Presentation supplied by a styled compound control. Standalone buttons
293    /// retain their normal size-derived content style.
294    pub(crate) fn content_style(mut self, style: StyleRefinement, icon_size: Size) -> Self {
295        self.content_style = style;
296        self.icon_size = Some(icon_size);
297        self
298    }
299
300    pub(super) fn button_size(&self) -> Size {
301        self.size
302    }
303
304    pub(crate) fn is_disabled(&self) -> bool {
305        self.disabled
306    }
307
308    pub(crate) fn is_outline(&self) -> bool {
309        self.outline
310    }
311
312    /// Whether the button shows only its icon.
313    pub(crate) fn is_icon_only(&self) -> bool {
314        self.icon.is_some() && self.label.is_none() && self.children.is_empty()
315    }
316
317    pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
318        self.role = role.into();
319        self
320    }
321
322    /// Set the outline style of the Button.
323    pub fn outline(mut self) -> Self {
324        self.outline = true;
325        self
326    }
327
328    /// Set the border radius of the Button.
329    pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self {
330        self.rounded = rounded.into();
331        self
332    }
333
334    /// Set the border corners side of the Button.
335    pub(crate) fn border_corners(mut self, corners: impl Into<Corners<bool>>) -> Self {
336        self.border_corners = corners.into();
337        self
338    }
339
340    /// Set the border edges of the Button.
341    pub(crate) fn border_edges(mut self, edges: impl Into<Edges<bool>>) -> Self {
342        self.border_edges = edges.into();
343        self
344    }
345
346    /// Join a hover group: while any member is hovered, an idle member shows
347    /// its hover surface at half strength, so a composite such as a split
348    /// button reads as one control with the hovered part emphasized.
349    pub(crate) fn hover_group(mut self, group: impl Into<SharedString>) -> Self {
350        self.hover_group = Some(group.into());
351        self
352    }
353
354    /// Keep the hover group's idle surface up without a pointer, for as long as
355    /// the group is held engaged, such as while a sibling's menu is open.
356    pub(crate) fn hover_group_held(mut self, held: bool) -> Self {
357        self.hover_group_held = held;
358        self
359    }
360
361    /// Set label to the Button, if no label is set, the button will be in Icon Button mode.
362    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
363        self.label = Some(label.into());
364        self
365    }
366
367    /// Set the developer-assigned identifier exposed to accessibility clients.
368    pub fn accessibility_id(mut self, id: impl Into<SharedString>) -> Self {
369        self.base = self.base.accessibility_id(id);
370        self
371    }
372
373    /// Set the name a screen reader announces, when the visible content is not
374    /// it.
375    ///
376    /// A button's name comes from its [`label`](Self::label) by default, which
377    /// is right for the ordinary case and wrong for two: an icon-only button has
378    /// no label to read, and a button whose content is a row of cells — a table
379    /// row that is also a control — would be read out cell by cell with no
380    /// statement of what pressing it does. Setting this replaces the announced
381    /// name without adding anything to the screen.
382    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
383        self.accessibility_label = Some(label.into());
384        self
385    }
386
387    /// Set the icon of the button, if the Button have no label, the button well in Icon Button mode.
388    pub fn icon(mut self, icon: impl Into<ButtonIcon>) -> Self {
389        self.icon = Some(icon.into());
390        self
391    }
392
393    /// Set the tooltip of the button.
394    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
395        self.tooltip = Some((tooltip.into(), None));
396        self
397    }
398
399    /// Prefer a side for the tooltip, falling back when it does not fit.
400    ///
401    /// Applies to [`Self::tooltip`] and [`Self::tooltip_with_action`].
402    /// Omitting placement keeps automatic positioning.
403    pub fn tooltip_placement(mut self, placement: Placement) -> Self {
404        self.tooltip_placement = Some(placement);
405        self
406    }
407
408    /// Set the tooltip of the button with action to show keybinding.
409    pub fn tooltip_with_action(
410        mut self,
411        tooltip: impl Into<SharedString>,
412        action: &dyn gpui::Action,
413        context: Option<&str>,
414    ) -> Self {
415        self.tooltip = Some((
416            tooltip.into(),
417            Some((
418                Rc::new(action.boxed_clone()),
419                context.map(|c| c.to_string().into()),
420            )),
421        ));
422        self
423    }
424
425    /// Set true to show the loading indicator.
426    pub fn loading(mut self, loading: bool) -> Self {
427        self.loading = loading;
428        self
429    }
430
431    /// Set the button to compact mode, then padding will be reduced.
432    pub fn compact(mut self) -> Self {
433        self.compact = true;
434        self
435    }
436
437    /// Add click handler.
438    pub fn on_click(
439        mut self,
440        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
441    ) -> Self {
442        self.on_click = Some(Rc::new(handler));
443        self
444    }
445
446    /// Add hover handler, the bool parameter indicates whether the mouse is hovering.
447    pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
448        self.on_hover = Some(Rc::new(handler));
449        self
450    }
451
452    /// Set the loading icon of the button, it will be used when loading is true.
453    ///
454    /// Default is a spinner icon.
455    pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
456        self.loading_icon = Some(icon.into());
457        self
458    }
459
460    /// Set the tab index of the button, it will be used to focus the button by tab key.
461    ///
462    /// Default is 0.
463    pub fn tab_index(mut self, tab_index: isize) -> Self {
464        self.tab_index = tab_index;
465        self
466    }
467
468    /// Set the tab stop of the button, if true, the button will be focusable by tab key.
469    ///
470    /// Default is true.
471    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
472        self.tab_stop = tab_stop;
473        self
474    }
475
476    /// Set to show a dropdown caret icon at the end of the button.
477    pub fn dropdown_caret(mut self, dropdown_caret: bool) -> Self {
478        self.dropdown_caret = dropdown_caret;
479        self
480    }
481
482    /// Expose this button as a toggle button to assistive technology, with
483    /// `toggled` as its pressed state.
484    ///
485    /// Only affects accessibility metadata. Use [`Selectable::selected`] for
486    /// the selected styling, and call this in addition when the button really
487    /// is a toggle, otherwise the button stays an ordinary push button.
488    pub fn toggled(mut self, toggled: bool) -> Self {
489        self.toggled = Some(toggled);
490        self
491    }
492
493    /// Whether the button paints its selected styling, which is what both a
494    /// caller-set selection and an open popup look like today.
495    #[inline]
496    fn shows_selected_style(&self) -> bool {
497        self.selected || self.open
498    }
499
500    /// Whether the button responds to the pointer at all.
501    ///
502    /// A loading button is as inert as a disabled one, it just keeps looking
503    /// like itself instead of taking the disabled styling.
504    #[inline]
505    fn interactive(&self) -> bool {
506        !(self.disabled || self.loading)
507    }
508
509    #[inline]
510    fn hoverable(&self) -> bool {
511        self.interactive() && self.on_hover.is_some()
512    }
513}
514
515impl Disableable for Button {
516    fn disabled(mut self, disabled: bool) -> Self {
517        self.disabled = disabled;
518        self
519    }
520}
521
522impl crate::FocusableExt for Button {
523    fn focus_ring(mut self, enabled: bool) -> Self {
524        self.focus_ring_enabled = enabled;
525        self
526    }
527
528    fn is_focus_ring_enabled(&self) -> bool {
529        self.focus_ring_enabled
530    }
531}
532
533impl Selectable for Button {
534    fn selected(mut self, selected: bool) -> Self {
535        self.selected = selected;
536        self
537    }
538
539    fn is_selected(&self) -> bool {
540        self.selected
541    }
542
543    fn open(mut self, open: bool) -> Self {
544        self.open = open;
545        self
546    }
547
548    fn is_open(&self) -> bool {
549        self.open
550    }
551}
552
553impl Sizable for Button {
554    fn with_size(mut self, size: impl Into<Size>) -> Self {
555        self.size = size.into();
556        self
557    }
558}
559
560impl ButtonVariants for Button {
561    fn with_variant(mut self, variant: ButtonVariant) -> Self {
562        self.variant = variant;
563        self
564    }
565}
566
567impl Styled for Button {
568    fn style(&mut self) -> &mut StyleRefinement {
569        self.base.style()
570    }
571}
572
573impl ParentElement for Button {
574    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
575        self.children.extend(elements)
576    }
577}
578
579impl InteractiveElement for Button {
580    fn interactivity(&mut self) -> &mut Interactivity {
581        self.base.interactivity()
582    }
583}
584
585impl RenderOnce for Button {
586    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
587        let style: ButtonVariant = self.variant;
588        let interactive = self.interactive();
589        let hoverable = self.hoverable();
590        let disabled = self.disabled;
591        let selected = self.shows_selected_style();
592        let loading = self.loading;
593        let tooltip_placement = self.tooltip_placement;
594        let hover_group = self.hover_group;
595        let hover_group_held = self.hover_group_held;
596        let mut base = self.base;
597        let children = self.children;
598        let instance_style = base.style().clone();
599        let normal_style = style.normal(self.outline, cx);
600        let selected_style = style.selected(self.outline, cx);
601        let disabled_style = style.disabled(self.outline, cx);
602        let icon_size = self.icon_size.unwrap_or_else(|| match self.size {
603            Size::Size(v) => Size::Size(v * 0.75),
604            _ => self.size,
605        });
606        let has_content = self.icon.is_some() || self.label.is_some() || !children.is_empty();
607
608        let focus_handle = window
609            .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
610            .read(cx)
611            .clone();
612        let is_focused = focus_handle.is_focused(window);
613
614        let rounding = match self.rounded {
615            ButtonRounded::Small => cx.theme().radius * 0.5,
616            ButtonRounded::Medium => cx.theme().radius,
617            ButtonRounded::Large => cx.theme().radius * 2.0,
618            ButtonRounded::Size(px) => px,
619            ButtonRounded::None => Pixels::ZERO,
620        };
621
622        let root = base
623            .cursor_default()
624            .flex()
625            .flex_shrink_0()
626            .items_center()
627            .justify_center()
628            .cursor_default()
629            .when(
630                interactive && (self.variant.is_link() || self.variant.is_text()),
631                |this| this.cursor_pointer(),
632            )
633            .when(
634                !disabled && cx.theme().shadow && normal_style.shadow,
635                |this| this.shadow_xs(),
636            )
637            .when(!style.no_padding(), |this| {
638                if self.label.is_none() && children.is_empty() {
639                    // Icon Button
640                    match self.size {
641                        Size::Size(px) => this.size(px),
642                        Size::XSmall => this.size_5(),
643                        Size::Small => this.size_6(),
644                        Size::Large | Size::Medium => this.size_8(),
645                    }
646                } else {
647                    // Normal Button
648                    match self.size {
649                        Size::Size(size) => this.px(size * 0.2),
650                        Size::XSmall => this.h_5().px_1().when(self.compact, |this| this.min_w_5()),
651                        Size::Small => this
652                            .h_6()
653                            .px_2()
654                            .when(self.compact, |this| this.min_w_6().px_1p5()),
655                        Size::Medium => this
656                            .h_8()
657                            .px_2p5()
658                            .when(self.compact, |this| this.min_w_8().px_2()),
659                        Size::Large => this
660                            .h_8()
661                            .px_3()
662                            .when(self.compact, |this| this.min_w_8().px_2()),
663                    }
664                }
665            })
666            .when(self.border_corners.top_left, |this| {
667                this.rounded_tl(rounding)
668            })
669            .when(self.border_corners.top_right, |this| {
670                this.rounded_tr(rounding)
671            })
672            .when(self.border_corners.bottom_left, |this| {
673                this.rounded_bl(rounding)
674            })
675            .when(self.border_corners.bottom_right, |this| {
676                this.rounded_br(rounding)
677            })
678            .when(self.variant.is_default() || self.outline, |this| {
679                this.when(self.border_edges.left, |this| this.border_l_1())
680                    .when(self.border_edges.right, |this| this.border_r_1())
681                    .when(self.border_edges.top, |this| this.border_t_1())
682                    .when(self.border_edges.bottom, |this| this.border_b_1())
683            })
684            .when(!self.disabled && !selected, |this| {
685                this.border_color(normal_style.border)
686                    .bg(normal_style.bg)
687                    .text_color(normal_style.fg)
688                    .when(normal_style.underline, |this| this.text_decoration_1())
689                    // A loading button keeps its normal colors, but must not react
690                    // to the pointer, it is not waiting for another click.
691                    .when(interactive, |this| {
692                        this.hover(|this| {
693                            let hover_style = style.hovered(self.outline, cx);
694                            this.bg(hover_style.bg)
695                                .border_color(hover_style.border)
696                                .text_color(hover_style.fg)
697                        })
698                        .active(|this| {
699                            let active_style = style.active(self.outline, cx);
700                            this.bg(active_style.bg)
701                                .border_color(active_style.border)
702                                .text_color(active_style.fg)
703                        })
704                        .when_some(hover_group, |this, group| {
705                            let idle_bg = style.hovered(self.outline, cx).bg.opacity(0.5);
706                            this.when(hover_group_held, |this| this.bg(idle_bg))
707                                .group_hover(group, |this| this.bg(idle_bg))
708                        })
709                    })
710            })
711            .line_height(relative(1.25))
712            .refine_style(&instance_style);
713
714        // The explicit name wins: it exists precisely for the cases where the
715        // visible content is not what a listener needs to hear.
716        let accessibility_label = self
717            .accessibility_label
718            .clone()
719            .or_else(|| self.label.clone());
720        let content = h_flex()
721            .id("label")
722            .size_full()
723            .min_w_0()
724            .overflow_hidden()
725            .whitespace_nowrap()
726            .items_center()
727            .justify_center()
728            .button_text_size(self.size)
729            .map(|this| match self.size {
730                Size::XSmall => this.gap_1(),
731                Size::Small => this.gap_1(),
732                _ => this.gap_2(),
733            })
734            .refine_style(&self.content_style)
735            .when_some(self.icon, |this, icon| {
736                this.child(
737                    icon.loading_icon(self.loading_icon)
738                        .loading(self.loading)
739                        .with_size(icon_size),
740                )
741            })
742            .when_some(self.label, |this, label| {
743                this.child(
744                    div()
745                        .min_w_0()
746                        .whitespace_nowrap()
747                        .text_ellipsis()
748                        .child(label),
749                )
750            })
751            .children(children)
752            .when(self.dropdown_caret, |this| {
753                this.when(has_content, |this| this.justify_between())
754                    .child(Caret::new(self.size).text_color(normal_style.fg.opacity(0.75)))
755            });
756        root.role(self.role.resolve(|| {
757            if self.variant.is_link() {
758                Role::Link
759            } else {
760                Role::Button
761            }
762        }))
763        .selected(selected)
764        .disabled(disabled)
765        // Base layers semantic states over the builder chain, so the caller's
766        // own style is replayed inside each state to keep it the closest layer.
767        .styles(|styles| {
768            styles
769                .selected(|style| {
770                    style
771                        .bg(selected_style.bg)
772                        .border_color(selected_style.border)
773                        .text_color(selected_style.fg)
774                        .refine_style(&instance_style)
775                })
776                .disabled(|style| {
777                    style
778                        .bg(disabled_style.bg)
779                        .text_color(disabled_style.fg)
780                        .border_color(disabled_style.border)
781                        .shadow_none()
782                        .refine_style(&instance_style)
783                })
784        })
785        .when_some(accessibility_label, |this, label| {
786            this.accessibility_label(label)
787        })
788        .when_some(self.toggled, |this, toggled| {
789            this.aria_toggled(if toggled {
790                gpui::accesskit::Toggled::True
791            } else {
792                gpui::accesskit::Toggled::False
793            })
794        })
795        .track_focus(&focus_handle)
796        .tab_index(self.tab_index)
797        .tab_stop(self.tab_stop)
798        .child(content)
799        // Fade the whole button while loading, so every variant is dimmed by
800        // the same amount. Fading `bg`, `border` and `fg` one by one instead
801        // only shows up on variants that have a background to begin with:
802        // `Ghost`, `Link` and `Text` are transparent, so an alpha on their
803        // background changes nothing.
804        .when(loading && !disabled, |this| this.opacity(0.8))
805        .when(!disabled, |this| {
806            this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
807                if loading {
808                    cx.stop_propagation();
809                    return;
810                }
811
812                // Avoid focus on mouse down.
813                window.prevent_default();
814
815                // Pressing a button must not start the window-level text selection.
816                crate::global_state::GlobalState::suppress_text_selection(cx);
817            })
818        })
819        .when_some(self.on_click, |this, on_click| {
820            this.on_click(move |event, window, cx| {
821                if loading {
822                    cx.stop_propagation();
823                    return;
824                }
825
826                on_click(event, window, cx);
827            })
828        })
829        .when(loading, |this| {
830            this.on_click(|_, _, cx| cx.stop_propagation())
831        })
832        .when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
833            this.on_hover(move |hovered, window, cx| {
834                on_hover(hovered, window, cx);
835            })
836        })
837        .map(|this| {
838            if let Some(builder) = self.tooltip_builder {
839                this.managed_tooltip_with_placement(tooltip_placement, move |window, cx| {
840                    builder(window, cx)
841                })
842            } else if let Some((tooltip, action)) = self.tooltip {
843                this.managed_tooltip_with_placement(tooltip_placement, move |window, cx| {
844                    Tooltip::new(tooltip.clone())
845                        .when_some(action.clone(), |this, (action, context)| {
846                            this.action(
847                                action.boxed_clone().as_ref(),
848                                context.as_ref().map(|c| c.as_ref()),
849                            )
850                        })
851                        .build(window, cx)
852                })
853            } else {
854                this
855            }
856        })
857        .when(is_focused && self.focus_ring_enabled, |this| {
858            this.focus_ring_style(window, cx)
859        })
860    }
861}
862
863struct ButtonVariantStyle {
864    bg: Background,
865    border: Hsla,
866    fg: Hsla,
867    underline: bool,
868    shadow: bool,
869}
870
871#[derive(Clone, Copy)]
872enum ButtonStyleState {
873    Normal,
874    Hovered,
875    Active,
876}
877
878impl ButtonVariant {
879    fn outline_background(&self, state: ButtonStyleState, cx: &mut App) -> Background {
880        match (self, state) {
881            (Self::Default, ButtonStyleState::Normal) => cx.theme().input_background().into(),
882            (Self::Default, ButtonStyleState::Hovered) => cx
883                .theme()
884                .input
885                .mix_oklab(cx.theme().transparent, 0.5)
886                .into(),
887            (Self::Default, ButtonStyleState::Active) => cx
888                .theme()
889                .input
890                .mix_oklab(cx.theme().transparent, 0.7)
891                .into(),
892            (Self::Primary, ButtonStyleState::Normal) => {
893                cx.theme().tokens.primary.background.opacity(0.1)
894            }
895            (Self::Primary, ButtonStyleState::Hovered) => {
896                cx.theme().tokens.primary_hover.background.opacity(0.2)
897            }
898            (Self::Primary, ButtonStyleState::Active) => {
899                cx.theme().tokens.primary_active.background.opacity(0.4)
900            }
901            (Self::Secondary, ButtonStyleState::Normal) => {
902                cx.theme().tokens.secondary.background.opacity(0.1)
903            }
904            (Self::Secondary, ButtonStyleState::Hovered) => {
905                cx.theme().tokens.secondary_hover.background.opacity(0.2)
906            }
907            (Self::Secondary, ButtonStyleState::Active) => {
908                cx.theme().tokens.secondary_active.background.opacity(0.4)
909            }
910            (Self::Danger, ButtonStyleState::Normal) => {
911                cx.theme().tokens.danger.background.opacity(0.1)
912            }
913            (Self::Danger, ButtonStyleState::Hovered) => {
914                cx.theme().tokens.danger_hover.background.opacity(0.2)
915            }
916            (Self::Danger, ButtonStyleState::Active) => {
917                cx.theme().tokens.danger_active.background.opacity(0.4)
918            }
919            (Self::Warning, ButtonStyleState::Normal) => {
920                cx.theme().tokens.warning.background.opacity(0.1)
921            }
922            (Self::Warning, ButtonStyleState::Hovered) => {
923                cx.theme().tokens.warning_hover.background.opacity(0.2)
924            }
925            (Self::Warning, ButtonStyleState::Active) => {
926                cx.theme().tokens.warning_active.background.opacity(0.4)
927            }
928            (Self::Success, ButtonStyleState::Normal) => {
929                cx.theme().tokens.success.background.opacity(0.1)
930            }
931            (Self::Success, ButtonStyleState::Hovered) => {
932                cx.theme().tokens.success_hover.background.opacity(0.2)
933            }
934            (Self::Success, ButtonStyleState::Active) => {
935                cx.theme().tokens.success_active.background.opacity(0.4)
936            }
937            (Self::Info, ButtonStyleState::Normal) => {
938                cx.theme().tokens.info.background.opacity(0.1)
939            }
940            (Self::Info, ButtonStyleState::Hovered) => {
941                cx.theme().tokens.info_hover.background.opacity(0.2)
942            }
943            (Self::Info, ButtonStyleState::Active) => {
944                cx.theme().tokens.info_active.background.opacity(0.4)
945            }
946            (Self::Ghost | Self::Link | Self::Text, _) => cx.theme().transparent.into(),
947            (Self::Custom(colors), _) => colors.color.mix_oklab(cx.theme().transparent, 0.2).into(),
948        }
949    }
950
951    fn bg_color(&self, outline: bool, cx: &mut App) -> Background {
952        if outline {
953            return self.outline_background(ButtonStyleState::Normal, cx);
954        }
955
956        match self {
957            Self::Default => cx.theme().tokens.button.into(),
958            Self::Primary => cx.theme().tokens.button_primary.into(),
959            Self::Secondary => cx.theme().tokens.button_secondary.into(),
960            Self::Danger => cx.theme().tokens.button_danger.into(),
961            Self::Warning => cx.theme().tokens.button_warning.into(),
962            Self::Success => cx.theme().tokens.button_success.into(),
963            Self::Info => cx.theme().tokens.button_info.into(),
964            Self::Ghost | Self::Link | Self::Text => cx.theme().transparent.into(),
965            Self::Custom(colors) => colors.color.mix_oklab(cx.theme().transparent, 0.2).into(),
966        }
967    }
968
969    fn text_color(&self, outline: bool, cx: &mut App) -> Hsla {
970        match self {
971            Self::Default => cx.theme().button_foreground,
972            Self::Primary => {
973                if outline {
974                    cx.theme().primary
975                } else {
976                    cx.theme().button_primary_foreground
977                }
978            }
979            Self::Secondary => {
980                if outline {
981                    cx.theme().secondary_foreground
982                } else {
983                    cx.theme().button_secondary_foreground
984                }
985            }
986            Self::Ghost => cx.theme().secondary_foreground,
987            Self::Danger => {
988                if outline {
989                    cx.theme().danger
990                } else {
991                    cx.theme().button_danger_foreground
992                }
993            }
994            Self::Warning => {
995                if outline {
996                    cx.theme().warning
997                } else {
998                    cx.theme().button_warning_foreground
999                }
1000            }
1001            Self::Success => {
1002                if outline {
1003                    cx.theme().success
1004                } else {
1005                    cx.theme().button_success_foreground
1006                }
1007            }
1008            Self::Info => {
1009                if outline {
1010                    cx.theme().info
1011                } else {
1012                    cx.theme().button_info_foreground
1013                }
1014            }
1015            Self::Link => cx.theme().link,
1016            Self::Text => cx.theme().foreground.opacity(0.9),
1017            Self::Custom(colors) => colors.foreground,
1018        }
1019    }
1020
1021    fn border_color(&self, outline: bool, cx: &mut App) -> Hsla {
1022        match self {
1023            Self::Default => cx.theme().input,
1024            Self::Secondary => cx.theme().border,
1025            Self::Primary => cx.theme().primary,
1026            Self::Danger => {
1027                if outline {
1028                    cx.theme().danger.mix_oklab(transparent_white(), 0.4)
1029                } else {
1030                    cx.theme().button_danger
1031                }
1032            }
1033            Self::Info => {
1034                if outline {
1035                    cx.theme().info.mix_oklab(transparent_white(), 0.4)
1036                } else {
1037                    cx.theme().button_info
1038                }
1039            }
1040            Self::Warning => {
1041                if outline {
1042                    cx.theme().warning.mix_oklab(transparent_white(), 0.4)
1043                } else {
1044                    cx.theme().button_warning
1045                }
1046            }
1047            Self::Success => {
1048                if outline {
1049                    cx.theme().success.mix_oklab(transparent_white(), 0.4)
1050                } else {
1051                    cx.theme().button_success
1052                }
1053            }
1054            Self::Ghost | Self::Link | Self::Text => cx.theme().transparent,
1055            Self::Custom(colors) => {
1056                if outline {
1057                    colors.color.mix_oklab(transparent_white(), 0.4)
1058                } else {
1059                    colors.color
1060                }
1061            }
1062        }
1063    }
1064
1065    fn underline(&self, _: &App) -> bool {
1066        match self {
1067            Self::Link => true,
1068            _ => false,
1069        }
1070    }
1071
1072    fn shadow(&self, _outline: bool, _: &App) -> bool {
1073        match self {
1074            Self::Custom(c) => c.shadow,
1075            _ => false,
1076        }
1077    }
1078
1079    fn normal(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
1080        let bg = self.bg_color(outline, cx);
1081        let border = self.border_color(outline, cx);
1082        let fg = self.text_color(outline, cx);
1083        let underline = self.underline(cx);
1084        let shadow = self.shadow(outline, cx);
1085
1086        ButtonVariantStyle {
1087            bg,
1088            border,
1089            fg,
1090            underline,
1091            shadow,
1092        }
1093    }
1094
1095    fn hovered(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
1096        let bg: Background = match self {
1097            Self::Default => {
1098                if outline {
1099                    self.outline_background(ButtonStyleState::Hovered, cx)
1100                } else {
1101                    cx.theme().tokens.button_hover.into()
1102                }
1103            }
1104            Self::Primary => {
1105                if outline {
1106                    self.outline_background(ButtonStyleState::Hovered, cx)
1107                } else {
1108                    cx.theme().tokens.button_primary_hover.into()
1109                }
1110            }
1111            Self::Secondary => {
1112                if outline {
1113                    self.outline_background(ButtonStyleState::Hovered, cx)
1114                } else {
1115                    cx.theme().tokens.button_secondary_hover.into()
1116                }
1117            }
1118            Self::Danger => {
1119                if outline {
1120                    self.outline_background(ButtonStyleState::Hovered, cx)
1121                } else {
1122                    cx.theme().tokens.button_danger_hover.into()
1123                }
1124            }
1125            Self::Warning => {
1126                if outline {
1127                    self.outline_background(ButtonStyleState::Hovered, cx)
1128                } else {
1129                    cx.theme().tokens.button_warning_hover.into()
1130                }
1131            }
1132            Self::Success => {
1133                if outline {
1134                    self.outline_background(ButtonStyleState::Hovered, cx)
1135                } else {
1136                    cx.theme().tokens.button_success_hover.into()
1137                }
1138            }
1139            Self::Info => {
1140                if outline {
1141                    self.outline_background(ButtonStyleState::Hovered, cx)
1142                } else {
1143                    cx.theme().tokens.button_info_hover.into()
1144                }
1145            }
1146            Self::Custom(colors) => colors.hover.into(),
1147            Self::Ghost => {
1148                let accent: Background = cx.theme().tokens.accent.into();
1149                if cx.theme().mode.is_dark() {
1150                    accent.opacity(0.5)
1151                } else {
1152                    accent
1153                }
1154            }
1155            Self::Link => cx.theme().transparent.into(),
1156            Self::Text => cx.theme().transparent.into(),
1157        };
1158
1159        let border = self.border_color(outline, cx);
1160        let fg = match self {
1161            Self::Link => cx.theme().link_hover,
1162            Self::Text => cx.theme().foreground,
1163            Self::Ghost => cx.theme().accent_foreground,
1164            _ => self.text_color(outline, cx),
1165        };
1166
1167        let underline = self.underline(cx);
1168        let shadow = self.shadow(outline, cx);
1169
1170        ButtonVariantStyle {
1171            bg,
1172            border,
1173            fg,
1174            underline,
1175            shadow,
1176        }
1177    }
1178
1179    fn active(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
1180        let bg = match self {
1181            Self::Default => {
1182                if outline {
1183                    self.outline_background(ButtonStyleState::Active, cx)
1184                } else {
1185                    cx.theme().tokens.button_active.into()
1186                }
1187            }
1188            Self::Primary => {
1189                if outline {
1190                    self.outline_background(ButtonStyleState::Active, cx)
1191                } else {
1192                    cx.theme().tokens.button_primary_active.into()
1193                }
1194            }
1195            Self::Secondary => {
1196                if outline {
1197                    self.outline_background(ButtonStyleState::Active, cx)
1198                } else {
1199                    cx.theme().tokens.button_secondary_active.into()
1200                }
1201            }
1202            Self::Ghost => cx.theme().tokens.button_active.into(),
1203            Self::Danger => {
1204                if outline {
1205                    self.outline_background(ButtonStyleState::Active, cx)
1206                } else {
1207                    cx.theme().tokens.button_danger_active.into()
1208                }
1209            }
1210            Self::Warning => {
1211                if outline {
1212                    self.outline_background(ButtonStyleState::Active, cx)
1213                } else {
1214                    cx.theme().tokens.button_warning_active.into()
1215                }
1216            }
1217            Self::Success => {
1218                if outline {
1219                    self.outline_background(ButtonStyleState::Active, cx)
1220                } else {
1221                    cx.theme().tokens.button_success_active.into()
1222                }
1223            }
1224            Self::Info => {
1225                if outline {
1226                    self.outline_background(ButtonStyleState::Active, cx)
1227                } else {
1228                    cx.theme().tokens.button_info_active.into()
1229                }
1230            }
1231            Self::Custom(colors) => colors.active.into(),
1232            Self::Link => cx.theme().transparent.into(),
1233            Self::Text => cx.theme().transparent.into(),
1234        };
1235        let border = self.border_color(outline, cx);
1236        let fg = match self {
1237            Self::Link => cx.theme().link_active,
1238            Self::Text => cx.theme().foreground.opacity(0.7),
1239            _ => self.text_color(outline, cx),
1240        };
1241        let underline = self.underline(cx);
1242        let shadow = self.shadow(outline, cx);
1243
1244        ButtonVariantStyle {
1245            bg,
1246            border,
1247            fg,
1248            underline,
1249            shadow,
1250        }
1251    }
1252
1253    fn selected(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
1254        if outline {
1255            let active_style = self.active(outline, cx);
1256
1257            return ButtonVariantStyle {
1258                fg: self.text_color(outline, cx),
1259                ..active_style
1260            };
1261        }
1262
1263        let bg = match self {
1264            Self::Default => cx.theme().tokens.button_active.into(),
1265            Self::Primary => cx.theme().tokens.button_primary_active.into(),
1266            Self::Secondary => cx.theme().tokens.button_secondary_active.into(),
1267            Self::Ghost => cx.theme().tokens.secondary_active.into(),
1268            Self::Danger => cx.theme().tokens.button_danger_active.into(),
1269            Self::Warning => cx.theme().tokens.button_warning_active.into(),
1270            Self::Success => cx.theme().tokens.button_success_active.into(),
1271            Self::Info => cx.theme().tokens.button_info_active.into(),
1272            Self::Link => cx.theme().transparent.into(),
1273            Self::Text => cx.theme().transparent.into(),
1274            Self::Custom(colors) => colors.active.into(),
1275        };
1276
1277        let border = self.border_color(outline, cx);
1278        let fg = match self {
1279            Self::Link => cx.theme().link_active,
1280            Self::Text => cx.theme().foreground.opacity(0.7),
1281            _ => self.text_color(false, cx),
1282        };
1283        let underline = self.underline(cx);
1284        let shadow = self.shadow(outline, cx);
1285
1286        ButtonVariantStyle {
1287            bg,
1288            border,
1289            fg,
1290            underline,
1291            shadow,
1292        }
1293    }
1294
1295    fn disabled(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
1296        let bg = match self {
1297            Self::Default | Self::Link | Self::Ghost | Self::Text => cx.theme().transparent.into(),
1298            Self::Primary => cx.theme().tokens.button_primary.background.opacity(0.15),
1299            Self::Danger => cx.theme().tokens.button_danger.background.opacity(0.15),
1300            Self::Warning => cx.theme().tokens.button_warning.background.opacity(0.15),
1301            Self::Success => cx.theme().tokens.button_success.background.opacity(0.15),
1302            Self::Info => cx.theme().tokens.button_info.background.opacity(0.15),
1303            Self::Secondary => cx.theme().tokens.button_secondary.background.opacity(1.5),
1304            Self::Custom(style) => style.color.opacity(0.15).into(),
1305        };
1306        let fg = cx.theme().muted_foreground.opacity(0.5);
1307        let (bg, border) = if outline {
1308            (
1309                self.outline_background(ButtonStyleState::Normal, cx)
1310                    .opacity(0.5),
1311                self.border_color(true, cx).opacity(0.5),
1312            )
1313        } else if let Self::Default = self {
1314            (
1315                cx.theme().input_background().opacity(0.5).into(),
1316                cx.theme().input.opacity(0.5),
1317            )
1318        } else {
1319            let border = match self {
1320                Self::Primary => cx.theme().button_primary.opacity(0.15),
1321                Self::Secondary => cx.theme().button_secondary.opacity(1.5),
1322                Self::Danger => cx.theme().button_danger.opacity(0.15),
1323                Self::Warning => cx.theme().button_warning.opacity(0.15),
1324                Self::Success => cx.theme().button_success.opacity(0.15),
1325                Self::Info => cx.theme().button_info.opacity(0.15),
1326                Self::Custom(style) => style.color.opacity(0.15),
1327                Self::Default | Self::Link | Self::Ghost | Self::Text => cx.theme().transparent,
1328            };
1329            (bg, border)
1330        };
1331
1332        let underline = self.underline(cx);
1333        let shadow = false;
1334
1335        ButtonVariantStyle {
1336            bg,
1337            border,
1338            fg,
1339            underline,
1340            shadow,
1341        }
1342    }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347    use super::*;
1348    use crate::IconName;
1349    use gpui::{linear_color_stop, linear_gradient, px};
1350
1351    /// A button's announced name is its label, unless it was given one — which
1352    /// is the case an icon-only button and a row-shaped button both need.
1353    #[test]
1354    fn an_explicit_accessibility_label_replaces_the_visible_one() {
1355        let plain = Button::new("save").label("Save");
1356        assert_eq!(plain.accessibility_label, None);
1357        assert_eq!(plain.label.as_deref(), Some("Save"));
1358
1359        let named = Button::new("row")
1360            .label("Save")
1361            .accessibility_label("Save the current document");
1362        assert_eq!(
1363            named.accessibility_label.as_deref(),
1364            Some("Save the current document"),
1365            "an explicit name must win over the visible label"
1366        );
1367        assert_eq!(
1368            named.label.as_deref(),
1369            Some("Save"),
1370            "and must not change what is drawn"
1371        );
1372    }
1373
1374    #[gpui::test]
1375    fn disabled_legacy_button_keeps_existing_pointer_blocking(cx: &mut gpui::TestAppContext) {
1376        use std::{cell::Cell, rc::Rc};
1377
1378        use gpui::{Context, Modifiers, Render, point};
1379
1380        struct Harness(Rc<Cell<usize>>, Rc<Cell<usize>>);
1381
1382        impl Render for Harness {
1383            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1384                let button_clicks = self.0.clone();
1385                let parent_clicks = self.1.clone();
1386                div()
1387                    .id("disabled-button-parent")
1388                    .tab_group()
1389                    .size(px(100.))
1390                    .on_click(move |_, _, _| parent_clicks.set(parent_clicks.get() + 1))
1391                    .child(
1392                        Button::new("disabled-legacy")
1393                            .disabled(true)
1394                            .size_full()
1395                            .on_click(move |_, _, _| button_clicks.set(button_clicks.get() + 1)),
1396                    )
1397            }
1398        }
1399
1400        cx.update(crate::init);
1401        let button_clicks = Rc::new(Cell::new(0));
1402        let parent_clicks = Rc::new(Cell::new(0));
1403        let (_, cx) = cx.add_window_view({
1404            let button_clicks = button_clicks.clone();
1405            let parent_clicks = parent_clicks.clone();
1406            move |_, _| Harness(button_clicks, parent_clicks)
1407        });
1408        cx.update(|window, cx| window.draw(cx).clear(cx));
1409        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
1410        cx.update(|window, cx| window.focus_next(cx));
1411        cx.simulate_keystrokes("enter space");
1412
1413        assert_eq!(button_clicks.get(), 0);
1414        assert_eq!(parent_clicks.get(), 0);
1415        cx.update(|window, cx| assert!(window.focused(cx).is_none()));
1416    }
1417
1418    #[gpui::test]
1419    fn enabled_button_delegates_pointer_enter_and_space_once(cx: &mut gpui::TestAppContext) {
1420        use std::{cell::Cell, rc::Rc};
1421
1422        use gpui::{
1423            ClickEvent, Context, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, Render, point,
1424        };
1425
1426        struct Harness(Rc<Cell<usize>>, Rc<Cell<usize>>);
1427
1428        impl Render for Harness {
1429            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1430                let clicks = self.0.clone();
1431                let keyboard_clicks = self.1.clone();
1432                div().tab_group().size(px(100.)).child(
1433                    Button::new("enabled-legacy")
1434                        .size_full()
1435                        .on_click(move |event, _, _| {
1436                            clicks.set(clicks.get() + 1);
1437                            if matches!(event, ClickEvent::Keyboard(_)) {
1438                                keyboard_clicks.set(keyboard_clicks.get() + 1);
1439                            }
1440                        }),
1441                )
1442            }
1443        }
1444
1445        cx.update(crate::init);
1446        let clicks = Rc::new(Cell::new(0));
1447        let keyboard_clicks = Rc::new(Cell::new(0));
1448        let (_, cx) = cx.add_window_view({
1449            let clicks = clicks.clone();
1450            let keyboard_clicks = keyboard_clicks.clone();
1451            move |_, _| Harness(clicks, keyboard_clicks)
1452        });
1453        cx.update(|window, cx| window.draw(cx).clear(cx));
1454        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
1455        cx.update(|window, cx| window.focus_next(cx));
1456        cx.update(|window, cx| {
1457            assert!(window.focused(cx).is_some());
1458            window.draw(cx).clear(cx);
1459        });
1460        for key in ["enter", "space"] {
1461            let keystroke = Keystroke::parse(key).unwrap();
1462            cx.simulate_event(KeyDownEvent {
1463                keystroke: keystroke.clone(),
1464                is_held: false,
1465                prefer_character_input: false,
1466            });
1467            cx.simulate_event(KeyUpEvent { keystroke });
1468        }
1469
1470        assert_eq!(clicks.get(), 3);
1471        assert_eq!(keyboard_clicks.get(), 2);
1472    }
1473
1474    #[gpui::test]
1475    fn loading_button_keeps_existing_pointer_blocking(cx: &mut gpui::TestAppContext) {
1476        use std::{cell::Cell, rc::Rc};
1477
1478        use gpui::{Context, Modifiers, Render, point};
1479
1480        struct Harness(Rc<Cell<usize>>, Rc<Cell<usize>>);
1481
1482        impl Render for Harness {
1483            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1484                let button_clicks = self.0.clone();
1485                let parent_clicks = self.1.clone();
1486                div()
1487                    .id("loading-button-parent")
1488                    .tab_group()
1489                    .size(px(100.))
1490                    .on_click(move |_, _, _| parent_clicks.set(parent_clicks.get() + 1))
1491                    .child(
1492                        Button::new("loading-legacy")
1493                            .loading(true)
1494                            .size_full()
1495                            .on_click(move |_, _, _| button_clicks.set(button_clicks.get() + 1)),
1496                    )
1497            }
1498        }
1499
1500        cx.update(crate::init);
1501        let button_clicks = Rc::new(Cell::new(0));
1502        let parent_clicks = Rc::new(Cell::new(0));
1503        let (_, cx) = cx.add_window_view({
1504            let button_clicks = button_clicks.clone();
1505            let parent_clicks = parent_clicks.clone();
1506            move |_, _| Harness(button_clicks, parent_clicks)
1507        });
1508        cx.update(|window, cx| window.draw(cx).clear(cx));
1509        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
1510        cx.update(|window, cx| window.focus_next(cx));
1511        cx.simulate_keystrokes("enter space");
1512
1513        assert_eq!(button_clicks.get(), 0);
1514        assert_eq!(parent_clicks.get(), 0);
1515        cx.update(|window, cx| assert!(window.focused(cx).is_some()));
1516    }
1517
1518    #[gpui::test]
1519    fn loading_button_without_callback_still_blocks_parent_activation(
1520        cx: &mut gpui::TestAppContext,
1521    ) {
1522        use std::{cell::Cell, rc::Rc};
1523
1524        use gpui::{Context, Modifiers, Render, point};
1525
1526        struct Harness(Rc<Cell<usize>>);
1527
1528        impl Render for Harness {
1529            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1530                let parent_clicks = self.0.clone();
1531                div()
1532                    .id("loading-without-callback-parent")
1533                    .tab_group()
1534                    .size(px(100.))
1535                    .on_click(move |_, _, _| parent_clicks.set(parent_clicks.get() + 1))
1536                    .child(
1537                        Button::new("loading-without-callback")
1538                            .loading(true)
1539                            .size_full(),
1540                    )
1541            }
1542        }
1543
1544        cx.update(crate::init);
1545        let parent_clicks = Rc::new(Cell::new(0));
1546        let (_, cx) = cx.add_window_view({
1547            let parent_clicks = parent_clicks.clone();
1548            move |_, _| Harness(parent_clicks)
1549        });
1550        cx.update(|window, cx| window.draw(cx).clear(cx));
1551        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
1552        cx.update(|window, cx| window.focus_next(cx));
1553        cx.simulate_keystrokes("enter space");
1554
1555        assert_eq!(parent_clicks.get(), 0);
1556        cx.update(|window, cx| assert!(window.focused(cx).is_some()));
1557    }
1558
1559    #[gpui::test]
1560    fn base_activation_is_preserved_and_blocked_while_loading(cx: &mut gpui::TestAppContext) {
1561        use gpui::{Context, Render, point};
1562        use std::{cell::Cell, rc::Rc};
1563
1564        struct Harness {
1565            clicks: Rc<Cell<usize>>,
1566            loading: bool,
1567        }
1568        impl Render for Harness {
1569            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1570                let clicks = self.clicks.clone();
1571                div().tab_group().child(
1572                    Button::new("close")
1573                        .with_base(
1574                            gpui_base::Button::new("close")
1575                                .on_click(move |_, _, _| clicks.set(clicks.get() + 1)),
1576                        )
1577                        .loading(self.loading)
1578                        .size(px(100.)),
1579                )
1580            }
1581        }
1582
1583        cx.update(crate::init);
1584        let clicks = Rc::new(Cell::new(0));
1585        let (view, cx) = cx.add_window_view({
1586            let clicks = clicks.clone();
1587            move |_, _| Harness {
1588                clicks,
1589                loading: false,
1590            }
1591        });
1592        cx.update(|window, cx| window.draw(cx).clear(cx));
1593        cx.simulate_click(point(px(10.), px(10.)), Default::default());
1594        assert_eq!(clicks.get(), 1);
1595
1596        view.update(cx, |view, cx| {
1597            view.loading = true;
1598            cx.notify();
1599        });
1600        cx.update(|window, cx| window.draw(cx).clear(cx));
1601        cx.simulate_click(point(px(10.), px(10.)), Default::default());
1602        cx.update(|window, cx| window.focus_next(cx));
1603        cx.simulate_keystrokes("enter space");
1604        assert_eq!(clicks.get(), 1);
1605    }
1606
1607    #[gpui::test]
1608    fn test_button_builder(_cx: &mut gpui::TestAppContext) {
1609        let button = Button::new("complex-button")
1610            .label("Save Changes")
1611            .primary()
1612            .outline()
1613            .large()
1614            .tooltip("Click to save")
1615            .compact()
1616            .loading(false)
1617            .disabled(false)
1618            .selected(false)
1619            .tab_index(1)
1620            .tab_stop(true)
1621            .dropdown_caret(false)
1622            .rounded(ButtonRounded::Medium)
1623            .on_click(|_, _, _| {});
1624
1625        assert_eq!(button.label, Some("Save Changes".into()));
1626        assert_eq!(button.variant, ButtonVariant::Primary);
1627        assert!(button.outline);
1628        assert_eq!(button.size, Size::Large);
1629        assert!(button.tooltip.is_some());
1630        assert!(button.compact);
1631        assert!(!button.loading);
1632        assert!(!button.disabled);
1633        assert!(!button.selected);
1634        assert_eq!(button.toggled, None);
1635        assert_eq!(button.tab_index, 1);
1636        assert!(button.tab_stop);
1637        assert!(!button.dropdown_caret);
1638        assert!(matches!(button.rounded, ButtonRounded::Medium));
1639    }
1640
1641    /// A button paints an open popup the way it paints a selection, but the
1642    /// two states are stored apart, so a caller can read back which one it set
1643    /// and a later design can tell them apart visually.
1644    #[test]
1645    fn an_open_trigger_is_stored_apart_from_a_selected_one() {
1646        let open = Button::new("trigger").open(true);
1647        assert!(open.is_open());
1648        assert!(!open.is_selected());
1649        assert!(open.shows_selected_style());
1650
1651        let selected = Button::new("trigger").selected(true);
1652        assert!(selected.is_selected());
1653        assert!(!selected.is_open());
1654        assert!(selected.shows_selected_style());
1655
1656        assert!(!Button::new("trigger").shows_selected_style());
1657    }
1658
1659    #[test]
1660    fn selected_trigger_presentation_does_not_imply_toggled_accessibility() {
1661        let selected = Button::new("menu-trigger").selected(true);
1662        assert!(selected.selected);
1663        assert_eq!(selected.toggled, None);
1664
1665        let selected_toggle = Button::new("explicit-toggle").selected(true).toggled(true);
1666        assert!(selected_toggle.selected);
1667        assert_eq!(selected_toggle.toggled, Some(true));
1668    }
1669
1670    /// A loading button must be as inert as a disabled one. `interactive` is what
1671    /// gates the hover and active styling, the `cursor_pointer` of link buttons and
1672    /// the `mouse_down` handler, none of which depend on a listener being set.
1673    #[gpui::test]
1674    fn test_button_loading_is_not_interactive(_cx: &mut gpui::TestAppContext) {
1675        assert!(Button::new("test").interactive());
1676        assert!(!Button::new("test").loading(true).interactive());
1677        assert!(!Button::new("test").disabled(true).interactive());
1678        assert!(
1679            !Button::new("test")
1680                .loading(true)
1681                .disabled(true)
1682                .interactive()
1683        );
1684
1685        // Loading gates hovering even when an `on_hover` listener is set.
1686        let loading = Button::new("test").loading(true).on_hover(|_, _, _| {});
1687        assert!(!loading.hoverable());
1688    }
1689
1690    /// `selected` is styling only; the toggle state must be opted into, so that
1691    /// ordinary buttons are not announced as toggle buttons.
1692    #[gpui::test]
1693    fn test_button_variant_methods(_cx: &mut gpui::TestAppContext) {
1694        // Test variant check methods
1695        assert!(ButtonVariant::Link.is_link());
1696        assert!(ButtonVariant::Text.is_text());
1697        assert!(ButtonVariant::Ghost.is_ghost());
1698
1699        // Test no_padding logic
1700        assert!(ButtonVariant::Link.no_padding());
1701        assert!(ButtonVariant::Text.no_padding());
1702        assert!(!ButtonVariant::Ghost.no_padding());
1703    }
1704
1705    #[gpui::test]
1706    fn link_button_prepaints_its_child(cx: &mut gpui::TestAppContext) {
1707        use gpui::{Context, Render};
1708
1709        struct LinkButtonHarness;
1710
1711        impl Render for LinkButtonHarness {
1712            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1713                Button::new("link-button").link().child(
1714                    div()
1715                        .debug_selector(|| "link-button-child".into())
1716                        .child("Visible link button"),
1717                )
1718            }
1719        }
1720
1721        cx.update(crate::init);
1722        let (_, cx) = cx.add_window_view(|_, _| LinkButtonHarness);
1723        cx.update(|window, cx| window.draw(cx).clear(cx));
1724
1725        let bounds = cx
1726            .debug_bounds("link-button-child")
1727            .expect("link button child must participate in layout and prepaint");
1728        assert!(bounds.size.width > px(0.));
1729        assert!(bounds.size.height > px(0.));
1730    }
1731
1732    #[gpui::test]
1733    fn base_slot_prepaints_complete_button_content(cx: &mut gpui::TestAppContext) {
1734        use gpui::{Context, Render};
1735
1736        struct CompleteButtonHarness;
1737
1738        impl Render for CompleteButtonHarness {
1739            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1740                div()
1741                    .child(
1742                        Button::new("complete-button")
1743                            .icon(IconName::Plus)
1744                            .label("Create")
1745                            .child(
1746                                div()
1747                                    .debug_selector(|| "button-custom-content".into())
1748                                    .child("Details"),
1749                            )
1750                            .dropdown_caret(true),
1751                    )
1752                    .child(
1753                        Button::new("loading-button")
1754                            .icon(IconName::Plus)
1755                            .loading_icon(IconName::Loader)
1756                            .loading(true)
1757                            .label("Creating")
1758                            .dropdown_caret(true),
1759                    )
1760            }
1761        }
1762
1763        cx.update(crate::init);
1764        let (_, cx) = cx.add_window_view(|_, _| CompleteButtonHarness);
1765        cx.update(|window, cx| window.draw(cx).clear(cx));
1766
1767        let bounds = cx
1768            .debug_bounds("button-custom-content")
1769            .expect("application content must prepaint through the Base child seam");
1770        assert!(bounds.size.width > px(0.));
1771        assert!(bounds.size.height > px(0.));
1772    }
1773
1774    #[gpui::test]
1775    fn test_outline_selected_uses_outline_active_style(cx: &mut gpui::TestAppContext) {
1776        cx.update(crate::init);
1777        let window = cx.add_empty_window();
1778        window.update(|_, cx| {
1779            let variant = ButtonVariant::Danger;
1780            let active_style = variant.active(true, cx);
1781            let selected_style = variant.selected(true, cx);
1782
1783            assert_eq!(selected_style.bg, active_style.bg);
1784            assert_eq!(selected_style.border, active_style.border);
1785            assert_eq!(selected_style.fg, cx.theme().danger);
1786            assert_ne!(selected_style.bg, cx.theme().tokens.danger_active.into());
1787        });
1788    }
1789
1790    #[gpui::test]
1791    fn test_primary_button_uses_gradient_background_tokens(cx: &mut gpui::TestAppContext) {
1792        cx.update(crate::init);
1793        let window = cx.add_empty_window();
1794        window.update(|_, cx| {
1795            let config = serde_json::from_value::<crate::ThemeConfig>(serde_json::json!({
1796                "name": "Gradient",
1797                "mode": "light",
1798                "colors": {
1799                    "button.primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)",
1800                    "button.primary.hover.background": "linear-gradient(145deg, #4338CA, #0891B2)",
1801                    "button.primary.active.background": "linear-gradient(155deg, #3730A3, #0E7490)"
1802                }
1803            }))
1804            .unwrap();
1805            crate::Theme::global_mut(cx).apply_config(&std::rc::Rc::new(config));
1806
1807            assert_eq!(
1808                ButtonVariant::Primary.normal(false, cx).bg,
1809                cx.theme().tokens.button_primary.into()
1810            );
1811            assert_eq!(
1812                ButtonVariant::Primary.hovered(false, cx).bg,
1813                cx.theme().tokens.button_primary_hover.into()
1814            );
1815            assert_eq!(
1816                ButtonVariant::Primary.active(false, cx).bg,
1817                cx.theme().tokens.button_primary_active.into()
1818            );
1819        });
1820    }
1821
1822    #[gpui::test]
1823    fn test_outline_primary_keeps_original_depth(cx: &mut gpui::TestAppContext) {
1824        cx.update(crate::init);
1825        let window = cx.add_empty_window();
1826        window.update(|_, cx| {
1827            let config = serde_json::from_value::<crate::ThemeConfig>(serde_json::json!({
1828                "name": "Outline Depth",
1829                "mode": "light",
1830                "colors": {
1831                    "primary.background": "linear-gradient(180deg, #111827, #020617)",
1832                    "primary.hover.background": "linear-gradient(180deg, #1F2937, #111827)",
1833                    "primary.active.background": "linear-gradient(180deg, #020617, #000000)"
1834                }
1835            }))
1836            .unwrap();
1837            crate::Theme::global_mut(cx).apply_config(&std::rc::Rc::new(config));
1838
1839            assert_eq!(
1840                ButtonVariant::Primary.normal(true, cx).bg,
1841                cx.theme().tokens.primary.background.opacity(0.1)
1842            );
1843            assert_eq!(
1844                ButtonVariant::Primary.hovered(true, cx).bg,
1845                cx.theme().tokens.primary_hover.background.opacity(0.2)
1846            );
1847            assert_eq!(
1848                ButtonVariant::Primary.active(true, cx).bg,
1849                cx.theme().tokens.primary_active.background.opacity(0.4)
1850            );
1851        });
1852    }
1853
1854    #[gpui::test]
1855    fn test_outline_buttons_use_semantic_gradient_tokens(cx: &mut gpui::TestAppContext) {
1856        cx.update(crate::init);
1857        let window = cx.add_empty_window();
1858        window.update(|_, cx| {
1859            let config = serde_json::from_value::<crate::ThemeConfig>(serde_json::json!({
1860                "name": "Outline Gradient",
1861                "mode": "light",
1862                "colors": {
1863                    "primary.background": "linear-gradient(180deg, #111827, #020617)",
1864                    "primary.hover.background": "linear-gradient(180deg, #1F2937, #111827)",
1865                    "primary.active.background": "linear-gradient(180deg, #020617, #000000)",
1866                    "button.primary.background": "linear-gradient(180deg, #FFFFFF, #E5E7EB)",
1867                    "button.primary.hover.background": "linear-gradient(180deg, #F9FAFB, #E5E7EB)",
1868                    "button.primary.active.background": "linear-gradient(180deg, #E5E7EB, #D1D5DB)",
1869                    "danger.background": "linear-gradient(180deg, #EF4444, #DC2626)",
1870                    "danger.hover.background": "linear-gradient(180deg, #F87171, #EF4444)",
1871                    "danger.active.background": "linear-gradient(180deg, #DC2626, #B91C1C)",
1872                    "button.danger.background": "linear-gradient(180deg, #FEF2F2, #FEE2E2)",
1873                    "button.danger.hover.background": "linear-gradient(180deg, #FEE2E2, #FECACA)",
1874                    "button.danger.active.background": "linear-gradient(180deg, #FECACA, #FCA5A5)"
1875                }
1876            }))
1877            .unwrap();
1878            crate::Theme::global_mut(cx).apply_config(&std::rc::Rc::new(config));
1879
1880            assert_eq!(
1881                ButtonVariant::Primary.normal(true, cx).bg,
1882                cx.theme().tokens.primary.background.opacity(0.1)
1883            );
1884            assert_eq!(
1885                ButtonVariant::Danger.normal(true, cx).bg,
1886                cx.theme().tokens.danger.background.opacity(0.1)
1887            );
1888            assert_eq!(
1889                ButtonVariant::Danger.hovered(true, cx).bg,
1890                cx.theme().tokens.danger_hover.background.opacity(0.2)
1891            );
1892            assert_eq!(
1893                ButtonVariant::Danger.active(true, cx).bg,
1894                cx.theme().tokens.danger_active.background.opacity(0.4)
1895            );
1896            assert_eq!(
1897                ButtonVariant::Primary.normal(false, cx).bg,
1898                cx.theme().tokens.button_primary.into()
1899            );
1900            assert_eq!(
1901                ButtonVariant::Danger.normal(false, cx).bg,
1902                linear_gradient(
1903                    180.,
1904                    linear_color_stop(crate::try_parse_color("#FEF2F2").unwrap(), 0.),
1905                    linear_color_stop(crate::try_parse_color("#FEE2E2").unwrap(), 1.)
1906                )
1907            );
1908        });
1909    }
1910
1911    #[gpui::test]
1912    fn test_disabled_outline_buttons_keep_semantic_backgrounds(cx: &mut gpui::TestAppContext) {
1913        cx.update(crate::init);
1914        let window = cx.add_empty_window();
1915        window.update(|_, cx| {
1916            let config = serde_json::from_value::<crate::ThemeConfig>(serde_json::json!({
1917                "name": "Disabled Outline Gradient",
1918                "mode": "light",
1919                "colors": {
1920                    "primary.background": "linear-gradient(180deg, #111827, #020617)",
1921                    "button.primary.background": "linear-gradient(180deg, #FFFFFF, #E5E7EB)",
1922                    "danger.background": "linear-gradient(180deg, #EF4444, #DC2626)",
1923                    "button.danger.background": "linear-gradient(180deg, #E5E7EB, #D1D5DB)"
1924                }
1925            }))
1926            .unwrap();
1927            crate::Theme::global_mut(cx).apply_config(&std::rc::Rc::new(config));
1928
1929            assert_eq!(
1930                ButtonVariant::Primary.disabled(true, cx).bg,
1931                cx.theme()
1932                    .tokens
1933                    .primary
1934                    .background
1935                    .opacity(0.1)
1936                    .opacity(0.5)
1937            );
1938            assert_eq!(
1939                ButtonVariant::Danger.disabled(true, cx).bg,
1940                cx.theme()
1941                    .tokens
1942                    .danger
1943                    .background
1944                    .opacity(0.1)
1945                    .opacity(0.5)
1946            );
1947            assert_ne!(
1948                ButtonVariant::Danger.disabled(true, cx).bg,
1949                cx.theme().input_background().opacity(0.5).into()
1950            );
1951            assert_ne!(
1952                ButtonVariant::Danger.disabled(true, cx).bg,
1953                cx.theme().tokens.button_danger.background.opacity(0.15)
1954            );
1955        });
1956    }
1957}