Skip to main content

gpui_component/button/
button.rs

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