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