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                self.div
109                    .cursor_pointer()
110                    .px_1()
111                    .py_4()
112                    .border_b_2()
113                    .border_color(border_color)
114                    .text_color(text_color)
115                    .hover(move |this| this.text_color(hover_color))
116                    .child(content)
117            }
118            TabBarStyle::Pills => {
119                let (text_color, bg) = if self.selected {
120                    (semantic::text(cx), semantic::surface(cx))
121                } else {
122                    (semantic::text_muted(cx), transparent_black())
123                };
124                let hover_bg = semantic::hover_bg(cx);
125
126                self.div
127                    .cursor_pointer()
128                    .px_3()
129                    .py_1p5()
130                    .rounded_md()
131                    .bg(bg)
132                    .text_color(text_color)
133                    .when(self.selected, |this| this.shadow_level(Shadow::Sm))
134                    .when(!self.selected, |this| {
135                        this.hover(move |this| this.bg(hover_bg))
136                    })
137                    .child(content)
138            }
139        }
140    }
141}
142
143impl Component for Tab {
144    fn scope() -> ComponentScope {
145        ComponentScope::Navigation
146    }
147
148    fn description() -> Option<&'static str> {
149        Some(
150            "A tab component that can be used in a tabbed interface, supporting underline and pills styles.",
151        )
152    }
153
154    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
155        Some(
156            v_flex()
157                .gap_6()
158                .children(vec![
159                    example_group_with_title(
160                        "Underline",
161                        vec![
162                            single_example(
163                                "Default",
164                                Tab::new("underline_default")
165                                    .child("Default Tab")
166                                    .into_any_element(),
167                            ),
168                            single_example(
169                                "Selected",
170                                Tab::new("underline_selected")
171                                    .toggle_state(true)
172                                    .child("Selected Tab")
173                                    .into_any_element(),
174                            ),
175                        ],
176                    ),
177                    example_group_with_title(
178                        "Pills",
179                        vec![
180                            single_example(
181                                "Default",
182                                Tab::new("pills_default")
183                                    .style(TabBarStyle::Pills)
184                                    .child("Default Tab")
185                                    .into_any_element(),
186                            ),
187                            single_example(
188                                "Selected",
189                                Tab::new("pills_selected")
190                                    .style(TabBarStyle::Pills)
191                                    .toggle_state(true)
192                                    .child("Selected Tab")
193                                    .into_any_element(),
194                            ),
195                        ],
196                    ),
197                ])
198                .into_any_element(),
199        )
200    }
201}