Skip to main content

ui/components/
tab.rs

1use gpui::{AnyElement, Div, IntoElement, Stateful, transparent_black};
2use smallvec::SmallVec;
3
4use crate::TabBarStyle;
5use crate::prelude::*;
6
7/// A single tab within a [`TabBar`](crate::TabBar).
8///
9/// Renders per [`TabBarStyle`] (default [`TabBarStyle::Underline`]); pass the
10/// same style used on the parent `TabBar` via [`Tab::style`] so both agree.
11#[derive(IntoElement, RegisterComponent)]
12pub struct Tab {
13    div: Stateful<Div>,
14    selected: bool,
15    style: TabBarStyle,
16    start_slot: Option<AnyElement>,
17    end_slot: Option<AnyElement>,
18    children: SmallVec<[AnyElement; 2]>,
19}
20
21impl Tab {
22    pub fn new(id: impl Into<ElementId>) -> Self {
23        let id = id.into();
24        Self {
25            div: div()
26                .id(id.clone())
27                .debug_selector(|| format!("TAB-{}", id)),
28            selected: false,
29            style: TabBarStyle::default(),
30            start_slot: None,
31            end_slot: None,
32            children: SmallVec::new(),
33        }
34    }
35
36    /// Sets the visual style. Should match the parent [`TabBar`](crate::TabBar)'s style.
37    pub fn style(mut self, style: TabBarStyle) -> Self {
38        self.style = style;
39        self
40    }
41
42    pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
43        self.start_slot = element.into().map(IntoElement::into_any_element);
44        self
45    }
46
47    pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
48        self.end_slot = element.into().map(IntoElement::into_any_element);
49        self
50    }
51
52    pub fn content_height(cx: &App) -> Pixels {
53        DynamicSpacing::Base32.px(cx) - px(1.)
54    }
55
56    pub fn container_height(cx: &App) -> Pixels {
57        DynamicSpacing::Base32.px(cx)
58    }
59}
60
61impl InteractiveElement for Tab {
62    fn interactivity(&mut self) -> &mut gpui::Interactivity {
63        self.div.interactivity()
64    }
65}
66
67impl StatefulInteractiveElement for Tab {}
68
69impl Toggleable for Tab {
70    fn toggle_state(mut self, selected: bool) -> Self {
71        self.selected = selected;
72        self
73    }
74}
75
76impl ParentElement for Tab {
77    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
78        self.children.extend(elements)
79    }
80}
81
82impl RenderOnce for Tab {
83    #[allow(refining_impl_trait)]
84    fn render(self, _: &mut Window, cx: &mut App) -> Stateful<Div> {
85        let content = h_flex()
86            .items_center()
87            .gap_2()
88            .children(self.start_slot)
89            .children(self.children)
90            .children(self.end_slot);
91
92        match self.style {
93            TabBarStyle::Underline => {
94                // VSCode-style tab: the active tab gets a top accent bar + a
95                // slightly elevated background + bright text; inactive tabs are
96                // flat/muted. Tabs sit flush, separated by a thin right divider.
97                // (Top accent, not a bottom border, per the requested look.)
98                let (text_color, bg, accent) = if self.selected {
99                    (
100                        semantic::text(cx),
101                        semantic::elevated_surface(cx),
102                        palette::primary(500),
103                    )
104                } else {
105                    (
106                        semantic::text_muted(cx),
107                        transparent_black(),
108                        transparent_black(),
109                    )
110                };
111                let hover_color = semantic::text(cx);
112
113                self.div
114                    .h_full()
115                    .flex()
116                    .items_center()
117                    .cursor_pointer()
118                    .px_3()
119                    .border_t_2()
120                    .border_color(accent)
121                    .bg(bg)
122                    .text_color(text_color)
123                    .hover(move |this| this.text_color(hover_color))
124                    .child(content)
125            }
126            TabBarStyle::Pills => {
127                let (text_color, bg) = if self.selected {
128                    (semantic::text(cx), semantic::surface(cx))
129                } else {
130                    (semantic::text_muted(cx), transparent_black())
131                };
132                let hover_bg = semantic::hover_bg(cx);
133
134                self.div
135                    .cursor_pointer()
136                    .px_3()
137                    .py_1p5()
138                    .rounded_md()
139                    .bg(bg)
140                    .text_color(text_color)
141                    .when(self.selected, |this| this.shadow_level(Shadow::Sm))
142                    .when(!self.selected, |this| {
143                        this.hover(move |this| this.bg(hover_bg))
144                    })
145                    .child(content)
146            }
147        }
148    }
149}
150
151impl Component for Tab {
152    fn scope() -> ComponentScope {
153        ComponentScope::Navigation
154    }
155
156    fn description() -> Option<&'static str> {
157        Some(
158            "A tab component that can be used in a tabbed interface, supporting underline and pills styles.",
159        )
160    }
161
162    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
163        Some(
164            v_flex()
165                .gap_6()
166                .children(vec![
167                    example_group_with_title(
168                        "Underline",
169                        vec![
170                            single_example(
171                                "Default",
172                                Tab::new("underline_default")
173                                    .child("Default Tab")
174                                    .into_any_element(),
175                            ),
176                            single_example(
177                                "Selected",
178                                Tab::new("underline_selected")
179                                    .toggle_state(true)
180                                    .child("Selected Tab")
181                                    .into_any_element(),
182                            ),
183                        ],
184                    ),
185                    example_group_with_title(
186                        "Pills",
187                        vec![
188                            single_example(
189                                "Default",
190                                Tab::new("pills_default")
191                                    .style(TabBarStyle::Pills)
192                                    .child("Default Tab")
193                                    .into_any_element(),
194                            ),
195                            single_example(
196                                "Selected",
197                                Tab::new("pills_selected")
198                                    .style(TabBarStyle::Pills)
199                                    .toggle_state(true)
200                                    .child("Selected Tab")
201                                    .into_any_element(),
202                            ),
203                        ],
204                    ),
205                ])
206                .into_any_element(),
207        )
208    }
209}