Skip to main content

rgpui_component/tab/
tab.rs

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/// Tab variants.
12#[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    /// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`]
71    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/// A Tab element for the [`super::TabBar`].
392#[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    /// Create a new tab with a label.
465    pub fn new() -> Self {
466        Self::default()
467    }
468
469    /// Set label for the tab.
470    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
471        self.label = Some(label.into());
472        self
473    }
474
475    /// Set icon for the tab.
476    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
477        self.icon = Some(icon.into());
478        self
479    }
480
481    /// Set Tab Variant.
482    pub fn with_variant(mut self, variant: TabVariant) -> Self {
483        self.variant = variant;
484        self
485    }
486
487    /// Use Pill variant.
488    pub fn pill(mut self) -> Self {
489        self.variant = TabVariant::Pill;
490        self
491    }
492
493    /// Use outline variant.
494    pub fn outline(mut self) -> Self {
495        self.variant = TabVariant::Outline;
496        self
497    }
498
499    /// Use Segmented variant.
500    pub fn segmented(mut self) -> Self {
501        self.variant = TabVariant::Segmented;
502        self
503    }
504
505    /// Use Underline variant.
506    pub fn underline(mut self) -> Self {
507        self.variant = TabVariant::Underline;
508        self
509    }
510
511    /// Set the left side of the tab
512    pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
513        self.prefix = Some(prefix.into_any_element());
514        self
515    }
516
517    /// Set the right side of the tab
518    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
519        self.suffix = Some(suffix.into_any_element());
520        self
521    }
522
523    /// Set disabled state to the tab, default false.
524    pub fn disabled(mut self, disabled: bool) -> Self {
525        self.disabled = disabled;
526        self
527    }
528
529    /// Set the click handler for the tab.
530    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    /// Set index to the tab.
539    pub(crate) fn ix(mut self, ix: usize) -> Self {
540        self.ix = ix;
541        self
542    }
543
544    /// Set if the tab bar has a prefix.
545    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                // Stop propagation behavior, for works on TitleBar.
722                // https://github.com/longbridge/gpui-component/issues/1836
723                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}