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_2())
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            // No bottom border line (borderless header per the requested
132            // mockup); the tabs row fills the bar directly.
133            TabBarStyle::Underline => div()
134                .relative()
135                .flex_1()
136                .h_full()
137                .child(tabs_row)
138                .into_any_element(),
139            TabBarStyle::Pills => div()
140                .flex_1()
141                .p_1()
142                .rounded_lg()
143                .bg(semantic::elevated_surface(cx))
144                .child(tabs_row)
145                .into_any_element(),
146        };
147
148        div()
149            .id(self.id)
150            .group("tab_bar")
151            .flex()
152            .flex_none()
153            .w_full()
154            .h(Tab::container_height(cx))
155            .bg(semantic::surface(cx))
156            .when(!self.start_children.is_empty(), |this| {
157                this.child(
158                    h_flex()
159                        .flex_none()
160                        .gap(DynamicSpacing::Base04.rems(cx))
161                        .px(DynamicSpacing::Base06.rems(cx))
162                        .children(self.start_children),
163                )
164            })
165            .child(middle)
166            .when(!self.end_children.is_empty(), |this| {
167                this.child(
168                    h_flex()
169                        .flex_none()
170                        .gap(DynamicSpacing::Base04.rems(cx))
171                        .px(DynamicSpacing::Base06.rems(cx))
172                        .children(self.end_children),
173                )
174            })
175    }
176}
177
178impl Component for TabBar {
179    fn scope() -> ComponentScope {
180        ComponentScope::Navigation
181    }
182
183    fn name() -> &'static str {
184        "TabBar"
185    }
186
187    fn description() -> Option<&'static str> {
188        Some("A horizontal bar containing tabs for navigation between different views or sections.")
189    }
190
191    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
192        Some(
193            v_flex()
194                .gap_6()
195                .children(vec![
196                    example_group_with_title(
197                        "Underline (default)",
198                        vec![single_example(
199                            "With Tabs",
200                            TabBar::new("underline_tab_bar")
201                                .child(Tab::new("u_tab1").toggle_state(true).child("Overview"))
202                                .child(Tab::new("u_tab2").child("Activity"))
203                                .child(Tab::new("u_tab3").child("Settings"))
204                                .into_any_element(),
205                        )],
206                    ),
207                    example_group_with_title(
208                        "Pills",
209                        vec![single_example(
210                            "With Tabs",
211                            TabBar::new("pills_tab_bar")
212                                .style(TabBarStyle::Pills)
213                                .child(
214                                    Tab::new("p_tab1")
215                                        .style(TabBarStyle::Pills)
216                                        .toggle_state(true)
217                                        .child("Overview"),
218                                )
219                                .child(
220                                    Tab::new("p_tab2")
221                                        .style(TabBarStyle::Pills)
222                                        .child("Activity"),
223                                )
224                                .child(
225                                    Tab::new("p_tab3")
226                                        .style(TabBarStyle::Pills)
227                                        .child("Settings"),
228                                )
229                                .into_any_element(),
230                        )],
231                    ),
232                    example_group_with_title(
233                        "With Start and End Children",
234                        vec![single_example(
235                            "Full TabBar",
236                            TabBar::new("full_tab_bar")
237                                .start_child(Button::new("start_button", "Start"))
238                                .child(Tab::new("tab1"))
239                                .child(Tab::new("tab2"))
240                                .child(Tab::new("tab3"))
241                                .end_child(Button::new("end_button", "End"))
242                                .into_any_element(),
243                        )],
244                    ),
245                ])
246                .into_any_element(),
247        )
248    }
249}