1use std::cmp::Ordering;
2
3use gpui::{AnyElement, Div, IntoElement, Pixels, Stateful, px, transparent_black};
4use smallvec::SmallVec;
5
6use crate::TabBarStyle;
7use crate::prelude::*;
8
9const START_TAB_SLOT_SIZE: Pixels = px(12.);
10const END_TAB_SLOT_SIZE: Pixels = px(14.);
11
12#[derive(Debug, PartialEq, Eq, Clone, Copy)]
16pub enum TabPosition {
17 First,
18 Middle(Ordering),
19 Last,
20}
21
22#[derive(Debug, PartialEq, Eq, Clone, Copy)]
25pub enum TabCloseSide {
26 Start,
27 End,
28}
29
30#[derive(IntoElement, RegisterComponent)]
35pub struct Tab {
36 div: Stateful<Div>,
37 selected: bool,
38 style: TabBarStyle,
39 position: TabPosition,
40 close_side: TabCloseSide,
41 start_slot: Option<AnyElement>,
42 end_slot: Option<AnyElement>,
43 children: SmallVec<[AnyElement; 2]>,
44}
45
46impl Tab {
47 pub fn new(id: impl Into<ElementId>) -> Self {
48 let id = id.into();
49 Self {
50 div: div()
51 .id(id.clone())
52 .debug_selector(|| format!("TAB-{}", id)),
53 selected: false,
54 style: TabBarStyle::default(),
55 position: TabPosition::First,
56 close_side: TabCloseSide::End,
57 start_slot: None,
58 end_slot: None,
59 children: SmallVec::new(),
60 }
61 }
62
63 pub fn style(mut self, style: TabBarStyle) -> Self {
65 self.style = style;
66 self
67 }
68
69 pub fn position(mut self, position: TabPosition) -> Self {
71 self.position = position;
72 self
73 }
74
75 pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
78 self.close_side = close_side;
79 self
80 }
81
82 pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
83 self.start_slot = element.into().map(IntoElement::into_any_element);
84 self
85 }
86
87 pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
88 self.end_slot = element.into().map(IntoElement::into_any_element);
89 self
90 }
91
92 pub fn content_height(cx: &App) -> Pixels {
93 DynamicSpacing::Base32.px(cx) - px(1.)
94 }
95
96 pub fn container_height(cx: &App) -> Pixels {
97 DynamicSpacing::Base32.px(cx)
98 }
99}
100
101impl InteractiveElement for Tab {
102 fn interactivity(&mut self) -> &mut gpui::Interactivity {
103 self.div.interactivity()
104 }
105}
106
107impl StatefulInteractiveElement for Tab {}
108
109impl Toggleable for Tab {
110 fn toggle_state(mut self, selected: bool) -> Self {
111 self.selected = selected;
112 self
113 }
114}
115
116impl ParentElement for Tab {
117 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
118 self.children.extend(elements)
119 }
120}
121
122impl RenderOnce for Tab {
123 #[allow(refining_impl_trait)]
124 fn render(self, _: &mut Window, cx: &mut App) -> Stateful<Div> {
125 match self.style {
126 TabBarStyle::Underline => {
127 let (text_color, tab_bg) = if self.selected {
128 (
129 cx.theme().colors().text,
130 cx.theme().colors().tab_active_background,
131 )
132 } else {
133 (
134 cx.theme().colors().text_muted,
135 cx.theme().colors().tab_inactive_background,
136 )
137 };
138
139 let start = h_flex()
144 .size(START_TAB_SLOT_SIZE)
145 .justify_center()
146 .children(self.start_slot);
147 let end = h_flex()
148 .size(END_TAB_SLOT_SIZE)
149 .justify_center()
150 .children(self.end_slot);
151 let (start_slot, end_slot) = match self.close_side {
152 TabCloseSide::End => (start, end),
153 TabCloseSide::Start => (end, start),
154 };
155
156 self.div
157 .h(Tab::container_height(cx))
158 .bg(tab_bg)
159 .border_color(cx.theme().colors().border)
160 .map(|this| match self.position {
161 TabPosition::First => {
162 if self.selected {
163 this.pl_px().border_r_1().pb_px()
164 } else {
165 this.pl_px().pr_px().border_b_1()
166 }
167 }
168 TabPosition::Last => {
169 if self.selected {
170 this.border_l_1().border_r_1().pb_px()
171 } else {
172 this.pl_px().border_b_1().border_r_1()
173 }
174 }
175 TabPosition::Middle(Ordering::Equal) => {
176 this.border_l_1().border_r_1().pb_px()
177 }
178 TabPosition::Middle(Ordering::Less) => {
179 this.border_l_1().pr_px().border_b_1()
180 }
181 TabPosition::Middle(Ordering::Greater) => {
182 this.border_r_1().pl_px().border_b_1()
183 }
184 })
185 .cursor_pointer()
186 .child(
187 h_flex()
188 .group("")
189 .relative()
190 .h(Tab::content_height(cx))
191 .px(DynamicSpacing::Base04.px(cx))
192 .gap(DynamicSpacing::Base04.rems(cx))
193 .text_color(text_color)
194 .child(start_slot)
195 .children(self.children)
196 .child(end_slot),
197 )
198 }
199 TabBarStyle::Pills => {
200 let content = h_flex()
204 .w_full()
205 .items_center()
206 .gap_2()
207 .children(self.start_slot)
208 .child(
209 h_flex()
210 .flex_1()
211 .min_w_0()
212 .items_center()
213 .gap_2()
214 .children(self.children),
215 )
216 .children(self.end_slot);
217
218 let (text_color, bg) = if self.selected {
219 (semantic::text(cx), semantic::surface(cx))
220 } else {
221 (semantic::text_muted(cx), transparent_black())
222 };
223 let hover_bg = semantic::hover_bg(cx);
224
225 self.div
226 .cursor_pointer()
227 .px_3()
228 .py_1p5()
229 .rounded_md()
230 .bg(bg)
231 .text_color(text_color)
232 .when(self.selected, |this| this.shadow_level(Shadow::Sm))
233 .when(!self.selected, |this| {
234 this.hover(move |this| this.bg(hover_bg))
235 })
236 .child(content)
237 }
238 }
239 }
240}
241
242impl Component for Tab {
243 fn scope() -> ComponentScope {
244 ComponentScope::Navigation
245 }
246
247 fn description() -> Option<&'static str> {
248 Some(
249 "A tab component that can be used in a tabbed interface, supporting underline and pills styles.",
250 )
251 }
252
253 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
254 Some(
255 v_flex()
256 .gap_6()
257 .children(vec![
258 example_group_with_title(
259 "Underline",
260 vec![
261 single_example(
262 "Default",
263 Tab::new("underline_default")
264 .position(TabPosition::First)
265 .child("Default Tab")
266 .into_any_element(),
267 ),
268 single_example(
269 "Selected",
270 Tab::new("underline_selected")
271 .position(TabPosition::Middle(Ordering::Equal))
272 .toggle_state(true)
273 .child("Selected Tab")
274 .into_any_element(),
275 ),
276 single_example(
277 "Last",
278 Tab::new("underline_last")
279 .position(TabPosition::Last)
280 .child("Last Tab")
281 .into_any_element(),
282 ),
283 ],
284 ),
285 example_group_with_title(
286 "Pills",
287 vec![
288 single_example(
289 "Default",
290 Tab::new("pills_default")
291 .style(TabBarStyle::Pills)
292 .child("Default Tab")
293 .into_any_element(),
294 ),
295 single_example(
296 "Selected",
297 Tab::new("pills_selected")
298 .style(TabBarStyle::Pills)
299 .toggle_state(true)
300 .child("Selected Tab")
301 .into_any_element(),
302 ),
303 ],
304 ),
305 ])
306 .into_any_element(),
307 )
308 }
309}