Skip to main content

ui/components/
tab_bar.rs

1use gpui::{AnyElement, ScrollHandle};
2use smallvec::SmallVec;
3
4use crate::Tab;
5use crate::prelude::*;
6
7/// Visual style for [`TabBar`] and its child [`Tab`]s.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum TabBarStyle {
10    /// Bottom-border container line; the active tab shows a colored
11    /// underline. Preserves the existing default look for current callers.
12    #[default]
13    Underline,
14    /// Rounded pill container; the active tab shows a raised pill background.
15    Pills,
16}
17
18#[derive(IntoElement, RegisterComponent)]
19pub struct TabBar {
20    id: ElementId,
21    style: TabBarStyle,
22    start_children: SmallVec<[AnyElement; 2]>,
23    children: SmallVec<[AnyElement; 2]>,
24    end_children: SmallVec<[AnyElement; 2]>,
25    scroll_handle: Option<ScrollHandle>,
26}
27
28impl TabBar {
29    pub fn new(id: impl Into<ElementId>) -> Self {
30        Self {
31            id: id.into(),
32            style: TabBarStyle::default(),
33            start_children: SmallVec::new(),
34            children: SmallVec::new(),
35            end_children: SmallVec::new(),
36            scroll_handle: None,
37        }
38    }
39
40    /// Sets the visual style (default [`TabBarStyle::Underline`]).
41    pub fn style(mut self, style: TabBarStyle) -> Self {
42        self.style = style;
43        self
44    }
45
46    pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
47        self.scroll_handle = Some(scroll_handle.clone());
48        self
49    }
50
51    pub fn start_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
52        &mut self.start_children
53    }
54
55    pub fn start_child(mut self, start_child: impl IntoElement) -> Self
56    where
57        Self: Sized,
58    {
59        self.start_children_mut()
60            .push(start_child.into_element().into_any());
61        self
62    }
63
64    pub fn start_children(
65        mut self,
66        start_children: impl IntoIterator<Item = impl IntoElement>,
67    ) -> Self
68    where
69        Self: Sized,
70    {
71        self.start_children_mut().extend(
72            start_children
73                .into_iter()
74                .map(|child| child.into_any_element()),
75        );
76        self
77    }
78
79    pub fn end_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
80        &mut self.end_children
81    }
82
83    pub fn end_child(mut self, end_child: impl IntoElement) -> Self
84    where
85        Self: Sized,
86    {
87        self.end_children_mut()
88            .push(end_child.into_element().into_any());
89        self
90    }
91
92    pub fn end_children(mut self, end_children: impl IntoIterator<Item = impl IntoElement>) -> Self
93    where
94        Self: Sized,
95    {
96        self.end_children_mut().extend(
97            end_children
98                .into_iter()
99                .map(|child| child.into_any_element()),
100        );
101        self
102    }
103}
104
105impl ParentElement for TabBar {
106    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
107        self.children.extend(elements)
108    }
109}
110
111impl RenderOnce for TabBar {
112    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
113        let style = self.style;
114
115        let tabs_row = h_flex()
116            .id("tabs")
117            .flex_grow()
118            .overflow_x_scroll()
119            // Underline (VSCode-style) tabs sit flush with per-tab dividers;
120            // only the Pills style keeps an inter-tab gap.
121            .when(style == TabBarStyle::Pills, |this| this.gap_2())
122            .when_some(self.scroll_handle, |this, scroll_handle| {
123                this.track_scroll(&scroll_handle)
124            })
125            .children(self.children);
126
127        let middle = match style {
128            // No `.overflow_x_hidden()` here: the inner `tabs_row` already owns
129            // its horizontal overflow via `.overflow_x_scroll()`, and an outer
130            // clip on this `relative` wrapper made the `Tab` children
131            // non-hit-testable (clicks at their real bounds silently no-op'd).
132            // No bottom border line (borderless header per the requested
133            // mockup); the tabs row fills the bar directly.
134            TabBarStyle::Underline => div()
135                .relative()
136                .flex_1()
137                .h_full()
138                .child(tabs_row)
139                .into_any_element(),
140            TabBarStyle::Pills => div()
141                .flex_1()
142                .p_1()
143                .rounded_lg()
144                .bg(semantic::elevated_surface(cx))
145                .child(tabs_row)
146                .into_any_element(),
147        };
148
149        div()
150            .id(self.id)
151            .group("tab_bar")
152            .flex()
153            .flex_none()
154            .w_full()
155            .h(Tab::container_height(cx))
156            .bg(semantic::surface(cx))
157            .when(!self.start_children.is_empty(), |this| {
158                this.child(
159                    h_flex()
160                        .flex_none()
161                        .gap(DynamicSpacing::Base04.rems(cx))
162                        .px(DynamicSpacing::Base06.rems(cx))
163                        .children(self.start_children),
164                )
165            })
166            .child(middle)
167            .when(!self.end_children.is_empty(), |this| {
168                this.child(
169                    h_flex()
170                        .flex_none()
171                        .gap(DynamicSpacing::Base04.rems(cx))
172                        .px(DynamicSpacing::Base06.rems(cx))
173                        .children(self.end_children),
174                )
175            })
176    }
177}
178
179impl Component for TabBar {
180    fn scope() -> ComponentScope {
181        ComponentScope::Navigation
182    }
183
184    fn name() -> &'static str {
185        "TabBar"
186    }
187
188    fn description() -> Option<&'static str> {
189        Some("A horizontal bar containing tabs for navigation between different views or sections.")
190    }
191
192    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
193        Some(
194            v_flex()
195                .gap_6()
196                .children(vec![
197                    example_group_with_title(
198                        "Underline (default)",
199                        vec![single_example(
200                            "With Tabs",
201                            TabBar::new("underline_tab_bar")
202                                .child(Tab::new("u_tab1").toggle_state(true).child("Overview"))
203                                .child(Tab::new("u_tab2").child("Activity"))
204                                .child(Tab::new("u_tab3").child("Settings"))
205                                .into_any_element(),
206                        )],
207                    ),
208                    example_group_with_title(
209                        "Pills",
210                        vec![single_example(
211                            "With Tabs",
212                            TabBar::new("pills_tab_bar")
213                                .style(TabBarStyle::Pills)
214                                .child(
215                                    Tab::new("p_tab1")
216                                        .style(TabBarStyle::Pills)
217                                        .toggle_state(true)
218                                        .child("Overview"),
219                                )
220                                .child(
221                                    Tab::new("p_tab2")
222                                        .style(TabBarStyle::Pills)
223                                        .child("Activity"),
224                                )
225                                .child(
226                                    Tab::new("p_tab3")
227                                        .style(TabBarStyle::Pills)
228                                        .child("Settings"),
229                                )
230                                .into_any_element(),
231                        )],
232                    ),
233                    example_group_with_title(
234                        "With Start and End Children",
235                        vec![single_example(
236                            "Full TabBar",
237                            TabBar::new("full_tab_bar")
238                                .start_child(Button::new("start_button", "Start"))
239                                .child(Tab::new("tab1"))
240                                .child(Tab::new("tab2"))
241                                .child(Tab::new("tab3"))
242                                .end_child(Button::new("end_button", "End"))
243                                .into_any_element(),
244                        )],
245                    ),
246                ])
247                .into_any_element(),
248        )
249    }
250}