1#![allow(missing_docs)]
2use std::cmp::Ordering;
3
4use gpui::{AnyElement, IntoElement, Stateful};
5use smallvec::SmallVec;
6
7use crate::prelude::*;
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, IntoComponent)]
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 fn position(mut self, position: TabPosition) -> Self {
58 self.position = position;
59 self
60 }
61
62 pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
63 self.close_side = close_side;
64 self
65 }
66
67 pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
68 self.start_slot = element.into().map(IntoElement::into_any_element);
69 self
70 }
71
72 pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
73 self.end_slot = element.into().map(IntoElement::into_any_element);
74 self
75 }
76
77 pub fn content_height(cx: &mut App) -> Pixels {
78 DynamicSpacing::Base32.px(cx) - px(1.)
79 }
80
81 pub fn container_height(cx: &mut App) -> Pixels {
82 DynamicSpacing::Base32.px(cx)
83 }
84}
85
86impl InteractiveElement for Tab {
87 fn interactivity(&mut self) -> &mut gpui::Interactivity {
88 self.div.interactivity()
89 }
90}
91
92impl StatefulInteractiveElement for Tab {}
93
94impl Toggleable for Tab {
95 fn toggle_state(mut self, selected: bool) -> Self {
96 self.selected = selected;
97 self
98 }
99}
100
101impl ParentElement for Tab {
102 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
103 self.children.extend(elements)
104 }
105}
106
107impl RenderOnce for Tab {
108 #[allow(refining_impl_trait)]
109 fn render(self, _: &mut Window, cx: &mut App) -> Stateful<Div> {
110 let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
111 false => (
112 cx.theme().colors().text_muted,
113 cx.theme().colors().tab_inactive_background,
114 cx.theme().colors().ghost_element_hover,
115 cx.theme().colors().ghost_element_active,
116 ),
117 true => (
118 cx.theme().colors().text,
119 cx.theme().colors().tab_active_background,
120 cx.theme().colors().element_hover,
121 cx.theme().colors().element_active,
122 ),
123 };
124
125 let (start_slot, end_slot) = {
126 let start_slot = h_flex().size_3().justify_center().children(self.start_slot);
127
128 let end_slot = h_flex().size_3().justify_center().children(self.end_slot);
129
130 match self.close_side {
131 TabCloseSide::End => (start_slot, end_slot),
132 TabCloseSide::Start => (end_slot, start_slot),
133 }
134 };
135
136 self.div
137 .h(Tab::container_height(cx))
138 .bg(tab_bg)
139 .border_color(cx.theme().colors().border)
140 .map(|this| match self.position {
141 TabPosition::First => {
142 if self.selected {
143 this.pl_px().border_r_1().pb_px()
144 } else {
145 this.pl_px().pr_px().border_b_1()
146 }
147 }
148 TabPosition::Last => {
149 if self.selected {
150 this.border_l_1().border_r_1().pb_px()
151 } else {
152 this.pr_px().pl_px().border_b_1().border_r_1()
153 }
154 }
155 TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
156 TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
157 TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
158 })
159 .cursor_pointer()
160 .child(
161 h_flex()
162 .group("")
163 .relative()
164 .h(Tab::content_height(cx))
165 .px(DynamicSpacing::Base04.px(cx))
166 .gap(DynamicSpacing::Base04.rems(cx))
167 .text_color(text_color)
168 .child(start_slot)
169 .children(self.children)
170 .child(end_slot),
171 )
172 }
173}
174
175// View this component preview using `workspace: open component-preview`
176impl ComponentPreview for Tab {
177 fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
178 v_flex()
179 .gap_6()
180 .children(vec![example_group_with_title(
181 "Variations",
182 vec![
183 single_example(
184 "Default",
185 Tab::new("default").child("Default Tab").into_any_element(),
186 ),
187 single_example(
188 "Selected",
189 Tab::new("selected")
190 .toggle_state(true)
191 .child("Selected Tab")
192 .into_any_element(),
193 ),
194 single_example(
195 "First",
196 Tab::new("first")
197 .position(TabPosition::First)
198 .child("First Tab")
199 .into_any_element(),
200 ),
201 single_example(
202 "Middle",
203 Tab::new("middle")
204 .position(TabPosition::Middle(Ordering::Equal))
205 .child("Middle Tab")
206 .into_any_element(),
207 ),
208 single_example(
209 "Last",
210 Tab::new("last")
211 .position(TabPosition::Last)
212 .child("Last Tab")
213 .into_any_element(),
214 ),
215 ],
216 )])
217 .into_any_element()
218 }
219}