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        let content = h_flex()
86            .items_center()
87            .gap_2()
88            .children(self.start_slot)
89            .children(self.children)
90            .children(self.end_slot);
91
92        match self.style {
93            TabBarStyle::Underline => {
94                // Borderless: the active tab is indicated by a subtle rounded
95                // background + brighter text (no bottom-border underline), per
96                // the requested mockup.
97                let (text_color, bg) = if self.selected {
98                    (semantic::text(cx), semantic::active_bg(cx))
99                } else {
100                    (semantic::text_muted(cx), transparent_black())
101                };
102                let hover_color = semantic::text(cx);
103
104                // Fill the parent TabBar's fixed height and vertically center
105                // the label instead of setting a large fixed `py` (which made
106                // the tab taller than `TabBar`'s container height, overflowing
107                // and clipping the title at the top).
108                self.div
109                    .h_full()
110                    .flex()
111                    .items_center()
112                    .cursor_pointer()
113                    .px_2()
114                    .rounded_md()
115                    .bg(bg)
116                    .text_color(text_color)
117                    .hover(move |this| this.text_color(hover_color))
118                    .child(content)
119            }
120            TabBarStyle::Pills => {
121                let (text_color, bg) = if self.selected {
122                    (semantic::text(cx), semantic::surface(cx))
123                } else {
124                    (semantic::text_muted(cx), transparent_black())
125                };
126                let hover_bg = semantic::hover_bg(cx);
127
128                self.div
129                    .cursor_pointer()
130                    .px_3()
131                    .py_1p5()
132                    .rounded_md()
133                    .bg(bg)
134                    .text_color(text_color)
135                    .when(self.selected, |this| this.shadow_level(Shadow::Sm))
136                    .when(!self.selected, |this| {
137                        this.hover(move |this| this.bg(hover_bg))
138                    })
139                    .child(content)
140            }
141        }
142    }
143}
144
145impl Component for Tab {
146    fn scope() -> ComponentScope {
147        ComponentScope::Navigation
148    }
149
150    fn description() -> Option<&'static str> {
151        Some(
152            "A tab component that can be used in a tabbed interface, supporting underline and pills styles.",
153        )
154    }
155
156    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
157        Some(
158            v_flex()
159                .gap_6()
160                .children(vec![
161                    example_group_with_title(
162                        "Underline",
163                        vec![
164                            single_example(
165                                "Default",
166                                Tab::new("underline_default")
167                                    .child("Default Tab")
168                                    .into_any_element(),
169                            ),
170                            single_example(
171                                "Selected",
172                                Tab::new("underline_selected")
173                                    .toggle_state(true)
174                                    .child("Selected Tab")
175                                    .into_any_element(),
176                            ),
177                        ],
178                    ),
179                    example_group_with_title(
180                        "Pills",
181                        vec![
182                            single_example(
183                                "Default",
184                                Tab::new("pills_default")
185                                    .style(TabBarStyle::Pills)
186                                    .child("Default Tab")
187                                    .into_any_element(),
188                            ),
189                            single_example(
190                                "Selected",
191                                Tab::new("pills_selected")
192                                    .style(TabBarStyle::Pills)
193                                    .toggle_state(true)
194                                    .child("Selected Tab")
195                                    .into_any_element(),
196                            ),
197                        ],
198                    ),
199                ])
200                .into_any_element(),
201        )
202    }
203}