1use gpui::{AnyElement, Div, IntoElement, Stateful, transparent_black};
2use smallvec::SmallVec;
3
4use crate::TabBarStyle;
5use crate::prelude::*;
6
7#[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 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()
89 .w_full()
90 .items_center()
91 .gap_2()
92 .children(self.start_slot)
93 .child(
94 h_flex()
95 .flex_1()
96 .min_w_0()
97 .items_center()
98 .gap_2()
99 .children(self.children),
100 )
101 .children(self.end_slot);
102
103 match self.style {
104 TabBarStyle::Underline => {
105 let (text_color, bg, accent) = if self.selected {
110 (
111 semantic::text(cx),
112 semantic::elevated_surface(cx),
113 palette::primary(500),
114 )
115 } else {
116 (
117 semantic::text_muted(cx),
118 transparent_black(),
119 transparent_black(),
120 )
121 };
122 let hover_color = semantic::text(cx);
123
124 self.div
125 .h_full()
126 .min_w(px(140.))
127 .flex()
128 .items_center()
129 .cursor_pointer()
130 .px_3()
131 .border_t_2()
132 .border_color(accent)
133 .bg(bg)
134 .text_color(text_color)
135 .hover(move |this| this.text_color(hover_color))
136 .child(content)
137 }
138 TabBarStyle::Pills => {
139 let (text_color, bg) = if self.selected {
140 (semantic::text(cx), semantic::surface(cx))
141 } else {
142 (semantic::text_muted(cx), transparent_black())
143 };
144 let hover_bg = semantic::hover_bg(cx);
145
146 self.div
147 .cursor_pointer()
148 .px_3()
149 .py_1p5()
150 .rounded_md()
151 .bg(bg)
152 .text_color(text_color)
153 .when(self.selected, |this| this.shadow_level(Shadow::Sm))
154 .when(!self.selected, |this| {
155 this.hover(move |this| this.bg(hover_bg))
156 })
157 .child(content)
158 }
159 }
160 }
161}
162
163impl Component for Tab {
164 fn scope() -> ComponentScope {
165 ComponentScope::Navigation
166 }
167
168 fn description() -> Option<&'static str> {
169 Some(
170 "A tab component that can be used in a tabbed interface, supporting underline and pills styles.",
171 )
172 }
173
174 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
175 Some(
176 v_flex()
177 .gap_6()
178 .children(vec![
179 example_group_with_title(
180 "Underline",
181 vec![
182 single_example(
183 "Default",
184 Tab::new("underline_default")
185 .child("Default Tab")
186 .into_any_element(),
187 ),
188 single_example(
189 "Selected",
190 Tab::new("underline_selected")
191 .toggle_state(true)
192 .child("Selected Tab")
193 .into_any_element(),
194 ),
195 ],
196 ),
197 example_group_with_title(
198 "Pills",
199 vec![
200 single_example(
201 "Default",
202 Tab::new("pills_default")
203 .style(TabBarStyle::Pills)
204 .child("Default Tab")
205 .into_any_element(),
206 ),
207 single_example(
208 "Selected",
209 Tab::new("pills_selected")
210 .style(TabBarStyle::Pills)
211 .toggle_state(true)
212 .child("Selected Tab")
213 .into_any_element(),
214 ),
215 ],
216 ),
217 ])
218 .into_any_element(),
219 )
220 }
221}