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