gpui_component/tab/
tab.rs

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