gpui_component/button/
button.rs

1use std::rc::Rc;
2
3use crate::{
4    h_flex, spinner::Spinner, tooltip::Tooltip, ActiveTheme, Colorize as _, Disableable,
5    FocusableExt as _, Icon, IconName, Selectable, Sizable, Size, StyleSized, StyledExt,
6};
7use gpui::{
8    div, prelude::FluentBuilder as _, px, relative, Action, AnyElement, App, ClickEvent, Corners,
9    Div, Edges, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement, MouseButton,
10    ParentElement, Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
11    StyleRefinement, Styled, Window,
12};
13
14#[derive(Default, Clone, Copy)]
15pub enum ButtonRounded {
16    None,
17    Small,
18    #[default]
19    Medium,
20    Large,
21    Size(Pixels),
22}
23
24impl From<Pixels> for ButtonRounded {
25    fn from(px: Pixels) -> Self {
26        ButtonRounded::Size(px)
27    }
28}
29
30#[derive(Clone, Copy, PartialEq, Eq)]
31pub struct ButtonCustomVariant {
32    color: Hsla,
33    foreground: Hsla,
34    border: Hsla,
35    shadow: bool,
36    hover: Hsla,
37    active: Hsla,
38}
39
40pub trait ButtonVariants: Sized {
41    fn with_variant(self, variant: ButtonVariant) -> Self;
42
43    /// With the primary style for the Button.
44    fn primary(self) -> Self {
45        self.with_variant(ButtonVariant::Primary)
46    }
47
48    /// With the danger style for the Button.
49    fn danger(self) -> Self {
50        self.with_variant(ButtonVariant::Danger)
51    }
52
53    /// With the warning style for the Button.
54    fn warning(self) -> Self {
55        self.with_variant(ButtonVariant::Warning)
56    }
57
58    /// With the success style for the Button.
59    fn success(self) -> Self {
60        self.with_variant(ButtonVariant::Success)
61    }
62
63    /// With the info style for the Button.
64    fn info(self) -> Self {
65        self.with_variant(ButtonVariant::Info)
66    }
67
68    /// With the ghost style for the Button.
69    fn ghost(self) -> Self {
70        self.with_variant(ButtonVariant::Ghost)
71    }
72
73    /// With the link style for the Button.
74    fn link(self) -> Self {
75        self.with_variant(ButtonVariant::Link)
76    }
77
78    /// With the text style for the Button, it will no padding look like a normal text.
79    fn text(self) -> Self {
80        self.with_variant(ButtonVariant::Text)
81    }
82
83    /// With the custom style for the Button.
84    fn custom(self, style: ButtonCustomVariant) -> Self {
85        self.with_variant(ButtonVariant::Custom(style))
86    }
87}
88
89impl ButtonCustomVariant {
90    pub fn new(cx: &App) -> Self {
91        Self {
92            color: cx.theme().transparent,
93            foreground: cx.theme().foreground,
94            border: cx.theme().transparent,
95            hover: cx.theme().transparent,
96            active: cx.theme().transparent,
97            shadow: false,
98        }
99    }
100
101    /// Set background color, default is transparent.
102    pub fn color(mut self, color: Hsla) -> Self {
103        self.color = color;
104        self
105    }
106
107    /// Set foreground color, default is theme foreground.
108    pub fn foreground(mut self, color: Hsla) -> Self {
109        self.foreground = color;
110        self
111    }
112
113    /// Set border color, default is transparent.
114    pub fn border(mut self, color: Hsla) -> Self {
115        self.border = color;
116        self
117    }
118
119    /// Set hover background color, default is transparent.
120    pub fn hover(mut self, color: Hsla) -> Self {
121        self.hover = color;
122        self
123    }
124
125    /// Set active background color, default is transparent.
126    pub fn active(mut self, color: Hsla) -> Self {
127        self.active = color;
128        self
129    }
130
131    /// Set shadow, default is false.
132    pub fn shadow(mut self, shadow: bool) -> Self {
133        self.shadow = shadow;
134        self
135    }
136}
137
138/// The veriant of the Button.
139#[derive(Clone, Copy, PartialEq, Eq, Default)]
140pub enum ButtonVariant {
141    Primary,
142    #[default]
143    Secondary,
144    Danger,
145    Info,
146    Success,
147    Warning,
148    Ghost,
149    Link,
150    Text,
151    Custom(ButtonCustomVariant),
152}
153
154impl ButtonVariant {
155    #[inline]
156    pub fn is_link(&self) -> bool {
157        matches!(self, Self::Link)
158    }
159
160    #[inline]
161    pub fn is_text(&self) -> bool {
162        matches!(self, Self::Text)
163    }
164
165    #[inline]
166    pub fn is_ghost(&self) -> bool {
167        matches!(self, Self::Ghost)
168    }
169
170    #[inline]
171    fn no_padding(&self) -> bool {
172        self.is_link() || self.is_text()
173    }
174}
175
176/// A Button element.
177#[derive(IntoElement)]
178pub struct Button {
179    id: ElementId,
180    base: Stateful<Div>,
181    style: StyleRefinement,
182    icon: Option<Icon>,
183    label: Option<SharedString>,
184    children: Vec<AnyElement>,
185    disabled: bool,
186    pub(crate) selected: bool,
187    variant: ButtonVariant,
188    rounded: ButtonRounded,
189    outline: bool,
190    border_corners: Corners<bool>,
191    border_edges: Edges<bool>,
192    dropdown_caret: bool,
193    size: Size,
194    compact: bool,
195    tooltip: Option<(
196        SharedString,
197        Option<(Rc<Box<dyn Action>>, Option<SharedString>)>,
198    )>,
199    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
200    on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
201    loading: bool,
202    loading_icon: Option<Icon>,
203
204    tab_index: isize,
205    tab_stop: bool,
206}
207
208impl From<Button> for AnyElement {
209    fn from(button: Button) -> Self {
210        button.into_any_element()
211    }
212}
213
214impl Button {
215    pub fn new(id: impl Into<ElementId>) -> Self {
216        let id = id.into();
217
218        Self {
219            id: id.clone(),
220            // ID must be set after div is created;
221            // `dropdown_menu` uses this id to create the popup menu.
222            base: div().flex_shrink_0().id(id),
223            style: StyleRefinement::default(),
224            icon: None,
225            label: None,
226            disabled: false,
227            selected: false,
228            variant: ButtonVariant::default(),
229            rounded: ButtonRounded::Medium,
230            border_corners: Corners::all(true),
231            border_edges: Edges::all(true),
232            size: Size::Medium,
233            tooltip: None,
234            on_click: None,
235            on_hover: None,
236            loading: false,
237            compact: false,
238            outline: false,
239            children: Vec::new(),
240            loading_icon: None,
241            dropdown_caret: false,
242            tab_index: 0,
243            tab_stop: true,
244        }
245    }
246
247    /// Set the outline style of the Button.
248    pub fn outline(mut self) -> Self {
249        self.outline = true;
250        self
251    }
252
253    /// Set the border radius of the Button.
254    pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self {
255        self.rounded = rounded.into();
256        self
257    }
258
259    /// Set the border corners side of the Button.
260    pub(crate) fn border_corners(mut self, corners: impl Into<Corners<bool>>) -> Self {
261        self.border_corners = corners.into();
262        self
263    }
264
265    /// Set the border edges of the Button.
266    pub(crate) fn border_edges(mut self, edges: impl Into<Edges<bool>>) -> Self {
267        self.border_edges = edges.into();
268        self
269    }
270
271    /// Set label to the Button, if no label is set, the button will be in Icon Button mode.
272    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
273        self.label = Some(label.into());
274        self
275    }
276
277    /// Set the icon of the button, if the Button have no label, the button well in Icon Button mode.
278    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
279        self.icon = Some(icon.into());
280        self
281    }
282
283    /// Set the tooltip of the button.
284    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
285        self.tooltip = Some((tooltip.into(), None));
286        self
287    }
288
289    /// Set the tooltip of the button with action to show keybinding.
290    pub fn tooltip_with_action(
291        mut self,
292        tooltip: impl Into<SharedString>,
293        action: &dyn Action,
294        context: Option<&str>,
295    ) -> Self {
296        self.tooltip = Some((
297            tooltip.into(),
298            Some((
299                Rc::new(action.boxed_clone()),
300                context.map(|c| c.to_string().into()),
301            )),
302        ));
303        self
304    }
305
306    /// Set true to show the loading indicator.
307    pub fn loading(mut self, loading: bool) -> Self {
308        self.loading = loading;
309        self
310    }
311
312    /// Set the button to compact mode, then padding will be reduced.
313    pub fn compact(mut self) -> Self {
314        self.compact = true;
315        self
316    }
317
318    /// Add click handler.
319    pub fn on_click(
320        mut self,
321        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
322    ) -> Self {
323        self.on_click = Some(Rc::new(handler));
324        self
325    }
326
327    /// Add hover handler, the bool parameter indicates whether the mouse is hovering.
328    pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
329        self.on_hover = Some(Rc::new(handler));
330        self
331    }
332
333    /// Set the loading icon of the button, it will be used when loading is true.
334    ///
335    /// Default is a spinner icon.
336    pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
337        self.loading_icon = Some(icon.into());
338        self
339    }
340
341    /// Set the tab index of the button, it will be used to focus the button by tab key.
342    ///
343    /// Default is 0.
344    pub fn tab_index(mut self, tab_index: isize) -> Self {
345        self.tab_index = tab_index;
346        self
347    }
348
349    /// Set the tab stop of the button, if true, the button will be focusable by tab key.
350    ///
351    /// Default is true.
352    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
353        self.tab_stop = tab_stop;
354        self
355    }
356
357    /// Set to show a dropdown caret icon at the end of the button.
358    pub fn dropdown_caret(mut self, dropdown_caret: bool) -> Self {
359        self.dropdown_caret = dropdown_caret;
360        self
361    }
362
363    #[inline]
364    fn clickable(&self) -> bool {
365        !(self.disabled || self.loading) && self.on_click.is_some()
366    }
367
368    #[inline]
369    fn hoverable(&self) -> bool {
370        !(self.disabled || self.loading) && self.on_hover.is_some()
371    }
372}
373
374impl Disableable for Button {
375    fn disabled(mut self, disabled: bool) -> Self {
376        self.disabled = disabled;
377        self
378    }
379}
380
381impl Selectable for Button {
382    fn selected(mut self, selected: bool) -> Self {
383        self.selected = selected;
384        self
385    }
386
387    fn is_selected(&self) -> bool {
388        self.selected
389    }
390}
391
392impl Sizable for Button {
393    fn with_size(mut self, size: impl Into<Size>) -> Self {
394        self.size = size.into();
395        self
396    }
397}
398
399impl ButtonVariants for Button {
400    fn with_variant(mut self, variant: ButtonVariant) -> Self {
401        self.variant = variant;
402        self
403    }
404}
405
406impl Styled for Button {
407    fn style(&mut self) -> &mut StyleRefinement {
408        &mut self.style
409    }
410}
411
412impl ParentElement for Button {
413    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
414        self.children.extend(elements)
415    }
416}
417
418impl InteractiveElement for Button {
419    fn interactivity(&mut self) -> &mut Interactivity {
420        self.base.interactivity()
421    }
422}
423
424impl RenderOnce for Button {
425    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
426        let style: ButtonVariant = self.variant;
427        let clickable = self.clickable();
428        let is_disabled = self.disabled;
429        let hoverable = self.hoverable();
430        let normal_style = style.normal(self.outline, cx);
431        let icon_size = match self.size {
432            Size::Size(v) => Size::Size(v * 0.75),
433            _ => self.size,
434        };
435
436        let focus_handle = window
437            .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
438            .read(cx)
439            .clone();
440        let is_focused = focus_handle.is_focused(window);
441
442        let rounding = match self.rounded {
443            ButtonRounded::Small => cx.theme().radius * 0.5,
444            ButtonRounded::Medium => cx.theme().radius,
445            ButtonRounded::Large => cx.theme().radius * 2.0,
446            ButtonRounded::Size(px) => px,
447            ButtonRounded::None => Pixels::ZERO,
448        };
449
450        self.base
451            .when(!self.disabled, |this| {
452                this.track_focus(
453                    &focus_handle
454                        .tab_index(self.tab_index)
455                        .tab_stop(self.tab_stop),
456                )
457            })
458            .cursor_default()
459            .flex()
460            .flex_shrink_0()
461            .items_center()
462            .justify_center()
463            .cursor_default()
464            .when(self.variant.is_link(), |this| this.cursor_pointer())
465            .when(cx.theme().shadow && normal_style.shadow, |this| {
466                this.shadow_xs()
467            })
468            .when(!style.no_padding(), |this| {
469                if self.label.is_none() && self.children.is_empty() {
470                    // Icon Button
471                    match self.size {
472                        Size::Size(px) => this.size(px),
473                        Size::XSmall => this.size_5(),
474                        Size::Small => this.size_6(),
475                        Size::Large | Size::Medium => this.size_8(),
476                    }
477                } else {
478                    // Normal Button
479                    match self.size {
480                        Size::Size(size) => this.px(size * 0.2),
481                        Size::XSmall => this.h_5().px_1(),
482                        Size::Small => this.h_6().px_3().when(self.compact, |this| this.px_1p5()),
483                        _ => this.h_8().px_4().when(self.compact, |this| this.px_2()),
484                    }
485                }
486            })
487            .when(self.border_corners.top_left, |this| {
488                this.rounded_tl(rounding)
489            })
490            .when(self.border_corners.top_right, |this| {
491                this.rounded_tr(rounding)
492            })
493            .when(self.border_corners.bottom_left, |this| {
494                this.rounded_bl(rounding)
495            })
496            .when(self.border_corners.bottom_right, |this| {
497                this.rounded_br(rounding)
498            })
499            .when(self.border_edges.left, |this| this.border_l_1())
500            .when(self.border_edges.right, |this| this.border_r_1())
501            .when(self.border_edges.top, |this| this.border_t_1())
502            .when(self.border_edges.bottom, |this| this.border_b_1())
503            .text_color(normal_style.fg)
504            .when(self.selected, |this| {
505                let selected_style = style.selected(self.outline, cx);
506                this.bg(selected_style.bg)
507                    .border_color(selected_style.border)
508                    .text_color(selected_style.fg)
509            })
510            .when(!self.disabled && !self.selected, |this| {
511                this.border_color(normal_style.border)
512                    .bg(normal_style.bg)
513                    .when(normal_style.underline, |this| this.text_decoration_1())
514                    .hover(|this| {
515                        let hover_style = style.hovered(self.outline, cx);
516                        this.bg(hover_style.bg)
517                            .border_color(hover_style.border)
518                            .text_color(crate::red_400())
519                    })
520                    .active(|this| {
521                        let active_style = style.active(self.outline, cx);
522                        this.bg(active_style.bg)
523                            .border_color(active_style.border)
524                            .text_color(active_style.fg)
525                    })
526            })
527            .when(self.disabled, |this| {
528                let disabled_style = style.disabled(self.outline, cx);
529                this.bg(disabled_style.bg)
530                    .text_color(disabled_style.fg)
531                    .border_color(disabled_style.border)
532                    .shadow_none()
533            })
534            .refine_style(&self.style)
535            .on_mouse_down(MouseButton::Left, move |_, window, cx| {
536                // Stop handle any click event when disabled.
537                // To avoid handle dropdown menu open when button is disabled.
538                if is_disabled {
539                    cx.stop_propagation();
540                    return;
541                }
542
543                // Avoid focus on mouse down.
544                window.prevent_default();
545            })
546            .when_some(self.on_click, |this, on_click| {
547                this.on_click(move |event, window, cx| {
548                    // Stop handle any click event when disabled.
549                    // To avoid handle dropdown menu open when button is disabled.
550                    if !clickable {
551                        cx.stop_propagation();
552                        return;
553                    }
554
555                    (on_click)(event, window, cx);
556                })
557            })
558            .when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
559                this.on_hover(move |hovered, window, cx| {
560                    (on_hover)(hovered, window, cx);
561                })
562            })
563            .child({
564                h_flex()
565                    .id("label")
566                    .items_center()
567                    .justify_center()
568                    .button_text_size(self.size)
569                    .map(|this| match self.size {
570                        Size::XSmall => this.gap_1(),
571                        Size::Small => this.gap_1(),
572                        _ => this.gap_2(),
573                    })
574                    .when(!self.loading, |this| {
575                        this.when_some(self.icon, |this, icon| {
576                            this.child(icon.with_size(icon_size))
577                        })
578                    })
579                    .when(self.loading, |this| {
580                        this.child(
581                            Spinner::new()
582                                .with_size(self.size)
583                                .when_some(self.loading_icon, |this, icon| this.icon(icon)),
584                        )
585                    })
586                    .when_some(self.label, |this, label| {
587                        this.child(div().flex_none().line_height(relative(1.)).child(label))
588                    })
589                    .children(self.children)
590                    .when(self.dropdown_caret, |this| {
591                        this.child(Icon::new(IconName::ChevronDown).with_size(icon_size))
592                    })
593            })
594            .when(self.loading && !self.disabled, |this| {
595                this.bg(normal_style.bg.opacity(0.8))
596                    .border_color(normal_style.border.opacity(0.8))
597                    .text_color(normal_style.fg.opacity(0.8))
598            })
599            .when_some(self.tooltip, |this, (tooltip, action)| {
600                this.tooltip(move |window, cx| {
601                    Tooltip::new(tooltip.clone())
602                        .when_some(action.clone(), |this, (action, context)| {
603                            this.action(
604                                action.boxed_clone().as_ref(),
605                                context.as_ref().map(|c| c.as_ref()),
606                            )
607                        })
608                        .build(window, cx)
609                })
610            })
611            .focus_ring(is_focused, px(0.), window, cx)
612    }
613}
614
615struct ButtonVariantStyle {
616    bg: Hsla,
617    border: Hsla,
618    fg: Hsla,
619    underline: bool,
620    shadow: bool,
621}
622
623impl ButtonVariant {
624    fn bg_color(&self, outline: bool, cx: &mut App) -> Hsla {
625        if outline {
626            return cx.theme().background;
627        }
628
629        match self {
630            ButtonVariant::Primary => cx.theme().primary,
631            ButtonVariant::Secondary => cx.theme().secondary,
632            ButtonVariant::Danger => cx.theme().danger,
633            ButtonVariant::Warning => cx.theme().warning,
634            ButtonVariant::Success => cx.theme().success,
635            ButtonVariant::Info => cx.theme().info,
636            ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
637                cx.theme().transparent
638            }
639            ButtonVariant::Custom(colors) => colors.color,
640        }
641    }
642
643    fn text_color(&self, outline: bool, cx: &mut App) -> Hsla {
644        match self {
645            ButtonVariant::Primary => {
646                if outline {
647                    cx.theme().primary
648                } else {
649                    cx.theme().primary_foreground
650                }
651            }
652            ButtonVariant::Secondary | ButtonVariant::Ghost => cx.theme().secondary_foreground,
653            ButtonVariant::Danger => {
654                if outline {
655                    cx.theme().danger
656                } else {
657                    cx.theme().danger_foreground
658                }
659            }
660            ButtonVariant::Warning => {
661                if outline {
662                    cx.theme().warning
663                } else {
664                    cx.theme().warning_foreground
665                }
666            }
667            ButtonVariant::Success => {
668                if outline {
669                    cx.theme().success
670                } else {
671                    cx.theme().success_foreground
672                }
673            }
674            ButtonVariant::Info => {
675                if outline {
676                    cx.theme().info
677                } else {
678                    cx.theme().info_foreground
679                }
680            }
681            ButtonVariant::Link => cx.theme().link,
682            ButtonVariant::Text => cx.theme().foreground,
683            ButtonVariant::Custom(colors) => {
684                if outline {
685                    colors.color
686                } else {
687                    colors.foreground
688                }
689            }
690        }
691    }
692
693    fn border_color(&self, bg: Hsla, outline: bool, cx: &mut App) -> Hsla {
694        match self {
695            ButtonVariant::Secondary => {
696                if outline {
697                    cx.theme().border
698                } else {
699                    bg
700                }
701            }
702            ButtonVariant::Primary => {
703                if outline {
704                    cx.theme().primary
705                } else {
706                    bg
707                }
708            }
709            ButtonVariant::Danger => {
710                if outline {
711                    cx.theme().danger
712                } else {
713                    bg
714                }
715            }
716            ButtonVariant::Info => {
717                if outline {
718                    cx.theme().info
719                } else {
720                    bg
721                }
722            }
723            ButtonVariant::Warning => {
724                if outline {
725                    cx.theme().warning
726                } else {
727                    bg
728                }
729            }
730            ButtonVariant::Success => {
731                if outline {
732                    cx.theme().success
733                } else {
734                    bg
735                }
736            }
737            ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
738                cx.theme().transparent
739            }
740            ButtonVariant::Custom(colors) => colors.border,
741        }
742    }
743
744    fn underline(&self, _: &App) -> bool {
745        match self {
746            ButtonVariant::Link => true,
747            _ => false,
748        }
749    }
750
751    fn shadow(&self, outline: bool, _: &App) -> bool {
752        match self {
753            ButtonVariant::Primary | ButtonVariant::Secondary | ButtonVariant::Danger => outline,
754            ButtonVariant::Custom(c) => c.shadow,
755            _ => false,
756        }
757    }
758
759    fn normal(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
760        let bg = self.bg_color(outline, cx);
761        let border = self.border_color(bg, outline, cx);
762        let fg = self.text_color(outline, cx);
763        let underline = self.underline(cx);
764        let shadow = self.shadow(outline, cx);
765
766        ButtonVariantStyle {
767            bg,
768            border,
769            fg,
770            underline,
771            shadow,
772        }
773    }
774
775    fn hovered(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
776        let bg = match self {
777            ButtonVariant::Primary => {
778                if outline {
779                    cx.theme().secondary_hover
780                } else {
781                    cx.theme().primary_hover
782                }
783            }
784            ButtonVariant::Secondary => cx.theme().secondary_hover,
785            ButtonVariant::Danger => {
786                if outline {
787                    cx.theme().secondary_hover
788                } else {
789                    cx.theme().danger_hover
790                }
791            }
792            ButtonVariant::Warning => {
793                if outline {
794                    cx.theme().secondary_hover
795                } else {
796                    cx.theme().warning_hover
797                }
798            }
799            ButtonVariant::Success => {
800                if outline {
801                    cx.theme().secondary_hover
802                } else {
803                    cx.theme().success_hover
804                }
805            }
806            ButtonVariant::Info => {
807                if outline {
808                    cx.theme().secondary_hover
809                } else {
810                    cx.theme().info_hover
811                }
812            }
813            ButtonVariant::Ghost => {
814                if cx.theme().mode.is_dark() {
815                    cx.theme().secondary.lighten(0.1).opacity(0.8)
816                } else {
817                    cx.theme().secondary.darken(0.1).opacity(0.8)
818                }
819            }
820            ButtonVariant::Link => cx.theme().transparent,
821            ButtonVariant::Text => cx.theme().transparent,
822            ButtonVariant::Custom(colors) => {
823                if outline {
824                    cx.theme().secondary_hover
825                } else {
826                    colors.hover
827                }
828            }
829        };
830
831        let border = self.border_color(bg, outline, cx);
832        let fg = match self {
833            ButtonVariant::Link => cx.theme().link_hover,
834            _ => self.text_color(outline, cx),
835        };
836
837        let underline = self.underline(cx);
838        let shadow = self.shadow(outline, cx);
839
840        ButtonVariantStyle {
841            bg,
842            border,
843            fg,
844            underline,
845            shadow,
846        }
847    }
848
849    fn active(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
850        let bg = match self {
851            ButtonVariant::Primary => {
852                if outline {
853                    cx.theme().primary_active.opacity(0.1)
854                } else {
855                    cx.theme().primary_active
856                }
857            }
858            ButtonVariant::Secondary => cx.theme().secondary_active,
859            ButtonVariant::Ghost => {
860                if cx.theme().mode.is_dark() {
861                    cx.theme().secondary.lighten(0.2).opacity(0.8)
862                } else {
863                    cx.theme().secondary.darken(0.2).opacity(0.8)
864                }
865            }
866            ButtonVariant::Danger => {
867                if outline {
868                    cx.theme().danger_active.opacity(0.1)
869                } else {
870                    cx.theme().danger_active
871                }
872            }
873            ButtonVariant::Warning => {
874                if outline {
875                    cx.theme().warning_active.opacity(0.1)
876                } else {
877                    cx.theme().warning_active
878                }
879            }
880            ButtonVariant::Success => {
881                if outline {
882                    cx.theme().success_active.opacity(0.1)
883                } else {
884                    cx.theme().success_active
885                }
886            }
887            ButtonVariant::Info => {
888                if outline {
889                    cx.theme().info_active.opacity(0.1)
890                } else {
891                    cx.theme().info_active
892                }
893            }
894            ButtonVariant::Link => cx.theme().transparent,
895            ButtonVariant::Text => cx.theme().transparent,
896            ButtonVariant::Custom(colors) => {
897                if outline {
898                    colors.active.opacity(0.1)
899                } else {
900                    colors.active
901                }
902            }
903        };
904        let border = self.border_color(bg, outline, cx);
905        let fg = match self {
906            ButtonVariant::Link => cx.theme().link_active,
907            ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
908            _ => self.text_color(outline, cx),
909        };
910        let underline = self.underline(cx);
911        let shadow = self.shadow(outline, cx);
912
913        ButtonVariantStyle {
914            bg,
915            border,
916            fg,
917            underline,
918            shadow,
919        }
920    }
921
922    fn selected(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
923        let bg = match self {
924            ButtonVariant::Primary => cx.theme().primary_active,
925            ButtonVariant::Secondary | ButtonVariant::Ghost => cx.theme().secondary_active,
926            ButtonVariant::Danger => cx.theme().danger_active,
927            ButtonVariant::Warning => cx.theme().warning_active,
928            ButtonVariant::Success => cx.theme().success_active,
929            ButtonVariant::Info => cx.theme().info_active,
930            ButtonVariant::Link => cx.theme().transparent,
931            ButtonVariant::Text => cx.theme().transparent,
932            ButtonVariant::Custom(colors) => colors.active,
933        };
934
935        let border = self.border_color(bg, outline, cx);
936        let fg = match self {
937            ButtonVariant::Link => cx.theme().link_active,
938            ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
939            _ => self.text_color(false, cx),
940        };
941        let underline = self.underline(cx);
942        let shadow = self.shadow(outline, cx);
943
944        ButtonVariantStyle {
945            bg,
946            border,
947            fg,
948            underline,
949            shadow,
950        }
951    }
952
953    fn disabled(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
954        let bg = match self {
955            ButtonVariant::Link | ButtonVariant::Ghost | ButtonVariant::Text => {
956                cx.theme().transparent
957            }
958            ButtonVariant::Primary => cx.theme().primary.opacity(0.15),
959            ButtonVariant::Danger => cx.theme().danger.opacity(0.15),
960            ButtonVariant::Warning => cx.theme().warning.opacity(0.15),
961            ButtonVariant::Success => cx.theme().success.opacity(0.15),
962            ButtonVariant::Info => cx.theme().info.opacity(0.15),
963            ButtonVariant::Secondary => cx.theme().secondary.opacity(1.5),
964            ButtonVariant::Custom(style) => style.color.opacity(0.15),
965        };
966        let fg = cx.theme().muted_foreground.opacity(0.5);
967        let (bg, border) = if outline {
968            (cx.theme().transparent, cx.theme().border.opacity(0.5))
969        } else {
970            (bg, bg)
971        };
972
973        let underline = self.underline(cx);
974        let shadow = false;
975
976        ButtonVariantStyle {
977            bg,
978            border,
979            fg,
980            underline,
981            shadow,
982        }
983    }
984}