1use std::sync::Arc;
2
3use crate::{h_flex, ActiveTheme, Icon, IconName, Selectable, Sizable, Size, StyledExt};
4use gpui::prelude::FluentBuilder as _;
5use gpui::{
6 div, px, relative, AnyElement, App, ClickEvent, Div, Edges, ElementId, Hsla,
7 InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
8 StatefulInteractiveElement, Styled, Window,
9};
10
11#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
12pub enum TabVariant {
13 #[default]
14 Tab,
15 Outline,
16 Pill,
17 Segmented,
18 Underline,
19}
20
21#[allow(dead_code)]
22struct TabStyle {
23 borders: Edges<Pixels>,
24 border_color: Hsla,
25 bg: Hsla,
26 fg: Hsla,
27 radius: Pixels,
28 shadow: bool,
29 inner_bg: Hsla,
30 inner_radius: Pixels,
31}
32
33impl Default for TabStyle {
34 fn default() -> Self {
35 TabStyle {
36 borders: Edges::all(px(0.)),
37 border_color: gpui::transparent_white(),
38 bg: gpui::transparent_white(),
39 fg: gpui::transparent_white(),
40 radius: px(0.),
41 shadow: false,
42 inner_bg: gpui::transparent_white(),
43 inner_radius: px(0.),
44 }
45 }
46}
47
48impl TabVariant {
49 fn height(&self, size: Size) -> Pixels {
50 match size {
51 Size::XSmall => match self {
52 TabVariant::Underline => px(26.),
53 _ => px(20.),
54 },
55 Size::Small => match self {
56 TabVariant::Underline => px(30.),
57 _ => px(24.),
58 },
59 Size::Large => match self {
60 TabVariant::Underline => px(44.),
61 _ => px(36.),
62 },
63 _ => match self {
64 TabVariant::Underline => px(36.),
65 _ => px(32.),
66 },
67 }
68 }
69
70 fn inner_height(&self, size: Size) -> Pixels {
71 match size {
72 Size::XSmall => match self {
73 TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
74 TabVariant::Segmented => px(16.),
75 TabVariant::Underline => px(20.),
76 },
77 Size::Small => match self {
78 TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
79 TabVariant::Segmented => px(20.),
80 TabVariant::Underline => px(22.),
81 },
82 Size::Large => match self {
83 TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
84 TabVariant::Segmented => px(28.),
85 TabVariant::Underline => px(32.),
86 },
87 _ => match self {
88 TabVariant::Tab => px(30.),
89 TabVariant::Outline | TabVariant::Pill => px(26.),
90 TabVariant::Segmented => px(24.),
91 TabVariant::Underline => px(26.),
92 },
93 }
94 }
95
96 fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
98 let mut padding_x = match size {
99 Size::XSmall => px(8.),
100 Size::Small => px(10.),
101 Size::Large => px(16.),
102 _ => px(12.),
103 };
104
105 if matches!(self, TabVariant::Underline) {
106 padding_x = px(0.);
107 }
108
109 Edges {
110 left: padding_x,
111 right: padding_x,
112 ..Default::default()
113 }
114 }
115
116 fn inner_margins(&self, size: Size) -> Edges<Pixels> {
117 match size {
118 Size::XSmall => match self {
119 TabVariant::Underline => Edges {
120 top: px(1.),
121 bottom: px(2.),
122 ..Default::default()
123 },
124 _ => Edges::all(px(0.)),
125 },
126 Size::Small => match self {
127 TabVariant::Underline => Edges {
128 top: px(2.),
129 bottom: px(3.),
130 ..Default::default()
131 },
132 _ => Edges::all(px(0.)),
133 },
134 Size::Large => match self {
135 TabVariant::Underline => Edges {
136 top: px(5.),
137 bottom: px(6.),
138 ..Default::default()
139 },
140 _ => Edges::all(px(0.)),
141 },
142 _ => match self {
143 TabVariant::Underline => Edges {
144 top: px(3.),
145 bottom: px(4.),
146 ..Default::default()
147 },
148 _ => Edges::all(px(0.)),
149 },
150 }
151 }
152
153 fn normal(&self, cx: &App) -> TabStyle {
154 match self {
155 TabVariant::Tab => TabStyle {
156 fg: cx.theme().tab_foreground,
157 bg: cx.theme().transparent,
158 borders: Edges {
159 top: px(1.),
160 left: px(1.),
161 right: px(1.),
162 ..Default::default()
163 },
164 border_color: cx.theme().transparent,
165 ..Default::default()
166 },
167 TabVariant::Outline => TabStyle {
168 fg: cx.theme().tab_foreground,
169 bg: cx.theme().transparent,
170 borders: Edges::all(px(1.)),
171 border_color: cx.theme().border,
172 radius: px(99.),
173 ..Default::default()
174 },
175 TabVariant::Pill => TabStyle {
176 fg: cx.theme().foreground,
177 bg: cx.theme().transparent,
178 radius: px(99.),
179 ..Default::default()
180 },
181 TabVariant::Segmented => TabStyle {
182 fg: cx.theme().tab_foreground,
183 bg: cx.theme().transparent,
184 inner_radius: cx.theme().radius,
185 ..Default::default()
186 },
187 TabVariant::Underline => TabStyle {
188 fg: cx.theme().tab_foreground,
189 bg: cx.theme().transparent,
190 radius: px(0.),
191 inner_bg: cx.theme().transparent,
192 inner_radius: cx.theme().radius,
193 borders: Edges {
194 bottom: px(2.),
195 ..Default::default()
196 },
197 border_color: cx.theme().transparent,
198 ..Default::default()
199 },
200 }
201 }
202
203 fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
204 match self {
205 TabVariant::Tab => TabStyle {
206 fg: cx.theme().tab_foreground,
207 bg: cx.theme().transparent,
208 borders: Edges {
209 top: px(1.),
210 left: px(1.),
211 right: px(1.),
212 ..Default::default()
213 },
214 border_color: cx.theme().transparent,
215 ..Default::default()
216 },
217 TabVariant::Outline => TabStyle {
218 fg: cx.theme().secondary_foreground,
219 bg: cx.theme().secondary_hover,
220 borders: Edges::all(px(1.)),
221 border_color: cx.theme().border,
222 radius: px(99.),
223 ..Default::default()
224 },
225 TabVariant::Pill => TabStyle {
226 fg: cx.theme().secondary_foreground,
227 bg: cx.theme().secondary,
228 radius: px(99.),
229 ..Default::default()
230 },
231 TabVariant::Segmented => TabStyle {
232 fg: cx.theme().tab_foreground,
233 bg: cx.theme().transparent,
234 inner_bg: if selected {
235 cx.theme().background
236 } else {
237 cx.theme().transparent
238 },
239 inner_radius: cx.theme().radius,
240 ..Default::default()
241 },
242 TabVariant::Underline => TabStyle {
243 fg: cx.theme().tab_foreground,
244 bg: cx.theme().transparent,
245 radius: px(0.),
246 inner_bg: cx.theme().transparent,
247 inner_radius: cx.theme().radius,
248 borders: Edges {
249 bottom: px(2.),
250 ..Default::default()
251 },
252 border_color: cx.theme().transparent,
253 ..Default::default()
254 },
255 }
256 }
257
258 fn selected(&self, cx: &App) -> TabStyle {
259 match self {
260 TabVariant::Tab => TabStyle {
261 fg: cx.theme().tab_active_foreground,
262 bg: cx.theme().tab_active,
263 borders: Edges {
264 top: px(1.),
265 left: px(1.),
266 right: px(1.),
267 ..Default::default()
268 },
269 border_color: cx.theme().border,
270 ..Default::default()
271 },
272 TabVariant::Outline => TabStyle {
273 fg: cx.theme().primary,
274 bg: cx.theme().transparent,
275 borders: Edges::all(px(1.)),
276 border_color: cx.theme().primary,
277 radius: px(99.),
278 ..Default::default()
279 },
280 TabVariant::Pill => TabStyle {
281 fg: cx.theme().primary_foreground,
282 bg: cx.theme().primary,
283 radius: px(99.),
284 ..Default::default()
285 },
286 TabVariant::Segmented => TabStyle {
287 fg: cx.theme().tab_active_foreground,
288 bg: cx.theme().transparent,
289 inner_radius: cx.theme().radius,
290 inner_bg: cx.theme().background,
291 shadow: true,
292 ..Default::default()
293 },
294 TabVariant::Underline => TabStyle {
295 fg: cx.theme().tab_active_foreground,
296 bg: cx.theme().transparent,
297 borders: Edges {
298 bottom: px(2.),
299 ..Default::default()
300 },
301 border_color: cx.theme().primary,
302 ..Default::default()
303 },
304 }
305 }
306
307 fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
308 match self {
309 TabVariant::Tab => TabStyle {
310 fg: cx.theme().muted_foreground,
311 bg: cx.theme().transparent,
312 border_color: if selected {
313 cx.theme().border
314 } else {
315 cx.theme().transparent
316 },
317 borders: Edges {
318 top: px(1.),
319 left: px(1.),
320 right: px(1.),
321 ..Default::default()
322 },
323 ..Default::default()
324 },
325 TabVariant::Outline => TabStyle {
326 fg: cx.theme().muted_foreground,
327 bg: cx.theme().transparent,
328 borders: Edges::all(px(1.)),
329 border_color: if selected {
330 cx.theme().primary
331 } else {
332 cx.theme().border
333 },
334 radius: px(99.),
335 ..Default::default()
336 },
337 TabVariant::Pill => TabStyle {
338 fg: if selected {
339 cx.theme().primary_foreground.opacity(0.5)
340 } else {
341 cx.theme().muted_foreground
342 },
343 bg: if selected {
344 cx.theme().primary.opacity(0.5)
345 } else {
346 cx.theme().transparent
347 },
348 radius: px(99.),
349 ..Default::default()
350 },
351 TabVariant::Segmented => TabStyle {
352 fg: cx.theme().muted_foreground,
353 bg: cx.theme().tab_bar,
354 inner_bg: if selected {
355 cx.theme().background
356 } else {
357 cx.theme().transparent
358 },
359 inner_radius: cx.theme().radius,
360 ..Default::default()
361 },
362 TabVariant::Underline => TabStyle {
363 fg: cx.theme().muted_foreground,
364 bg: cx.theme().transparent,
365 radius: cx.theme().radius,
366 border_color: if selected {
367 cx.theme().border
368 } else {
369 cx.theme().transparent
370 },
371 borders: Edges {
372 bottom: px(2.),
373 ..Default::default()
374 },
375 ..Default::default()
376 },
377 }
378 }
379}
380
381#[derive(IntoElement)]
382pub struct Tab {
383 id: ElementId,
384 base: Div,
385 pub(super) label: Option<SharedString>,
386 icon: Option<Icon>,
387 prefix: Option<AnyElement>,
388 suffix: Option<AnyElement>,
389 children: Vec<AnyElement>,
390 variant: TabVariant,
391 size: Size,
392 pub(super) disabled: bool,
393 pub(super) selected: bool,
394 on_click: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
395}
396
397impl From<&'static str> for Tab {
398 fn from(label: &'static str) -> Self {
399 let label = SharedString::from(label);
400 Self::new(label)
401 }
402}
403
404impl From<String> for Tab {
405 fn from(label: String) -> Self {
406 let label = SharedString::from(label);
407 Self::new(label)
408 }
409}
410
411impl From<SharedString> for Tab {
412 fn from(label: SharedString) -> Self {
413 Self::new(label)
414 }
415}
416
417impl From<Icon> for Tab {
418 fn from(icon: Icon) -> Self {
419 Self::icon(icon)
420 }
421}
422
423impl From<IconName> for Tab {
424 fn from(icon_name: IconName) -> Self {
425 Self::icon(Icon::new(icon_name))
426 }
427}
428
429impl Default for Tab {
430 fn default() -> Self {
431 Self {
432 id: ElementId::Integer(0),
433 base: div(),
434 label: None,
435 icon: None,
436 children: Vec::new(),
437 disabled: false,
438 selected: false,
439 prefix: None,
440 suffix: None,
441 variant: TabVariant::default(),
442 size: Size::default(),
443 on_click: None,
444 }
445 }
446}
447
448impl Tab {
449 pub fn new(label: impl Into<SharedString>) -> Self {
451 let mut this = Self::default();
452 this.label = Some(label.into());
453 this
454 }
455
456 pub fn empty() -> Self {
458 Self::default()
459 }
460
461 pub fn icon(icon: impl Into<Icon>) -> Self {
463 let mut this = Self::default();
464 this.icon = Some(icon.into());
465 this
466 }
467
468 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
470 self.id = id.into();
471 self
472 }
473
474 pub fn with_variant(mut self, variant: TabVariant) -> Self {
476 self.variant = variant;
477 self
478 }
479
480 pub fn pill(mut self) -> Self {
482 self.variant = TabVariant::Pill;
483 self
484 }
485
486 pub fn outline(mut self) -> Self {
488 self.variant = TabVariant::Outline;
489 self
490 }
491
492 pub fn segmented(mut self) -> Self {
494 self.variant = TabVariant::Segmented;
495 self
496 }
497
498 pub fn underline(mut self) -> Self {
500 self.variant = TabVariant::Underline;
501 self
502 }
503
504 pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
506 self.prefix = Some(prefix.into());
507 self
508 }
509
510 pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> Self {
512 self.suffix = Some(suffix.into());
513 self
514 }
515
516 pub fn disabled(mut self, disabled: bool) -> Self {
518 self.disabled = disabled;
519 self
520 }
521
522 pub fn on_click(
524 mut self,
525 on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
526 ) -> Self {
527 self.on_click = Some(Arc::new(on_click));
528 self
529 }
530}
531
532impl ParentElement for Tab {
533 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
534 self.children.extend(elements);
535 }
536}
537
538impl Selectable for Tab {
539 fn selected(mut self, selected: bool) -> Self {
540 self.selected = selected;
541 self
542 }
543
544 fn is_selected(&self) -> bool {
545 self.selected
546 }
547}
548
549impl InteractiveElement for Tab {
550 fn interactivity(&mut self) -> &mut gpui::Interactivity {
551 self.base.interactivity()
552 }
553}
554
555impl StatefulInteractiveElement for Tab {}
556
557impl Styled for Tab {
558 fn style(&mut self) -> &mut gpui::StyleRefinement {
559 self.base.style()
560 }
561}
562
563impl Sizable for Tab {
564 fn with_size(mut self, size: impl Into<Size>) -> Self {
565 self.size = size.into();
566 self
567 }
568}
569
570impl RenderOnce for Tab {
571 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
572 let mut tab_style = if self.selected {
573 self.variant.selected(cx)
574 } else {
575 self.variant.normal(cx)
576 };
577 let mut hover_style = self.variant.hovered(self.selected, cx);
578 if self.disabled {
579 tab_style = self.variant.disabled(self.selected, cx);
580 hover_style = self.variant.disabled(self.selected, cx);
581 }
582 let inner_paddings = self.variant.inner_paddings(self.size);
583 let inner_margins = self.variant.inner_margins(self.size);
584 let inner_height = self.variant.inner_height(self.size);
585 let height = self.variant.height(self.size);
586
587 self.base
588 .id(self.id)
589 .flex()
590 .flex_wrap()
591 .gap_1()
592 .items_center()
593 .flex_shrink_0()
594 .overflow_hidden()
595 .h(height)
596 .overflow_hidden()
597 .text_color(tab_style.fg)
598 .map(|this| match self.size {
599 Size::XSmall => this.text_xs(),
600 Size::Large => this.text_base(),
601 _ => this.text_sm(),
602 })
603 .bg(tab_style.bg)
604 .border_l(tab_style.borders.left)
605 .border_r(tab_style.borders.right)
606 .border_t(tab_style.borders.top)
607 .border_b(tab_style.borders.bottom)
608 .border_color(tab_style.border_color)
609 .rounded(tab_style.radius)
610 .when(!self.selected && !self.disabled, |this| {
611 this.hover(|this| {
612 this.text_color(hover_style.fg)
613 .bg(hover_style.bg)
614 .border_l(hover_style.borders.left)
615 .border_r(hover_style.borders.right)
616 .border_t(hover_style.borders.top)
617 .border_b(hover_style.borders.bottom)
618 .border_color(hover_style.border_color)
619 .rounded(tab_style.radius)
620 })
621 })
622 .when_some(self.prefix, |this, prefix| this.child(prefix))
623 .child(
624 h_flex()
625 .h(inner_height)
626 .line_height(relative(1.))
627 .items_center()
628 .justify_center()
629 .overflow_hidden()
630 .margins(inner_margins)
631 .flex_shrink_0()
632 .map(|this| match self.icon {
633 Some(icon) => {
634 this.w(inner_height * 1.25)
635 .child(icon.map(|this| match self.size {
636 Size::XSmall => this.size_2p5(),
637 Size::Small => this.size_3p5(),
638 Size::Large => this.size_4(),
639 _ => this.size_4(),
640 }))
641 }
642 None => this
643 .paddings(inner_paddings)
644 .map(|this| match self.label {
645 Some(label) => this.child(label),
646 None => this,
647 })
648 .children(self.children),
649 })
650 .bg(tab_style.inner_bg)
651 .rounded(tab_style.inner_radius)
652 .when(tab_style.shadow, |this| this.shadow_xs())
653 .hover(|this| {
654 this.bg(hover_style.inner_bg)
655 .rounded(hover_style.inner_radius)
656 }),
657 )
658 .when_some(self.suffix, |this, suffix| this.child(suffix))
659 .when(!self.disabled, |this| {
660 this.when_some(self.on_click.clone(), |this, on_click| {
661 this.on_click(move |event, window, cx| on_click(event, window, cx))
662 })
663 })
664 }
665}