1use std::cmp::Ordering;
2
3use gpui::{AnyElement, IntoElement, 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 .h(Tab::container_height(cx))
146 .bg(tab_bg)
147 .border_color(cx.theme().colors().border)
148 .map(|this| match self.position {
149 TabPosition::First => {
150 if self.selected {
151 this.pl_px().border_r_1().pb_px()
152 } else {
153 this.pl_px().pr_px().border_b_1()
154 }
155 }
156 TabPosition::Last => {
157 if self.selected {
158 this.border_l_1().border_r_1().pb_px()
159 } else {
160 this.pl_px().border_b_1().border_r_1()
161 }
162 }
163 TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
164 TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
165 TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
166 })
167 .cursor_pointer()
168 .child(
169 h_flex()
170 .group("")
171 .relative()
172 .h(Tab::content_height(cx))
173 .px(DynamicSpacing::Base04.px(cx))
174 .gap(DynamicSpacing::Base04.rems(cx))
175 .text_color(text_color)
176 .child(start_slot)
177 .children(self.children)
178 .child(end_slot),
179 )
180 }
181}
182
183impl Component for Tab {
184 fn scope() -> ComponentScope {
185 ComponentScope::Navigation
186 }
187
188 fn description() -> Option<&'static str> {
189 Some(
190 "A tab component that can be used in a tabbed interface, supporting different positions and states.",
191 )
192 }
193
194 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
195 Some(
196 v_flex()
197 .gap_6()
198 .children(vec![example_group_with_title(
199 "Variations",
200 vec![
201 single_example(
202 "Default",
203 Tab::new("default").child("Default Tab").into_any_element(),
204 ),
205 single_example(
206 "Selected",
207 Tab::new("selected")
208 .toggle_state(true)
209 .child("Selected Tab")
210 .into_any_element(),
211 ),
212 single_example(
213 "First",
214 Tab::new("first")
215 .position(TabPosition::First)
216 .child("First Tab")
217 .into_any_element(),
218 ),
219 single_example(
220 "Middle",
221 Tab::new("middle")
222 .position(TabPosition::Middle(Ordering::Equal))
223 .child("Middle Tab")
224 .into_any_element(),
225 ),
226 single_example(
227 "Last",
228 Tab::new("last")
229 .position(TabPosition::Last)
230 .child("Last Tab")
231 .into_any_element(),
232 ),
233 ],
234 )])
235 .into_any_element(),
236 )
237 }
238}