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