1use std::rc::Rc;
2
3use crate::{ActiveTheme, Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
4use rgpui::prelude::FluentBuilder as _;
5use rgpui::{
6 AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton,
7 ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
8 div, px, relative,
9};
10
11#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
13pub enum TabVariant {
14 #[default]
15 Tab,
16 Outline,
17 Pill,
18 Segmented,
19 Underline,
20}
21
22impl TabVariant {
23 fn height(&self, size: Size) -> Pixels {
24 match size {
25 Size::XSmall => match self {
26 TabVariant::Underline => px(26.),
27 _ => px(20.),
28 },
29 Size::Small => match self {
30 TabVariant::Underline => px(30.),
31 _ => px(24.),
32 },
33 Size::Large => match self {
34 TabVariant::Underline => px(44.),
35 _ => px(36.),
36 },
37 _ => match self {
38 TabVariant::Underline => px(36.),
39 _ => px(32.),
40 },
41 }
42 }
43
44 pub(super) fn inner_height(&self, size: Size) -> Pixels {
45 match size {
46 Size::XSmall => match self {
47 TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
48 TabVariant::Segmented => px(16.),
49 TabVariant::Underline => px(20.),
50 },
51 Size::Small => match self {
52 TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
53 TabVariant::Segmented => px(18.),
54 TabVariant::Underline => px(22.),
55 },
56 Size::Large => match self {
57 TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
58 TabVariant::Segmented => px(28.),
59 TabVariant::Underline => px(32.),
60 },
61 _ => match self {
62 TabVariant::Tab => px(30.),
63 TabVariant::Outline | TabVariant::Pill => px(26.),
64 TabVariant::Segmented => px(24.),
65 TabVariant::Underline => px(26.),
66 },
67 }
68 }
69
70 fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
72 let mut padding_x = match size {
73 Size::XSmall => px(8.),
74 Size::Small => px(10.),
75 Size::Large => px(16.),
76 _ => px(12.),
77 };
78
79 if matches!(self, TabVariant::Underline) {
80 padding_x = px(0.);
81 }
82
83 Edges {
84 left: padding_x,
85 right: padding_x,
86 ..Default::default()
87 }
88 }
89
90 fn inner_margins(&self, size: Size) -> Edges<Pixels> {
91 match size {
92 Size::XSmall => match self {
93 TabVariant::Underline => Edges {
94 top: px(1.),
95 bottom: px(2.),
96 ..Default::default()
97 },
98 _ => Edges::all(px(0.)),
99 },
100 Size::Small => match self {
101 TabVariant::Underline => Edges {
102 top: px(2.),
103 bottom: px(3.),
104 ..Default::default()
105 },
106 _ => Edges::all(px(0.)),
107 },
108 Size::Large => match self {
109 TabVariant::Underline => Edges {
110 top: px(5.),
111 bottom: px(6.),
112 ..Default::default()
113 },
114 _ => Edges::all(px(0.)),
115 },
116 _ => match self {
117 TabVariant::Underline => Edges {
118 top: px(3.),
119 bottom: px(4.),
120 ..Default::default()
121 },
122 _ => Edges::all(px(0.)),
123 },
124 }
125 }
126
127 fn normal(&self, cx: &App) -> TabStyle {
128 match self {
129 TabVariant::Tab => TabStyle {
130 fg: cx.theme().tab_foreground,
131 bg: cx.theme().transparent,
132 borders: Edges {
133 left: px(1.),
134 right: px(1.),
135 ..Default::default()
136 },
137 border_color: cx.theme().transparent,
138 ..Default::default()
139 },
140 TabVariant::Outline => TabStyle {
141 fg: cx.theme().tab_foreground,
142 bg: cx.theme().transparent,
143 borders: Edges::all(px(1.)),
144 border_color: cx.theme().border,
145 ..Default::default()
146 },
147 TabVariant::Pill => TabStyle {
148 fg: cx.theme().foreground,
149 bg: cx.theme().transparent,
150 ..Default::default()
151 },
152 TabVariant::Segmented => TabStyle {
153 fg: cx.theme().tab_foreground,
154 bg: cx.theme().transparent,
155 ..Default::default()
156 },
157 TabVariant::Underline => TabStyle {
158 fg: cx.theme().tab_foreground,
159 bg: cx.theme().transparent,
160 inner_bg: cx.theme().transparent,
161 borders: Edges {
162 bottom: px(2.),
163 ..Default::default()
164 },
165 border_color: cx.theme().transparent,
166 ..Default::default()
167 },
168 }
169 }
170
171 fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
172 match self {
173 TabVariant::Tab => TabStyle {
174 fg: cx.theme().tab_active_foreground,
175 bg: cx.theme().transparent,
176 borders: Edges {
177 left: px(1.),
178 right: px(1.),
179 ..Default::default()
180 },
181 border_color: cx.theme().transparent,
182 ..Default::default()
183 },
184 TabVariant::Outline => TabStyle {
185 fg: cx.theme().secondary_foreground,
186 bg: cx.theme().secondary_hover,
187 borders: Edges::all(px(1.)),
188 border_color: cx.theme().border,
189 ..Default::default()
190 },
191 TabVariant::Pill => TabStyle {
192 fg: cx.theme().secondary_foreground,
193 bg: cx.theme().secondary,
194 ..Default::default()
195 },
196 TabVariant::Segmented => TabStyle {
197 fg: cx.theme().tab_active_foreground,
198 bg: cx.theme().transparent,
199 inner_bg: if selected {
200 cx.theme().background
201 } else {
202 cx.theme().transparent
203 },
204 ..Default::default()
205 },
206 TabVariant::Underline => TabStyle {
207 fg: cx.theme().tab_active_foreground,
208 bg: cx.theme().transparent,
209 inner_bg: cx.theme().transparent,
210 borders: Edges {
211 bottom: px(2.),
212 ..Default::default()
213 },
214 border_color: cx.theme().transparent,
215 ..Default::default()
216 },
217 }
218 }
219
220 fn selected(&self, cx: &App) -> TabStyle {
221 match self {
222 TabVariant::Tab => TabStyle {
223 fg: cx.theme().tab_active_foreground,
224 bg: cx.theme().tab_active,
225 borders: Edges {
226 left: px(1.),
227 right: px(1.),
228 ..Default::default()
229 },
230 border_color: cx.theme().border,
231 ..Default::default()
232 },
233 TabVariant::Outline => TabStyle {
234 fg: cx.theme().primary,
235 bg: cx.theme().transparent,
236 borders: Edges::all(px(1.)),
237 border_color: cx.theme().primary,
238 ..Default::default()
239 },
240 TabVariant::Pill => TabStyle {
241 fg: cx.theme().primary_foreground,
242 bg: cx.theme().primary,
243 ..Default::default()
244 },
245 TabVariant::Segmented => TabStyle {
246 fg: cx.theme().tab_active_foreground,
247 bg: cx.theme().transparent,
248 inner_bg: cx.theme().background,
249 shadow: true,
250 ..Default::default()
251 },
252 TabVariant::Underline => TabStyle {
253 fg: cx.theme().tab_active_foreground,
254 bg: cx.theme().transparent,
255 borders: Edges {
256 bottom: px(2.),
257 ..Default::default()
258 },
259 border_color: cx.theme().primary,
260 ..Default::default()
261 },
262 }
263 }
264
265 fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
266 match self {
267 TabVariant::Tab => TabStyle {
268 fg: cx.theme().muted_foreground,
269 bg: cx.theme().transparent,
270 border_color: if selected {
271 cx.theme().border
272 } else {
273 cx.theme().transparent
274 },
275 borders: Edges {
276 left: px(1.),
277 right: px(1.),
278 ..Default::default()
279 },
280 ..Default::default()
281 },
282 TabVariant::Outline => TabStyle {
283 fg: cx.theme().muted_foreground,
284 bg: cx.theme().transparent,
285 borders: Edges::all(px(1.)),
286 border_color: if selected {
287 cx.theme().primary
288 } else {
289 cx.theme().border
290 },
291 ..Default::default()
292 },
293 TabVariant::Pill => TabStyle {
294 fg: if selected {
295 cx.theme().primary_foreground.opacity(0.5)
296 } else {
297 cx.theme().muted_foreground
298 },
299 bg: if selected {
300 cx.theme().primary.opacity(0.5)
301 } else {
302 cx.theme().transparent
303 },
304 ..Default::default()
305 },
306 TabVariant::Segmented => TabStyle {
307 fg: cx.theme().muted_foreground,
308 bg: cx.theme().tab_bar,
309 inner_bg: if selected {
310 cx.theme().background
311 } else {
312 cx.theme().transparent
313 },
314 ..Default::default()
315 },
316 TabVariant::Underline => TabStyle {
317 fg: cx.theme().muted_foreground,
318 bg: cx.theme().transparent,
319 border_color: if selected {
320 cx.theme().border
321 } else {
322 cx.theme().transparent
323 },
324 borders: Edges {
325 bottom: px(2.),
326 ..Default::default()
327 },
328 ..Default::default()
329 },
330 }
331 }
332
333 pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels {
334 if *self != TabVariant::Segmented {
335 return px(0.);
336 }
337
338 match size {
339 Size::XSmall | Size::Small => cx.theme().radius,
340 Size::Large => cx.theme().radius_lg,
341 _ => cx.theme().radius_lg,
342 }
343 }
344
345 fn radius(&self, size: Size, cx: &App) -> Pixels {
346 match self {
347 TabVariant::Outline | TabVariant::Pill => px(99.),
348 TabVariant::Segmented => match size {
349 Size::XSmall | Size::Small => cx.theme().radius,
350 Size::Large => cx.theme().radius_lg,
351 _ => cx.theme().radius_lg,
352 },
353 _ => px(0.),
354 }
355 }
356
357 pub(super) fn inner_radius(&self, size: Size, cx: &App) -> Pixels {
358 match self {
359 TabVariant::Segmented => match size {
360 Size::Large => self.tab_bar_radius(size, cx) - px(3.),
361 _ => self.tab_bar_radius(size, cx) - px(2.),
362 },
363 _ => px(0.),
364 }
365 }
366}
367
368#[allow(dead_code)]
369struct TabStyle {
370 borders: Edges<Pixels>,
371 border_color: Hsla,
372 bg: Hsla,
373 fg: Hsla,
374 shadow: bool,
375 inner_bg: Hsla,
376}
377
378impl Default for TabStyle {
379 fn default() -> Self {
380 TabStyle {
381 borders: Edges::all(px(0.)),
382 border_color: rgpui::transparent_white(),
383 bg: rgpui::transparent_white(),
384 fg: rgpui::transparent_white(),
385 shadow: false,
386 inner_bg: rgpui::transparent_white(),
387 }
388 }
389}
390
391#[derive(IntoElement)]
393pub struct Tab {
394 ix: usize,
395 base: Div,
396 pub(super) label: Option<SharedString>,
397 pub(super) icon: Option<Icon>,
398 prefix: Option<AnyElement>,
399 pub(super) tab_bar_prefix: Option<bool>,
400 suffix: Option<AnyElement>,
401 children: Vec<AnyElement>,
402 variant: TabVariant,
403 size: Size,
404 pub(super) disabled: bool,
405 pub(super) selected: bool,
406 pub(super) indicator_active: bool,
407 pub(super) indicator_ready: bool,
408 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
409}
410
411impl From<&'static str> for Tab {
412 fn from(label: &'static str) -> Self {
413 Self::new().label(label)
414 }
415}
416
417impl From<String> for Tab {
418 fn from(label: String) -> Self {
419 Self::new().label(label)
420 }
421}
422
423impl From<SharedString> for Tab {
424 fn from(label: SharedString) -> Self {
425 Self::new().label(label)
426 }
427}
428
429impl From<Icon> for Tab {
430 fn from(icon: Icon) -> Self {
431 Self::default().icon(icon)
432 }
433}
434
435impl From<IconName> for Tab {
436 fn from(icon_name: IconName) -> Self {
437 Self::default().icon(Icon::new(icon_name))
438 }
439}
440
441impl Default for Tab {
442 fn default() -> Self {
443 Self {
444 ix: 0,
445 base: div(),
446 label: None,
447 icon: None,
448 tab_bar_prefix: None,
449 children: Vec::new(),
450 disabled: false,
451 selected: false,
452 indicator_active: false,
453 indicator_ready: true,
454 prefix: None,
455 suffix: None,
456 variant: TabVariant::default(),
457 size: Size::default(),
458 on_click: None,
459 }
460 }
461}
462
463impl Tab {
464 pub fn new() -> Self {
466 Self::default()
467 }
468
469 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
471 self.label = Some(label.into());
472 self
473 }
474
475 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
477 self.icon = Some(icon.into());
478 self
479 }
480
481 pub fn with_variant(mut self, variant: TabVariant) -> Self {
483 self.variant = variant;
484 self
485 }
486
487 pub fn pill(mut self) -> Self {
489 self.variant = TabVariant::Pill;
490 self
491 }
492
493 pub fn outline(mut self) -> Self {
495 self.variant = TabVariant::Outline;
496 self
497 }
498
499 pub fn segmented(mut self) -> Self {
501 self.variant = TabVariant::Segmented;
502 self
503 }
504
505 pub fn underline(mut self) -> Self {
507 self.variant = TabVariant::Underline;
508 self
509 }
510
511 pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
513 self.prefix = Some(prefix.into_any_element());
514 self
515 }
516
517 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
519 self.suffix = Some(suffix.into_any_element());
520 self
521 }
522
523 pub fn disabled(mut self, disabled: bool) -> Self {
525 self.disabled = disabled;
526 self
527 }
528
529 pub fn on_click(
531 mut self,
532 on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
533 ) -> Self {
534 self.on_click = Some(Rc::new(on_click));
535 self
536 }
537
538 pub(crate) fn ix(mut self, ix: usize) -> Self {
540 self.ix = ix;
541 self
542 }
543
544 pub(crate) fn tab_bar_prefix(mut self, tab_bar_prefix: bool) -> Self {
546 self.tab_bar_prefix = Some(tab_bar_prefix);
547 self
548 }
549}
550
551impl ParentElement for Tab {
552 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
553 self.children.extend(elements);
554 }
555}
556
557impl Selectable for Tab {
558 fn selected(mut self, selected: bool) -> Self {
559 self.selected = selected;
560 self
561 }
562
563 fn is_selected(&self) -> bool {
564 self.selected
565 }
566}
567
568impl InteractiveElement for Tab {
569 fn interactivity(&mut self) -> &mut rgpui::Interactivity {
570 self.base.interactivity()
571 }
572}
573
574impl StatefulInteractiveElement for Tab {}
575
576impl Styled for Tab {
577 fn style(&mut self) -> &mut rgpui::StyleRefinement {
578 self.base.style()
579 }
580}
581
582impl Sizable for Tab {
583 fn with_size(mut self, size: impl Into<Size>) -> Self {
584 self.size = size.into();
585 self
586 }
587}
588
589impl RenderOnce for Tab {
590 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
591 let mut tab_style = if self.selected {
592 self.variant.selected(cx)
593 } else {
594 self.variant.normal(cx)
595 };
596 let mut hover_style = self.variant.hovered(self.selected, cx);
597 if self.disabled {
598 tab_style = self.variant.disabled(self.selected, cx);
599 hover_style = self.variant.disabled(self.selected, cx);
600 }
601 let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default();
602 if !tab_bar_prefix {
603 if self.ix == 0 && self.variant == TabVariant::Tab {
604 tab_style.borders.left = px(0.);
605 hover_style.borders.left = px(0.);
606 }
607 }
608 let radius = self.variant.radius(self.size, cx);
609 let inner_radius = self.variant.inner_radius(self.size, cx);
610 let inner_paddings = self.variant.inner_paddings(self.size);
611 let inner_margins = self.variant.inner_margins(self.size);
612 let inner_height = self.variant.inner_height(self.size);
613 let height = self.variant.height(self.size);
614
615 let segmented_indicator_active =
616 self.variant == TabVariant::Segmented && self.indicator_active;
617 let has_inline_inner_bg =
618 self.selected && segmented_indicator_active && !self.indicator_ready;
619 let inline_inner_bg = tab_style.inner_bg;
620 let (inner_bg, hover_inner_bg) = if segmented_indicator_active && self.indicator_ready {
621 (cx.theme().transparent, cx.theme().transparent)
622 } else if has_inline_inner_bg {
623 (inline_inner_bg, inline_inner_bg)
624 } else {
625 (tab_style.inner_bg, hover_style.inner_bg)
626 };
627 let inner_shadow = tab_style.shadow && !segmented_indicator_active;
628
629 self.base
630 .id(self.ix)
631 .relative()
632 .flex()
633 .flex_wrap()
634 .gap_1()
635 .items_center()
636 .flex_shrink_0()
637 .h(height)
638 .overflow_hidden()
639 .text_color(tab_style.fg)
640 .map(|this| match self.size {
641 Size::XSmall => this.text_xs(),
642 Size::Large => this.text_base(),
643 _ => this.text_sm(),
644 })
645 .bg(tab_style.bg)
646 .border_l(tab_style.borders.left)
647 .border_r(tab_style.borders.right)
648 .border_t(tab_style.borders.top)
649 .border_b(tab_style.borders.bottom)
650 .border_color(tab_style.border_color)
651 .rounded(radius)
652 .when(!self.selected && !self.disabled, |this| {
653 this.hover(|this| {
654 this.text_color(hover_style.fg)
655 .bg(hover_style.bg)
656 .border_l(hover_style.borders.left)
657 .border_r(hover_style.borders.right)
658 .border_t(hover_style.borders.top)
659 .border_b(hover_style.borders.bottom)
660 .border_color(hover_style.border_color)
661 .rounded(radius)
662 })
663 })
664 .when(has_inline_inner_bg, |this| {
665 this.child(
666 div()
667 .absolute()
668 .left_0()
669 .right_0()
670 .top_0()
671 .bottom_0()
672 .flex()
673 .items_center()
674 .child(
675 div()
676 .w_full()
677 .h(inner_height)
678 .bg(inline_inner_bg)
679 .rounded(inner_radius)
680 .when(tab_style.shadow, |this| this.shadow_xs()),
681 ),
682 )
683 })
684 .when_some(self.prefix, |this, prefix| this.child(prefix))
685 .child(
686 h_flex()
687 .flex_1()
688 .h(inner_height)
689 .line_height(relative(1.))
690 .whitespace_nowrap()
691 .items_center()
692 .justify_center()
693 .overflow_hidden()
694 .margins(inner_margins)
695 .flex_shrink_0()
696 .map(|this| match self.icon {
697 Some(icon) => {
698 this.w(inner_height * 1.25)
699 .child(icon.map(|this| match self.size {
700 Size::XSmall => this.size_2p5(),
701 Size::Small => this.size_3p5(),
702 Size::Large => this.size_4(),
703 _ => this.size_4(),
704 }))
705 }
706 None => this
707 .paddings(inner_paddings)
708 .map(|this| match self.label {
709 Some(label) => this.child(label),
710 None => this,
711 })
712 .children(self.children),
713 })
714 .bg(inner_bg)
715 .rounded(inner_radius)
716 .when(inner_shadow, |this| this.shadow_xs())
717 .hover(|this| this.bg(hover_inner_bg).rounded(inner_radius)),
718 )
719 .when_some(self.suffix, |this, suffix| this.child(suffix))
720 .on_mouse_down(MouseButton::Left, |_, _, cx| {
721 cx.stop_propagation();
724 })
725 .when(!self.disabled, |this| {
726 this.when_some(self.on_click.clone(), |this, on_click| {
727 this.on_click(move |event, window, cx| on_click(event, window, cx))
728 })
729 })
730 }
731}