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