Skip to main content

rgpui_component/button/
button.rs

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