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        // Title area grows (`flex_1`) so the end slot (e.g. a close "x") is
86        // pinned to the tab's right edge instead of sitting right after the
87        // text.
88        let content = h_flex()
89            .w_full()
90            .items_center()
91            .gap_2()
92            .children(self.start_slot)
93            .child(
94                h_flex()
95                    .flex_1()
96                    .min_w_0()
97                    .items_center()
98                    .gap_2()
99                    .children(self.children),
100            )
101            .children(self.end_slot);
102
103        match self.style {
104            TabBarStyle::Underline => {
105                // VSCode-style tab: the active tab gets a top accent bar + a
106                // slightly elevated background + bright text; inactive tabs are
107                // flat/muted. Tabs sit flush, separated by a thin right divider.
108                // (Top accent, not a bottom border, per the requested look.)
109                let (text_color, bg, accent) = if self.selected {
110                    (
111                        semantic::text(cx),
112                        semantic::elevated_surface(cx),
113                        palette::primary(500),
114                    )
115                } else {
116                    (
117                        semantic::text_muted(cx),
118                        transparent_black(),
119                        transparent_black(),
120                    )
121                };
122                let hover_color = semantic::text(cx);
123
124                self.div
125                    .h_full()
126                    .min_w(px(140.))
127                    .flex()
128                    .items_center()
129                    .cursor_pointer()
130                    .px_3()
131                    .border_t_2()
132                    .border_color(accent)
133                    .bg(bg)
134                    .text_color(text_color)
135                    .hover(move |this| this.text_color(hover_color))
136                    .child(content)
137            }
138            TabBarStyle::Pills => {
139                let (text_color, bg) = if self.selected {
140                    (semantic::text(cx), semantic::surface(cx))
141                } else {
142                    (semantic::text_muted(cx), transparent_black())
143                };
144                let hover_bg = semantic::hover_bg(cx);
145
146                self.div
147                    .cursor_pointer()
148                    .px_3()
149                    .py_1p5()
150                    .rounded_md()
151                    .bg(bg)
152                    .text_color(text_color)
153                    .when(self.selected, |this| this.shadow_level(Shadow::Sm))
154                    .when(!self.selected, |this| {
155                        this.hover(move |this| this.bg(hover_bg))
156                    })
157                    .child(content)
158            }
159        }
160    }
161}
162
163impl Component for Tab {
164    fn scope() -> ComponentScope {
165        ComponentScope::Navigation
166    }
167
168    fn description() -> Option<&'static str> {
169        Some(
170            "A tab component that can be used in a tabbed interface, supporting underline and pills styles.",
171        )
172    }
173
174    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
175        Some(
176            v_flex()
177                .gap_6()
178                .children(vec![
179                    example_group_with_title(
180                        "Underline",
181                        vec![
182                            single_example(
183                                "Default",
184                                Tab::new("underline_default")
185                                    .child("Default Tab")
186                                    .into_any_element(),
187                            ),
188                            single_example(
189                                "Selected",
190                                Tab::new("underline_selected")
191                                    .toggle_state(true)
192                                    .child("Selected Tab")
193                                    .into_any_element(),
194                            ),
195                        ],
196                    ),
197                    example_group_with_title(
198                        "Pills",
199                        vec![
200                            single_example(
201                                "Default",
202                                Tab::new("pills_default")
203                                    .style(TabBarStyle::Pills)
204                                    .child("Default Tab")
205                                    .into_any_element(),
206                            ),
207                            single_example(
208                                "Selected",
209                                Tab::new("pills_selected")
210                                    .style(TabBarStyle::Pills)
211                                    .toggle_state(true)
212                                    .child("Selected Tab")
213                                    .into_any_element(),
214                            ),
215                        ],
216                    ),
217                ])
218                .into_any_element(),
219        )
220    }
221}