1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone)]
10pub struct TabsTheme {
11 pub container_bg: Rgba,
13 pub container_border: Rgba,
15 pub selected_bg: Rgba,
17 pub selected_hover_bg: Rgba,
19 pub hover_bg: Rgba,
21 pub accent: Rgba,
23 pub text_selected: Rgba,
25 pub text_unselected: Rgba,
27 pub text_hover: Rgba,
29 pub badge_bg: Rgba,
31 pub close_color: Rgba,
33 pub close_hover_color: Rgba,
35}
36
37impl Default for TabsTheme {
38 fn default() -> Self {
39 Self {
40 container_bg: rgba(0x2a2a2aff),
41 container_border: rgba(0x3a3a3aff),
42 selected_bg: rgba(0x3a3a3aff),
43 selected_hover_bg: rgba(0x4a4a4aff),
44 hover_bg: rgba(0x2a2a2aff),
45 accent: rgba(0x007accff),
46 text_selected: rgba(0xffffffff),
47 text_unselected: rgba(0x888888ff),
48 text_hover: rgba(0xccccccff),
49 badge_bg: rgba(0x555555ff),
50 close_color: rgba(0x888888ff),
51 close_hover_color: rgba(0xffffffff),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum TabVariant {
59 #[default]
61 Underline,
62 Enclosed,
64 Pills,
66}
67
68pub struct TabItem {
70 id: SharedString,
71 label: SharedString,
72 icon: Option<SharedString>,
73 custom_icon: Option<AnyElement>,
74 badge: Option<SharedString>,
75 disabled: bool,
76 closeable: bool,
77}
78
79impl TabItem {
80 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
82 Self {
83 id: id.into(),
84 label: label.into(),
85 icon: None,
86 custom_icon: None,
87 badge: None,
88 disabled: false,
89 closeable: false,
90 }
91 }
92
93 pub fn icon(mut self, icon: impl Into<SharedString>) -> Self {
95 self.icon = Some(icon.into());
96 self
97 }
98
99 pub fn custom_icon(mut self, icon: impl IntoElement) -> Self {
101 self.custom_icon = Some(icon.into_any_element());
102 self
103 }
104
105 pub fn badge(mut self, badge: impl Into<SharedString>) -> Self {
107 self.badge = Some(badge.into());
108 self
109 }
110
111 pub fn disabled(mut self, disabled: bool) -> Self {
113 self.disabled = disabled;
114 self
115 }
116
117 pub fn closeable(mut self, closeable: bool) -> Self {
119 self.closeable = closeable;
120 self
121 }
122
123 pub fn id(&self) -> &SharedString {
125 &self.id
126 }
127}
128
129pub struct Tabs {
131 tabs: Vec<TabItem>,
132 selected_index: usize,
133 variant: TabVariant,
134 theme: Option<TabsTheme>,
135 on_change: Option<Box<dyn Fn(usize, &mut Window, &mut App) + 'static>>,
136 on_close: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
137}
138
139impl Tabs {
140 pub fn new() -> Self {
142 Self {
143 tabs: Vec::new(),
144 selected_index: 0,
145 variant: TabVariant::default(),
146 theme: None,
147 on_change: None,
148 on_close: None,
149 }
150 }
151
152 pub fn tabs(mut self, tabs: Vec<TabItem>) -> Self {
154 self.tabs = tabs;
155 self
156 }
157
158 pub fn selected_index(mut self, index: usize) -> Self {
160 self.selected_index = index;
161 self
162 }
163
164 pub fn variant(mut self, variant: TabVariant) -> Self {
166 self.variant = variant;
167 self
168 }
169
170 pub fn theme(mut self, theme: TabsTheme) -> Self {
172 self.theme = Some(theme);
173 self
174 }
175
176 pub fn on_change(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
178 self.on_change = Some(Box::new(handler));
179 self
180 }
181
182 pub fn on_close(
184 mut self,
185 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
186 ) -> Self {
187 self.on_close = Some(Box::new(handler));
188 self
189 }
190
191 pub fn build(self) -> Div {
193 let default_theme = TabsTheme::default();
194 let theme = self.theme.as_ref().unwrap_or(&default_theme);
195
196 let mut container = div().flex().items_center();
197
198 match self.variant {
200 TabVariant::Underline => {
201 container = container.border_b_1().border_color(theme.container_border);
202 }
203 TabVariant::Enclosed => {
204 container = container.gap_1();
205 }
206 TabVariant::Pills => {
207 container = container.gap_2().p_1().bg(theme.container_bg).rounded_lg();
208 }
209 }
210
211 for (index, tab) in self.tabs.into_iter().enumerate() {
212 let is_selected = index == self.selected_index;
213 let tab_id = tab.id.clone();
214 let label = tab.label;
215 let icon = tab.icon;
216 let custom_icon = tab.custom_icon;
217 let badge = tab.badge;
218 let disabled = tab.disabled;
219 let closeable = tab.closeable;
220
221 let on_change: Option<*const dyn Fn(usize, &mut Window, &mut App)> =
222 self.on_change.as_ref().map(|f| f.as_ref() as *const _);
223 let on_close: Option<*const dyn Fn(&SharedString, &mut Window, &mut App)> =
224 self.on_close.as_ref().map(|f| f.as_ref() as *const _);
225
226 let mut tab_el = div()
227 .id(SharedString::from(format!("tab-{}", tab_id)))
228 .flex()
229 .items_center()
230 .gap_2()
231 .px_4()
232 .py_2();
233
234 match self.variant {
236 TabVariant::Underline => {
237 if is_selected {
238 tab_el = tab_el
239 .border_b_2()
240 .border_color(theme.accent)
241 .text_color(theme.text_selected)
242 .font_weight(FontWeight::SEMIBOLD);
243 } else {
244 let hover_color = theme.text_hover;
245 tab_el = tab_el
246 .text_color(theme.text_unselected)
247 .hover(move |s| s.text_color(hover_color));
248 }
249 }
250 TabVariant::Enclosed => {
251 if is_selected {
252 tab_el = tab_el
253 .bg(theme.selected_bg)
254 .rounded_t_md()
255 .text_color(theme.text_selected);
256 } else {
257 let hover_bg = theme.hover_bg;
258 let hover_text = theme.text_hover;
259 tab_el = tab_el
260 .text_color(theme.text_unselected)
261 .hover(move |s| s.bg(hover_bg).text_color(hover_text));
262 }
263 }
264 TabVariant::Pills => {
265 if is_selected {
266 tab_el = tab_el
267 .bg(theme.accent)
268 .rounded_md()
269 .text_color(theme.text_selected);
270 } else {
271 let hover_bg = theme.selected_bg;
272 let hover_text = theme.text_hover;
273 tab_el = tab_el
274 .rounded_md()
275 .text_color(theme.text_unselected)
276 .hover(move |s| s.bg(hover_bg).text_color(hover_text));
277 }
278 }
279 }
280
281 if disabled {
282 tab_el = tab_el.opacity(0.5).cursor_not_allowed();
283 } else {
284 tab_el = tab_el.cursor_pointer();
285
286 if let Some(handler_ptr) = on_change {
288 let idx = index;
289 tab_el =
290 tab_el.on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
291 (*handler_ptr)(idx, window, cx);
292 });
293 }
294 }
295
296 if let Some(custom_icon) = custom_icon {
298 tab_el = tab_el.child(custom_icon);
299 } else if let Some(icon) = icon {
300 tab_el = tab_el.child(div().text_sm().child(icon));
302 }
303
304 tab_el = tab_el.child(div().text_sm().child(label));
306
307 if let Some(badge) = badge {
309 tab_el = tab_el.child(
310 div()
311 .text_xs()
312 .px_1()
313 .py(px(1.0))
314 .bg(theme.badge_bg)
315 .rounded(px(3.0))
316 .child(badge),
317 );
318 }
319
320 if closeable {
322 let id = tab_id.clone();
323 let close_color = theme.close_color;
324 let close_hover = theme.close_hover_color;
325 let mut close_btn = div()
326 .id(SharedString::from(format!("tab-close-{}", tab_id)))
327 .text_xs()
328 .text_color(close_color)
329 .hover(move |s| s.text_color(close_hover));
330
331 if let Some(handler_ptr) = on_close {
332 close_btn = close_btn.on_mouse_up(
333 MouseButton::Left,
334 move |_event, window, cx| unsafe {
335 (*handler_ptr)(&id, window, cx);
336 },
337 );
338 }
339
340 tab_el = tab_el.child(close_btn.child("×"));
341 }
342
343 container = container.child(tab_el);
344 }
345
346 container
347 }
348}
349
350impl Default for Tabs {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356impl IntoElement for Tabs {
357 type Element = Div;
358
359 fn into_element(self) -> Self::Element {
360 self.build()
361 }
362}