gpui_component/tab/
tab.rs

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