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