Skip to main content

gpui_component/button/
button.rs

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