gpui_component/button/
button.rs

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