Skip to main content

ui/components/
tab_bar.rs

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