1#![allow(missing_docs)]
2use std::cmp::Ordering;
3
4use gpui::{AnyElement, IntoElement, Stateful};
5use smallvec::SmallVec;
6
7use crate::{prelude::*, BASE_REM_SIZE_IN_PX};
8
9/// The position of a [`Tab`] within a list of tabs.
10#[derive(Debug, PartialEq, Eq, Clone, Copy)]
11pub enum TabPosition {
12 /// The tab is first in the list.
13 First,
14
15 /// The tab is in the middle of the list (i.e., it is not the first or last tab).
16 ///
17 /// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
18 Middle(Ordering),
19
20 /// The tab is last in the list.
21 Last,
22}
23
24#[derive(Debug, PartialEq, Eq, Clone, Copy)]
25pub enum TabCloseSide {
26 Start,
27 End,
28}
29
30#[derive(IntoElement)]
31pub struct Tab {
32 div: Stateful<Div>,
33 selected: bool,
34 position: TabPosition,
35 close_side: TabCloseSide,
36 start_slot: Option<AnyElement>,
37 end_slot: Option<AnyElement>,
38 children: SmallVec<[AnyElement; 2]>,
39}
40
41impl Tab {
42 pub fn new(id: impl Into<ElementId>) -> Self {
43 let id = id.into();
44 Self {
45 div: div()
46 .id(id.clone())
47 .debug_selector(|| format!("TAB-{}", id)),
48 selected: false,
49 position: TabPosition::First,
50 close_side: TabCloseSide::End,
51 start_slot: None,
52 end_slot: None,
53 children: SmallVec::new(),
54 }
55 }
56
57 pub const CONTAINER_HEIGHT_IN_REMS: f32 = 29. / BASE_REM_SIZE_IN_PX;
58
59 const CONTENT_HEIGHT_IN_REMS: f32 = 28. / BASE_REM_SIZE_IN_PX;
60
61 pub fn position(mut self, position: TabPosition) -> Self {
62 self.position = position;
63 self
64 }
65
66 pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
67 self.close_side = close_side;
68 self
69 }
70
71 pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
72 self.start_slot = element.into().map(IntoElement::into_any_element);
73 self
74 }
75
76 pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
77 self.end_slot = element.into().map(IntoElement::into_any_element);
78 self
79 }
80}
81
82impl InteractiveElement for Tab {
83 fn interactivity(&mut self) -> &mut gpui::Interactivity {
84 self.div.interactivity()
85 }
86}
87
88impl StatefulInteractiveElement for Tab {}
89
90impl Selectable for Tab {
91 fn selected(mut self, selected: bool) -> Self {
92 self.selected = selected;
93 self
94 }
95}
96
97impl ParentElement for Tab {
98 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
99 self.children.extend(elements)
100 }
101}
102
103impl RenderOnce for Tab {
104 #[allow(refining_impl_trait)]
105 fn render(self, cx: &mut WindowContext) -> Stateful<Div> {
106 let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
107 false => (
108 cx.theme().colors().text_muted,
109 cx.theme().colors().tab_inactive_background,
110 cx.theme().colors().ghost_element_hover,
111 cx.theme().colors().ghost_element_active,
112 ),
113 true => (
114 cx.theme().colors().text,
115 cx.theme().colors().tab_active_background,
116 cx.theme().colors().element_hover,
117 cx.theme().colors().element_active,
118 ),
119 };
120
121 let (start_slot, end_slot) = {
122 let start_slot = h_flex().size_3().justify_center().children(self.start_slot);
123
124 let end_slot = h_flex().size_3().justify_center().children(self.end_slot);
125
126 match self.close_side {
127 TabCloseSide::End => (start_slot, end_slot),
128 TabCloseSide::Start => (end_slot, start_slot),
129 }
130 };
131
132 self.div
133 .h(rems(Self::CONTAINER_HEIGHT_IN_REMS))
134 .bg(tab_bg)
135 .border_color(cx.theme().colors().border)
136 .map(|this| match self.position {
137 TabPosition::First => {
138 if self.selected {
139 this.pl_px().border_r_1().pb_px()
140 } else {
141 this.pl_px().pr_px().border_b_1()
142 }
143 }
144 TabPosition::Last => {
145 if self.selected {
146 this.border_l_1().border_r_1().pb_px()
147 } else {
148 this.pr_px().pl_px().border_b_1().border_r_1()
149 }
150 }
151 TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
152 TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
153 TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
154 })
155 .cursor_pointer()
156 .child(
157 h_flex()
158 .group("")
159 .relative()
160 .h(rems(Self::CONTENT_HEIGHT_IN_REMS))
161 .px(crate::custom_spacing(cx, 4.))
162 .gap(Spacing::Small.rems(cx))
163 .text_color(text_color)
164 .child(start_slot)
165 .children(self.children)
166 .child(end_slot),
167 )
168 }
169}