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 fn primary(self) -> Self {
47 self.with_variant(ButtonVariant::Primary)
48 }
49
50 fn secondary(self) -> Self {
52 self.with_variant(ButtonVariant::Secondary)
53 }
54
55 fn danger(self) -> Self {
57 self.with_variant(ButtonVariant::Danger)
58 }
59
60 fn warning(self) -> Self {
62 self.with_variant(ButtonVariant::Warning)
63 }
64
65 fn success(self) -> Self {
67 self.with_variant(ButtonVariant::Success)
68 }
69
70 fn info(self) -> Self {
72 self.with_variant(ButtonVariant::Info)
73 }
74
75 fn ghost(self) -> Self {
77 self.with_variant(ButtonVariant::Ghost)
78 }
79
80 fn link(self) -> Self {
82 self.with_variant(ButtonVariant::Link)
83 }
84
85 fn text(self) -> Self {
87 self.with_variant(ButtonVariant::Text)
88 }
89
90 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 pub fn color(mut self, color: Hsla) -> Self {
109 self.color = color;
110 self
111 }
112
113 pub fn foreground(mut self, color: Hsla) -> Self {
115 self.foreground = 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, 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#[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 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 pub fn outline(mut self) -> Self {
262 self.outline = true;
263 self
264 }
265
266 pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self {
268 self.rounded = rounded.into();
269 self
270 }
271
272 pub fn border_corners(mut self, corners: impl Into<Corners<bool>>) -> Self {
274 self.border_corners = corners.into();
275 self
276 }
277
278 pub fn border_edges(mut self, edges: impl Into<Edges<bool>>) -> Self {
280 self.border_edges = edges.into();
281 self
282 }
283
284 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
286 self.label = Some(label.into());
287 self
288 }
289
290 pub fn icon(mut self, icon: impl Into<ButtonIcon>) -> Self {
292 self.icon = Some(icon.into());
293 self
294 }
295
296 pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
298 self.tooltip = Some((tooltip.into(), None));
299 self
300 }
301
302 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 pub fn loading(mut self, loading: bool) -> Self {
321 self.loading = loading;
322 self
323 }
324
325 pub fn compact(mut self) -> Self {
327 self.compact = true;
328 self
329 }
330
331 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 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 pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
350 self.loading_icon = Some(icon.into());
351 self
352 }
353
354 pub fn tab_index(mut self, tab_index: isize) -> Self {
358 self.tab_index = tab_index;
359 self
360 }
361
362 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
366 self.tab_stop = tab_stop;
367 self
368 }
369
370 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 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 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 if is_disabled {
560 cx.stop_propagation();
561 return;
562 }
563
564 window.prevent_default();
566
567 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 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 let clickable = Button::new("test").on_click(|_, _, _| {});
1007 assert!(clickable.clickable());
1008
1009 let disabled = Button::new("test").disabled(true).on_click(|_, _, _| {});
1011 assert!(!disabled.clickable());
1012
1013 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 assert!(ButtonVariant::Link.is_link());
1022 assert!(ButtonVariant::Text.is_text());
1023 assert!(ButtonVariant::Ghost.is_ghost());
1024
1025 assert!(ButtonVariant::Link.no_padding());
1027 assert!(ButtonVariant::Text.no_padding());
1028 assert!(!ButtonVariant::Ghost.no_padding());
1029 }
1030}