1use std::rc::Rc;
2
3use crate::{
4 h_flex, spinner::Spinner, 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 fn primary(self) -> Self {
45 self.with_variant(ButtonVariant::Primary)
46 }
47
48 fn danger(self) -> Self {
50 self.with_variant(ButtonVariant::Danger)
51 }
52
53 fn warning(self) -> Self {
55 self.with_variant(ButtonVariant::Warning)
56 }
57
58 fn success(self) -> Self {
60 self.with_variant(ButtonVariant::Success)
61 }
62
63 fn info(self) -> Self {
65 self.with_variant(ButtonVariant::Info)
66 }
67
68 fn ghost(self) -> Self {
70 self.with_variant(ButtonVariant::Ghost)
71 }
72
73 fn link(self) -> Self {
75 self.with_variant(ButtonVariant::Link)
76 }
77
78 fn text(self) -> Self {
80 self.with_variant(ButtonVariant::Text)
81 }
82
83 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 pub fn color(mut self, color: Hsla) -> Self {
103 self.color = color;
104 self
105 }
106
107 pub fn foreground(mut self, color: Hsla) -> Self {
109 self.foreground = color;
110 self
111 }
112
113 pub fn border(mut self, color: Hsla) -> Self {
115 self.border = color;
116 self
117 }
118
119 pub fn hover(mut self, color: Hsla) -> Self {
121 self.hover = color;
122 self
123 }
124
125 pub fn active(mut self, color: Hsla) -> Self {
127 self.active = color;
128 self
129 }
130
131 pub fn shadow(mut self, shadow: bool) -> Self {
133 self.shadow = shadow;
134 self
135 }
136}
137
138#[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#[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 size: Size,
193 compact: bool,
194 tooltip: Option<(
195 SharedString,
196 Option<(Rc<Box<dyn Action>>, Option<SharedString>)>,
197 )>,
198 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
199 on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
200 loading: bool,
201 loading_icon: Option<Icon>,
202
203 tab_index: isize,
204 tab_stop: bool,
205}
206
207impl From<Button> for AnyElement {
208 fn from(button: Button) -> Self {
209 button.into_any_element()
210 }
211}
212
213impl Button {
214 pub fn new(id: impl Into<ElementId>) -> Self {
215 let id = id.into();
216
217 Self {
218 id: id.clone(),
219 base: div().flex_shrink_0().id(id),
222 style: StyleRefinement::default(),
223 icon: None,
224 label: None,
225 disabled: false,
226 selected: false,
227 variant: ButtonVariant::default(),
228 rounded: ButtonRounded::Medium,
229 border_corners: Corners::all(true),
230 border_edges: Edges::all(true),
231 size: Size::Medium,
232 tooltip: None,
233 on_click: None,
234 on_hover: None,
235 loading: false,
236 compact: false,
237 outline: false,
238 children: Vec::new(),
239 loading_icon: None,
240 tab_index: 0,
241 tab_stop: true,
242 }
243 }
244
245 pub fn outline(mut self) -> Self {
247 self.outline = true;
248 self
249 }
250
251 pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self {
253 self.rounded = rounded.into();
254 self
255 }
256
257 pub(crate) fn border_corners(mut self, corners: impl Into<Corners<bool>>) -> Self {
259 self.border_corners = corners.into();
260 self
261 }
262
263 pub(crate) fn border_edges(mut self, edges: impl Into<Edges<bool>>) -> Self {
265 self.border_edges = edges.into();
266 self
267 }
268
269 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
271 self.label = Some(label.into());
272 self
273 }
274
275 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
277 self.icon = Some(icon.into());
278 self
279 }
280
281 pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
283 self.tooltip = Some((tooltip.into(), None));
284 self
285 }
286
287 pub fn tooltip_with_action(
289 mut self,
290 tooltip: impl Into<SharedString>,
291 action: &dyn Action,
292 context: Option<&str>,
293 ) -> Self {
294 self.tooltip = Some((
295 tooltip.into(),
296 Some((
297 Rc::new(action.boxed_clone()),
298 context.map(|c| c.to_string().into()),
299 )),
300 ));
301 self
302 }
303
304 pub fn loading(mut self, loading: bool) -> Self {
306 self.loading = loading;
307 self
308 }
309
310 pub fn compact(mut self) -> Self {
312 self.compact = true;
313 self
314 }
315
316 pub fn on_click(
318 mut self,
319 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
320 ) -> Self {
321 self.on_click = Some(Rc::new(handler));
322 self
323 }
324
325 pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
327 self.on_hover = Some(Rc::new(handler));
328 self
329 }
330
331 pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
335 self.loading_icon = Some(icon.into());
336 self
337 }
338
339 pub fn tab_index(mut self, tab_index: isize) -> Self {
343 self.tab_index = tab_index;
344 self
345 }
346
347 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
351 self.tab_stop = tab_stop;
352 self
353 }
354
355 #[inline]
356 fn clickable(&self) -> bool {
357 !(self.disabled || self.loading) && self.on_click.is_some()
358 }
359
360 #[inline]
361 fn hoverable(&self) -> bool {
362 !(self.disabled || self.loading) && self.on_hover.is_some()
363 }
364}
365
366impl Disableable for Button {
367 fn disabled(mut self, disabled: bool) -> Self {
368 self.disabled = disabled;
369 self
370 }
371}
372
373impl Selectable for Button {
374 fn selected(mut self, selected: bool) -> Self {
375 self.selected = selected;
376 self
377 }
378
379 fn is_selected(&self) -> bool {
380 self.selected
381 }
382}
383
384impl Sizable for Button {
385 fn with_size(mut self, size: impl Into<Size>) -> Self {
386 self.size = size.into();
387 self
388 }
389}
390
391impl ButtonVariants for Button {
392 fn with_variant(mut self, variant: ButtonVariant) -> Self {
393 self.variant = variant;
394 self
395 }
396}
397
398impl Styled for Button {
399 fn style(&mut self) -> &mut StyleRefinement {
400 &mut self.style
401 }
402}
403
404impl ParentElement for Button {
405 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
406 self.children.extend(elements)
407 }
408}
409
410impl InteractiveElement for Button {
411 fn interactivity(&mut self) -> &mut Interactivity {
412 self.base.interactivity()
413 }
414}
415
416impl RenderOnce for Button {
417 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
418 let style: ButtonVariant = self.variant;
419 let clickable = self.clickable();
420 let hoverable = self.hoverable();
421 let normal_style = style.normal(self.outline, cx);
422 let icon_size = match self.size {
423 Size::Size(v) => Size::Size(v * 0.75),
424 _ => self.size,
425 };
426
427 let focus_handle = window
428 .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
429 .read(cx)
430 .clone();
431 let is_focused = focus_handle.is_focused(window);
432
433 let rounding = match self.rounded {
434 ButtonRounded::Small => cx.theme().radius * 0.5,
435 ButtonRounded::Medium => cx.theme().radius,
436 ButtonRounded::Large => cx.theme().radius * 2.0,
437 ButtonRounded::Size(px) => px,
438 ButtonRounded::None => Pixels::ZERO,
439 };
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 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 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(self.border_corners.top_left, |this| this.rounded_tl(rounding))
479 .when(self.border_corners.top_right, |this| this.rounded_tr(rounding))
480 .when(self.border_corners.bottom_left, |this| this.rounded_bl(rounding))
481 .when(self.border_corners.bottom_right, |this| this.rounded_br(rounding))
482 .when(self.border_edges.left, |this| this.border_l_1())
483 .when(self.border_edges.right, |this| this.border_r_1())
484 .when(self.border_edges.top, |this| this.border_t_1())
485 .when(self.border_edges.bottom, |this| this.border_b_1())
486 .text_color(normal_style.fg)
487 .when(self.selected, |this| {
488 let selected_style = style.selected(self.outline, cx);
489 this.bg(selected_style.bg)
490 .border_color(selected_style.border)
491 .text_color(selected_style.fg)
492 })
493 .when(!self.disabled && !self.selected, |this| {
494 this.border_color(normal_style.border)
495 .bg(normal_style.bg)
496 .when(normal_style.underline, |this| this.text_decoration_1())
497 .hover(|this| {
498 let hover_style = style.hovered(self.outline, cx);
499 this.bg(hover_style.bg)
500 .border_color(hover_style.border)
501 .text_color(crate::red_400())
502 })
503 .active(|this| {
504 let active_style = style.active(self.outline, cx);
505 this.bg(active_style.bg)
506 .border_color(active_style.border)
507 .text_color(active_style.fg)
508 })
509 })
510 .when(self.disabled, |this| {
511 let disabled_style = style.disabled(self.outline, cx);
512 this.bg(disabled_style.bg)
513 .text_color(disabled_style.fg)
514 .border_color(disabled_style.border)
515 .shadow_none()
516 })
517 .refine_style(&self.style)
518 .on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
519 window.prevent_default();
521 })
522 .when_some(self.on_click.filter(|_| clickable), |this, on_click| {
523 this.on_click(move |event, window, cx| {
524 (on_click)(event, window, cx);
525 })
526 })
527 .when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
528 this.on_hover(move |hovered, window, cx| {
529 (on_hover)(hovered, window, cx);
530 })
531 })
532 .child({
533 h_flex()
534 .id("label")
535 .items_center()
536 .justify_center()
537 .button_text_size(self.size)
538 .map(|this| match self.size {
539 Size::XSmall => this.gap_1(),
540 Size::Small => this.gap_1(),
541 _ => this.gap_2(),
542 })
543 .when(!self.loading, |this| {
544 this.when_some(self.icon, |this, icon| {
545 this.child(icon.with_size(icon_size))
546 })
547 })
548 .when(self.loading, |this| {
549 this.child(
550 Spinner::new()
551 .with_size(self.size)
552 .when_some(self.loading_icon, |this, icon| this.icon(icon)),
553 )
554 })
555 .when_some(self.label, |this, label| {
556 this.child(div().flex_none().line_height(relative(1.)).child(label))
557 })
558 .children(self.children)
559 })
560 .when(self.loading && !self.disabled, |this| {
561 this.bg(normal_style.bg.opacity(0.8))
562 .border_color(normal_style.border.opacity(0.8))
563 .text_color(normal_style.fg.opacity(0.8))
564 })
565 .when_some(self.tooltip, |this, (tooltip, action)| {
566 this.tooltip(move |window, cx| {
567 Tooltip::new(tooltip.clone())
568 .when_some(action.clone(), |this, (action, context)| {
569 this.action(
570 action.boxed_clone().as_ref(),
571 context.as_ref().map(|c| c.as_ref()),
572 )
573 })
574 .build(window, cx)
575 })
576 })
577 .focus_ring(is_focused, px(0.), window, cx)
578 }
579}
580
581struct ButtonVariantStyle {
582 bg: Hsla,
583 border: Hsla,
584 fg: Hsla,
585 underline: bool,
586 shadow: bool,
587}
588
589impl ButtonVariant {
590 fn bg_color(&self, outline: bool, cx: &mut App) -> Hsla {
591 if outline {
592 return cx.theme().background;
593 }
594
595 match self {
596 ButtonVariant::Primary => cx.theme().primary,
597 ButtonVariant::Secondary => cx.theme().secondary,
598 ButtonVariant::Danger => cx.theme().danger,
599 ButtonVariant::Warning => cx.theme().warning,
600 ButtonVariant::Success => cx.theme().success,
601 ButtonVariant::Info => cx.theme().info,
602 ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
603 cx.theme().transparent
604 }
605 ButtonVariant::Custom(colors) => colors.color,
606 }
607 }
608
609 fn text_color(&self, outline: bool, cx: &mut App) -> Hsla {
610 match self {
611 ButtonVariant::Primary => {
612 if outline {
613 cx.theme().primary
614 } else {
615 cx.theme().primary_foreground
616 }
617 }
618 ButtonVariant::Secondary | ButtonVariant::Ghost => cx.theme().secondary_foreground,
619 ButtonVariant::Danger => {
620 if outline {
621 cx.theme().danger
622 } else {
623 cx.theme().danger_foreground
624 }
625 }
626 ButtonVariant::Warning => {
627 if outline {
628 cx.theme().warning
629 } else {
630 cx.theme().warning_foreground
631 }
632 }
633 ButtonVariant::Success => {
634 if outline {
635 cx.theme().success
636 } else {
637 cx.theme().success_foreground
638 }
639 }
640 ButtonVariant::Info => {
641 if outline {
642 cx.theme().info
643 } else {
644 cx.theme().info_foreground
645 }
646 }
647 ButtonVariant::Link => cx.theme().link,
648 ButtonVariant::Text => cx.theme().foreground,
649 ButtonVariant::Custom(colors) => {
650 if outline {
651 colors.color
652 } else {
653 colors.foreground
654 }
655 }
656 }
657 }
658
659 fn border_color(&self, bg: Hsla, outline: bool, cx: &mut App) -> Hsla {
660 match self {
661 ButtonVariant::Secondary => {
662 if outline {
663 cx.theme().border
664 } else {
665 bg
666 }
667 }
668 ButtonVariant::Primary => {
669 if outline {
670 cx.theme().primary
671 } else {
672 bg
673 }
674 }
675 ButtonVariant::Danger => {
676 if outline {
677 cx.theme().danger
678 } else {
679 bg
680 }
681 }
682 ButtonVariant::Info => {
683 if outline {
684 cx.theme().info
685 } else {
686 bg
687 }
688 }
689 ButtonVariant::Warning => {
690 if outline {
691 cx.theme().warning
692 } else {
693 bg
694 }
695 }
696 ButtonVariant::Success => {
697 if outline {
698 cx.theme().success
699 } else {
700 bg
701 }
702 }
703 ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
704 cx.theme().transparent
705 }
706 ButtonVariant::Custom(colors) => colors.border,
707 }
708 }
709
710 fn underline(&self, _: &App) -> bool {
711 match self {
712 ButtonVariant::Link => true,
713 _ => false,
714 }
715 }
716
717 fn shadow(&self, outline: bool, _: &App) -> bool {
718 match self {
719 ButtonVariant::Primary | ButtonVariant::Secondary | ButtonVariant::Danger => outline,
720 ButtonVariant::Custom(c) => c.shadow,
721 _ => false,
722 }
723 }
724
725 fn normal(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
726 let bg = self.bg_color(outline, cx);
727 let border = self.border_color(bg, outline, cx);
728 let fg = self.text_color(outline, cx);
729 let underline = self.underline(cx);
730 let shadow = self.shadow(outline, cx);
731
732 ButtonVariantStyle {
733 bg,
734 border,
735 fg,
736 underline,
737 shadow,
738 }
739 }
740
741 fn hovered(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
742 let bg = match self {
743 ButtonVariant::Primary => {
744 if outline {
745 cx.theme().secondary_hover
746 } else {
747 cx.theme().primary_hover
748 }
749 }
750 ButtonVariant::Secondary => cx.theme().secondary_hover,
751 ButtonVariant::Danger => {
752 if outline {
753 cx.theme().secondary_hover
754 } else {
755 cx.theme().danger_hover
756 }
757 }
758 ButtonVariant::Warning => {
759 if outline {
760 cx.theme().secondary_hover
761 } else {
762 cx.theme().warning_hover
763 }
764 }
765 ButtonVariant::Success => {
766 if outline {
767 cx.theme().secondary_hover
768 } else {
769 cx.theme().success_hover
770 }
771 }
772 ButtonVariant::Info => {
773 if outline {
774 cx.theme().secondary_hover
775 } else {
776 cx.theme().info_hover
777 }
778 }
779 ButtonVariant::Ghost => {
780 if cx.theme().mode.is_dark() {
781 cx.theme().secondary.lighten(0.1).opacity(0.8)
782 } else {
783 cx.theme().secondary.darken(0.1).opacity(0.8)
784 }
785 }
786 ButtonVariant::Link => cx.theme().transparent,
787 ButtonVariant::Text => cx.theme().transparent,
788 ButtonVariant::Custom(colors) => {
789 if outline {
790 cx.theme().secondary_hover
791 } else {
792 colors.hover
793 }
794 }
795 };
796
797 let border = self.border_color(bg, outline, cx);
798 let fg = match self {
799 ButtonVariant::Link => cx.theme().link_hover,
800 _ => self.text_color(outline, cx),
801 };
802
803 let underline = self.underline(cx);
804 let shadow = self.shadow(outline, cx);
805
806 ButtonVariantStyle {
807 bg,
808 border,
809 fg,
810 underline,
811 shadow,
812 }
813 }
814
815 fn active(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
816 let bg = match self {
817 ButtonVariant::Primary => {
818 if outline {
819 cx.theme().primary_active.opacity(0.1)
820 } else {
821 cx.theme().primary_active
822 }
823 }
824 ButtonVariant::Secondary => cx.theme().secondary_active,
825 ButtonVariant::Ghost => {
826 if cx.theme().mode.is_dark() {
827 cx.theme().secondary.lighten(0.2).opacity(0.8)
828 } else {
829 cx.theme().secondary.darken(0.2).opacity(0.8)
830 }
831 }
832 ButtonVariant::Danger => {
833 if outline {
834 cx.theme().danger_active.opacity(0.1)
835 } else {
836 cx.theme().danger_active
837 }
838 }
839 ButtonVariant::Warning => {
840 if outline {
841 cx.theme().warning_active.opacity(0.1)
842 } else {
843 cx.theme().warning_active
844 }
845 }
846 ButtonVariant::Success => {
847 if outline {
848 cx.theme().success_active.opacity(0.1)
849 } else {
850 cx.theme().success_active
851 }
852 }
853 ButtonVariant::Info => {
854 if outline {
855 cx.theme().info_active.opacity(0.1)
856 } else {
857 cx.theme().info_active
858 }
859 }
860 ButtonVariant::Link => cx.theme().transparent,
861 ButtonVariant::Text => cx.theme().transparent,
862 ButtonVariant::Custom(colors) => {
863 if outline {
864 colors.active.opacity(0.1)
865 } else {
866 colors.active
867 }
868 }
869 };
870 let border = self.border_color(bg, outline, cx);
871 let fg = match self {
872 ButtonVariant::Link => cx.theme().link_active,
873 ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
874 _ => self.text_color(outline, cx),
875 };
876 let underline = self.underline(cx);
877 let shadow = self.shadow(outline, cx);
878
879 ButtonVariantStyle {
880 bg,
881 border,
882 fg,
883 underline,
884 shadow,
885 }
886 }
887
888 fn selected(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
889 let bg = match self {
890 ButtonVariant::Primary => cx.theme().primary_active,
891 ButtonVariant::Secondary | ButtonVariant::Ghost => cx.theme().secondary_active,
892 ButtonVariant::Danger => cx.theme().danger_active,
893 ButtonVariant::Warning => cx.theme().warning_active,
894 ButtonVariant::Success => cx.theme().success_active,
895 ButtonVariant::Info => cx.theme().info_active,
896 ButtonVariant::Link => cx.theme().transparent,
897 ButtonVariant::Text => cx.theme().transparent,
898 ButtonVariant::Custom(colors) => colors.active,
899 };
900
901 let border = self.border_color(bg, outline, cx);
902 let fg = match self {
903 ButtonVariant::Link => cx.theme().link_active,
904 ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
905 _ => self.text_color(false, cx),
906 };
907 let underline = self.underline(cx);
908 let shadow = self.shadow(outline, cx);
909
910 ButtonVariantStyle {
911 bg,
912 border,
913 fg,
914 underline,
915 shadow,
916 }
917 }
918
919 fn disabled(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
920 let bg = match self {
921 ButtonVariant::Link | ButtonVariant::Ghost | ButtonVariant::Text => {
922 cx.theme().transparent
923 }
924 ButtonVariant::Primary => cx.theme().primary.opacity(0.15),
925 ButtonVariant::Danger => cx.theme().danger.opacity(0.15),
926 ButtonVariant::Warning => cx.theme().warning.opacity(0.15),
927 ButtonVariant::Success => cx.theme().success.opacity(0.15),
928 ButtonVariant::Info => cx.theme().info.opacity(0.15),
929 ButtonVariant::Secondary => cx.theme().secondary.opacity(1.5),
930 ButtonVariant::Custom(style) => style.color.opacity(0.15),
931 };
932 let fg = cx.theme().muted_foreground.opacity(0.5);
933 let (bg, border) = if outline {
934 (cx.theme().transparent, cx.theme().border.opacity(0.5))
935 } else {
936 (bg, bg)
937 };
938
939 let underline = self.underline(cx);
940 let shadow = false;
941
942 ButtonVariantStyle {
943 bg,
944 border,
945 fg,
946 underline,
947 shadow,
948 }
949 }
950}