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        let label = SharedString::from(label);
402        Self::new(label)
403    }
404}
405
406impl From<String> for Tab {
407    fn from(label: String) -> Self {
408        let label = SharedString::from(label);
409        Self::new(label)
410    }
411}
412
413impl From<SharedString> for Tab {
414    fn from(label: SharedString) -> Self {
415        Self::new(label)
416    }
417}
418
419impl From<Icon> for Tab {
420    fn from(icon: Icon) -> Self {
421        Self::default().icon(icon)
422    }
423}
424
425impl From<IconName> for Tab {
426    fn from(icon_name: IconName) -> Self {
427        Self::default().icon(Icon::new(icon_name))
428    }
429}
430
431impl Default for Tab {
432    fn default() -> Self {
433        Self {
434            id: ElementId::Integer(0),
435            base: div(),
436            label: None,
437            icon: None,
438            children: Vec::new(),
439            disabled: false,
440            selected: false,
441            prefix: None,
442            suffix: None,
443            variant: TabVariant::default(),
444            size: Size::default(),
445            on_click: None,
446        }
447    }
448}
449
450impl Tab {
451    /// Create a new tab with a label.
452    pub fn new(label: impl Into<SharedString>) -> Self {
453        let mut this = Self::default();
454        this.label = Some(label.into());
455        this
456    }
457
458    /// Set icon for the tab.
459    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
460        self.icon = Some(icon.into());
461        self
462    }
463
464    /// Set Tab Variant.
465    pub fn with_variant(mut self, variant: TabVariant) -> Self {
466        self.variant = variant;
467        self
468    }
469
470    /// Use Pill variant.
471    pub fn pill(mut self) -> Self {
472        self.variant = TabVariant::Pill;
473        self
474    }
475
476    /// Use outline variant.
477    pub fn outline(mut self) -> Self {
478        self.variant = TabVariant::Outline;
479        self
480    }
481
482    /// Use Segmented variant.
483    pub fn segmented(mut self) -> Self {
484        self.variant = TabVariant::Segmented;
485        self
486    }
487
488    /// Use Underline variant.
489    pub fn underline(mut self) -> Self {
490        self.variant = TabVariant::Underline;
491        self
492    }
493
494    /// Set the left side of the tab
495    pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
496        self.prefix = Some(prefix.into_any_element());
497        self
498    }
499
500    /// Set the right side of the tab
501    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
502        self.suffix = Some(suffix.into_any_element());
503        self
504    }
505
506    /// Set disabled state to the tab, default false.
507    pub fn disabled(mut self, disabled: bool) -> Self {
508        self.disabled = disabled;
509        self
510    }
511
512    /// Set the click handler for the tab.
513    pub fn on_click(
514        mut self,
515        on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
516    ) -> Self {
517        self.on_click = Some(Rc::new(on_click));
518        self
519    }
520
521    /// Set id to the tab.
522    pub(super) fn id(mut self, id: impl Into<ElementId>) -> Self {
523        self.id = id.into();
524        self
525    }
526}
527
528impl ParentElement for Tab {
529    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
530        self.children.extend(elements);
531    }
532}
533
534impl Selectable for Tab {
535    fn selected(mut self, selected: bool) -> Self {
536        self.selected = selected;
537        self
538    }
539
540    fn is_selected(&self) -> bool {
541        self.selected
542    }
543}
544
545impl InteractiveElement for Tab {
546    fn interactivity(&mut self) -> &mut gpui::Interactivity {
547        self.base.interactivity()
548    }
549}
550
551impl StatefulInteractiveElement for Tab {}
552
553impl Styled for Tab {
554    fn style(&mut self) -> &mut gpui::StyleRefinement {
555        self.base.style()
556    }
557}
558
559impl Sizable for Tab {
560    fn with_size(mut self, size: impl Into<Size>) -> Self {
561        self.size = size.into();
562        self
563    }
564}
565
566impl RenderOnce for Tab {
567    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
568        let mut tab_style = if self.selected {
569            self.variant.selected(cx)
570        } else {
571            self.variant.normal(cx)
572        };
573        let mut hover_style = self.variant.hovered(self.selected, cx);
574        if self.disabled {
575            tab_style = self.variant.disabled(self.selected, cx);
576            hover_style = self.variant.disabled(self.selected, cx);
577        }
578        let inner_paddings = self.variant.inner_paddings(self.size);
579        let inner_margins = self.variant.inner_margins(self.size);
580        let inner_height = self.variant.inner_height(self.size);
581        let height = self.variant.height(self.size);
582
583        self.base
584            .id(self.id)
585            .flex()
586            .flex_wrap()
587            .gap_1()
588            .items_center()
589            .flex_shrink_0()
590            .overflow_hidden()
591            .h(height)
592            .overflow_hidden()
593            .text_color(tab_style.fg)
594            .map(|this| match self.size {
595                Size::XSmall => this.text_xs(),
596                Size::Large => this.text_base(),
597                _ => this.text_sm(),
598            })
599            .bg(tab_style.bg)
600            .border_l(tab_style.borders.left)
601            .border_r(tab_style.borders.right)
602            .border_t(tab_style.borders.top)
603            .border_b(tab_style.borders.bottom)
604            .border_color(tab_style.border_color)
605            .rounded(tab_style.radius)
606            .when(!self.selected && !self.disabled, |this| {
607                this.hover(|this| {
608                    this.text_color(hover_style.fg)
609                        .bg(hover_style.bg)
610                        .border_l(hover_style.borders.left)
611                        .border_r(hover_style.borders.right)
612                        .border_t(hover_style.borders.top)
613                        .border_b(hover_style.borders.bottom)
614                        .border_color(hover_style.border_color)
615                        .rounded(tab_style.radius)
616                })
617            })
618            .when_some(self.prefix, |this, prefix| this.child(prefix))
619            .child(
620                h_flex()
621                    .h(inner_height)
622                    .line_height(relative(1.))
623                    .items_center()
624                    .justify_center()
625                    .overflow_hidden()
626                    .margins(inner_margins)
627                    .flex_shrink_0()
628                    .map(|this| match self.icon {
629                        Some(icon) => {
630                            this.w(inner_height * 1.25)
631                                .child(icon.map(|this| match self.size {
632                                    Size::XSmall => this.size_2p5(),
633                                    Size::Small => this.size_3p5(),
634                                    Size::Large => this.size_4(),
635                                    _ => this.size_4(),
636                                }))
637                        }
638                        None => this
639                            .paddings(inner_paddings)
640                            .map(|this| match self.label {
641                                Some(label) => this.child(label),
642                                None => this,
643                            })
644                            .children(self.children),
645                    })
646                    .bg(tab_style.inner_bg)
647                    .rounded(tab_style.inner_radius)
648                    .when(tab_style.shadow, |this| this.shadow_xs())
649                    .hover(|this| {
650                        this.bg(hover_style.inner_bg)
651                            .rounded(hover_style.inner_radius)
652                    }),
653            )
654            .when_some(self.suffix, |this, suffix| this.child(suffix))
655            .when(!self.disabled, |this| {
656                this.when_some(self.on_click.clone(), |this, on_click| {
657                    this.on_click(move |event, window, cx| on_click(event, window, cx))
658                })
659            })
660    }
661}