1use std::cmp::Ordering;
2
3use gpui::{AnyElement, IntoElement, Stateful};
4use smallvec::SmallVec;
5
6use crate::prelude::*;
7
8/// The position of a [`Tab`] within a list of tabs.
9#[derive(Debug, PartialEq, Eq, Clone, Copy)]
10pub enum TabPosition {
11 /// The tab is first in the list.
12 First,
13
14 /// The tab is in the middle of the list (i.e., it is not the first or last tab).
15 ///
16 /// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
17 Middle(Ordering),
18
19 /// The tab is last in the list.
20 Last,
21}
22
23#[derive(Debug, PartialEq, Eq, Clone, Copy)]
24pub enum TabCloseSide {
25 Start,
26 End,
27}
28
29#[derive(IntoElement, RegisterComponent)]
30pub struct Tab {
31 div: Stateful<Div>,
32 selected: bool,
33 position: TabPosition,
34 close_side: TabCloseSide,
35 start_slot: Option<AnyElement>,
36 end_slot: Option<AnyElement>,
37 children: SmallVec<[AnyElement; 2]>,
38}
39
40impl Tab {
41 pub fn new(id: impl Into<ElementId>) -> Self {
42 let id = id.into();
43 Self {
44 div: div()
45 .id(id.clone())
46 .debug_selector(|| format!("TAB-{}", id)),
47 selected: false,
48 position: TabPosition::First,
49 close_side: TabCloseSide::End,
50 start_slot: None,
51 end_slot: None,
52 children: SmallVec::new(),
53 }
54 }
55
56 pub fn position(mut self, position: TabPosition) -> Self {
57 self.position = position;
58 self
59 }
60
61 pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
62 self.close_side = close_side;
63 self
64 }
65
66 pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
67 self.start_slot = element.into().map(IntoElement::into_any_element);
68 self
69 }
70
71 pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
72 self.end_slot = element.into().map(IntoElement::into_any_element);
73 self
74 }
75
76 pub fn content_height(cx: &App) -> Pixels {
77 DynamicSpacing::Base32.px(cx) - px(1.)
78 }
79
80 pub fn container_height(cx: &App) -> Pixels {
81 DynamicSpacing::Base32.px(cx)
82 }
83}
84
85impl InteractiveElement for Tab {
86 fn interactivity(&mut self) -> &mut gpui::Interactivity {
87 self.div.interactivity()
88 }
89}
90
91impl StatefulInteractiveElement for Tab {}
92
93impl Toggleable for Tab {
94 fn toggle_state(mut self, selected: bool) -> Self {
95 self.selected = selected;
96 self
97 }
98}
99
100impl ParentElement for Tab {
101 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
102 self.children.extend(elements)
103 }
104}
105
106impl RenderOnce for Tab {
107 #[allow(refining_impl_trait)]
108 fn render(self, _: &mut Window, cx: &mut App) -> Stateful<Div> {
109 let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
110 false => (
111 cx.theme().colors().text_muted,
112 cx.theme().colors().tab_inactive_background,
113 cx.theme().colors().ghost_element_hover,
114 cx.theme().colors().ghost_element_active,
115 ),
116 true => (
117 cx.theme().colors().text,
118 cx.theme().colors().tab_active_background,
119 cx.theme().colors().element_hover,
120 cx.theme().colors().element_active,
121 ),
122 };
123
124 let (start_slot, end_slot) = {
125 let start_slot = h_flex()
126 .size(px(12.)) // use px over rem from size_3
127 .justify_center()
128 .children(self.start_slot);
129
130 let end_slot = h_flex()
131 .size(px(12.)) // use px over rem from size_3
132 .justify_center()
133 .children(self.end_slot);
134
135 match self.close_side {
136 TabCloseSide::End => (start_slot, end_slot),
137 TabCloseSide::Start => (end_slot, start_slot),
138 }
139 };
140
141 self.div
142 .h(Tab::container_height(cx))
143 .bg(tab_bg)
144 .border_color(cx.theme().colors().border)
145 .map(|this| match self.position {
146 TabPosition::First => {
147 if self.selected {
148 this.pl_px().border_r_1().pb_px()
149 } else {
150 this.pl_px().pr_px().border_b_1()
151 }
152 }
153 TabPosition::Last => {
154 if self.selected {
155 this.border_l_1().border_r_1().pb_px()
156 } else {
157 this.pl_px().border_b_1().border_r_1()
158 }
159 }
160 TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
161 TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
162 TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
163 })
164 .cursor_pointer()
165 .child(
166 h_flex()
167 .group("")
168 .relative()
169 .h(Tab::content_height(cx))
170 .px(DynamicSpacing::Base04.px(cx))
171 .gap(DynamicSpacing::Base04.rems(cx))
172 .text_color(text_color)
173 .child(start_slot)
174 .children(self.children)
175 .child(end_slot),
176 )
177 }
178}
179
180impl Component for Tab {
181 fn scope() -> ComponentScope {
182 ComponentScope::Navigation
183 }
184
185 fn description() -> Option<&'static str> {
186 Some(
187 "A tab component that can be used in a tabbed interface, supporting different positions and states.",
188 )
189 }
190
191 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
192 Some(
193 v_flex()
194 .gap_6()
195 .children(vec![example_group_with_title(
196 "Variations",
197 vec![
198 single_example(
199 "Default",
200 Tab::new("default").child("Default Tab").into_any_element(),
201 ),
202 single_example(
203 "Selected",
204 Tab::new("selected")
205 .toggle_state(true)
206 .child("Selected Tab")
207 .into_any_element(),
208 ),
209 single_example(
210 "First",
211 Tab::new("first")
212 .position(TabPosition::First)
213 .child("First Tab")
214 .into_any_element(),
215 ),
216 single_example(
217 "Middle",
218 Tab::new("middle")
219 .position(TabPosition::Middle(Ordering::Equal))
220 .child("Middle Tab")
221 .into_any_element(),
222 ),
223 single_example(
224 "Last",
225 Tab::new("last")
226 .position(TabPosition::Last)
227 .child("Last Tab")
228 .into_any_element(),
229 ),
230 ],
231 )])
232 .into_any_element(),
233 )
234 }
235}