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