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            .when(style == TabBarStyle::Underline, |this| this.gap_8())
120            .when(style == TabBarStyle::Pills, |this| this.gap_2())
121            .when_some(self.scroll_handle, |this, scroll_handle| {
122                this.track_scroll(&scroll_handle)
123            })
124            .children(self.children);
125
126        let middle = match style {
127            // No `.overflow_x_hidden()` here: the inner `tabs_row` already owns
128            // its horizontal overflow via `.overflow_x_scroll()`, and an outer
129            // clip on this `relative` wrapper made the `Tab` children
130            // non-hit-testable (clicks at their real bounds silently no-op'd).
131            TabBarStyle::Underline => div()
132                .relative()
133                .flex_1()
134                .h_full()
135                .child(
136                    div()
137                        .absolute()
138                        .top_0()
139                        .left_0()
140                        .size_full()
141                        .border_b_1()
142                        .border_color(semantic::border_muted(cx)),
143                )
144                .child(tabs_row)
145                .into_any_element(),
146            TabBarStyle::Pills => div()
147                .flex_1()
148                .p_1()
149                .rounded_lg()
150                .bg(semantic::elevated_surface(cx))
151                .child(tabs_row)
152                .into_any_element(),
153        };
154
155        div()
156            .id(self.id)
157            .group("tab_bar")
158            .flex()
159            .flex_none()
160            .w_full()
161            .h(Tab::container_height(cx))
162            .bg(semantic::surface(cx))
163            .when(!self.start_children.is_empty(), |this| {
164                this.child(
165                    h_flex()
166                        .flex_none()
167                        .gap(DynamicSpacing::Base04.rems(cx))
168                        .px(DynamicSpacing::Base06.rems(cx))
169                        .border_b_1()
170                        .border_r_1()
171                        .border_color(semantic::border_muted(cx))
172                        .children(self.start_children),
173                )
174            })
175            .child(middle)
176            .when(!self.end_children.is_empty(), |this| {
177                this.child(
178                    h_flex()
179                        .flex_none()
180                        .gap(DynamicSpacing::Base04.rems(cx))
181                        .px(DynamicSpacing::Base06.rems(cx))
182                        .border_color(semantic::border_muted(cx))
183                        .border_b_1()
184                        .border_l_1()
185                        .children(self.end_children),
186                )
187            })
188    }
189}
190
191impl Component for TabBar {
192    fn scope() -> ComponentScope {
193        ComponentScope::Navigation
194    }
195
196    fn name() -> &'static str {
197        "TabBar"
198    }
199
200    fn description() -> Option<&'static str> {
201        Some("A horizontal bar containing tabs for navigation between different views or sections.")
202    }
203
204    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
205        Some(
206            v_flex()
207                .gap_6()
208                .children(vec![
209                    example_group_with_title(
210                        "Underline (default)",
211                        vec![single_example(
212                            "With Tabs",
213                            TabBar::new("underline_tab_bar")
214                                .child(Tab::new("u_tab1").toggle_state(true).child("Overview"))
215                                .child(Tab::new("u_tab2").child("Activity"))
216                                .child(Tab::new("u_tab3").child("Settings"))
217                                .into_any_element(),
218                        )],
219                    ),
220                    example_group_with_title(
221                        "Pills",
222                        vec![single_example(
223                            "With Tabs",
224                            TabBar::new("pills_tab_bar")
225                                .style(TabBarStyle::Pills)
226                                .child(
227                                    Tab::new("p_tab1")
228                                        .style(TabBarStyle::Pills)
229                                        .toggle_state(true)
230                                        .child("Overview"),
231                                )
232                                .child(
233                                    Tab::new("p_tab2")
234                                        .style(TabBarStyle::Pills)
235                                        .child("Activity"),
236                                )
237                                .child(
238                                    Tab::new("p_tab3")
239                                        .style(TabBarStyle::Pills)
240                                        .child("Settings"),
241                                )
242                                .into_any_element(),
243                        )],
244                    ),
245                    example_group_with_title(
246                        "With Start and End Children",
247                        vec![single_example(
248                            "Full TabBar",
249                            TabBar::new("full_tab_bar")
250                                .start_child(Button::new("start_button", "Start"))
251                                .child(Tab::new("tab1"))
252                                .child(Tab::new("tab2"))
253                                .child(Tab::new("tab3"))
254                                .end_child(Button::new("end_button", "End"))
255                                .into_any_element(),
256                        )],
257                    ),
258                ])
259                .into_any_element(),
260        )
261    }
262}