1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum TabVariant {
11 #[default]
13 Underline,
14 Enclosed,
16 Pills,
18}
19
20#[derive(Clone)]
22pub struct TabItem {
23 id: SharedString,
24 label: SharedString,
25 icon: Option<SharedString>,
26 badge: Option<SharedString>,
27 disabled: bool,
28 closeable: bool,
29}
30
31impl TabItem {
32 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
34 Self {
35 id: id.into(),
36 label: label.into(),
37 icon: None,
38 badge: None,
39 disabled: false,
40 closeable: false,
41 }
42 }
43
44 pub fn icon(mut self, icon: impl Into<SharedString>) -> Self {
46 self.icon = Some(icon.into());
47 self
48 }
49
50 pub fn badge(mut self, badge: impl Into<SharedString>) -> Self {
52 self.badge = Some(badge.into());
53 self
54 }
55
56 pub fn disabled(mut self, disabled: bool) -> Self {
58 self.disabled = disabled;
59 self
60 }
61
62 pub fn closeable(mut self, closeable: bool) -> Self {
64 self.closeable = closeable;
65 self
66 }
67
68 pub fn id(&self) -> &SharedString {
70 &self.id
71 }
72}
73
74pub struct Tabs {
76 tabs: Vec<TabItem>,
77 selected_index: usize,
78 variant: TabVariant,
79 on_change: Option<Box<dyn Fn(usize, &mut Window, &mut App) + 'static>>,
80 on_close: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
81}
82
83impl Tabs {
84 pub fn new() -> Self {
86 Self {
87 tabs: Vec::new(),
88 selected_index: 0,
89 variant: TabVariant::default(),
90 on_change: None,
91 on_close: None,
92 }
93 }
94
95 pub fn tabs(mut self, tabs: Vec<TabItem>) -> Self {
97 self.tabs = tabs;
98 self
99 }
100
101 pub fn selected_index(mut self, index: usize) -> Self {
103 self.selected_index = index;
104 self
105 }
106
107 pub fn variant(mut self, variant: TabVariant) -> Self {
109 self.variant = variant;
110 self
111 }
112
113 pub fn on_change(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
115 self.on_change = Some(Box::new(handler));
116 self
117 }
118
119 pub fn on_close(
121 mut self,
122 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
123 ) -> Self {
124 self.on_close = Some(Box::new(handler));
125 self
126 }
127
128 pub fn build(self) -> Div {
130 let mut container = div().flex().items_center();
131
132 match self.variant {
134 TabVariant::Underline => {
135 container = container.border_b_1().border_color(rgb(0x3a3a3a));
136 }
137 TabVariant::Enclosed => {
138 container = container.gap_1();
139 }
140 TabVariant::Pills => {
141 container = container.gap_2().p_1().bg(rgb(0x2a2a2a)).rounded_lg();
142 }
143 }
144
145 for (index, tab) in self.tabs.iter().enumerate() {
146 let is_selected = index == self.selected_index;
147 let tab_id = tab.id.clone();
148 let label = tab.label.clone();
149 let icon = tab.icon.clone();
150 let badge = tab.badge.clone();
151 let disabled = tab.disabled;
152 let closeable = tab.closeable;
153
154 let on_change: Option<*const dyn Fn(usize, &mut Window, &mut App)> =
155 self.on_change.as_ref().map(|f| f.as_ref() as *const _);
156 let on_close: Option<*const dyn Fn(&SharedString, &mut Window, &mut App)> =
157 self.on_close.as_ref().map(|f| f.as_ref() as *const _);
158
159 let mut tab_el = div()
160 .id(SharedString::from(format!("tab-{}", tab_id)))
161 .flex()
162 .items_center()
163 .gap_2()
164 .px_4()
165 .py_2();
166
167 match self.variant {
169 TabVariant::Underline => {
170 if is_selected {
171 tab_el = tab_el
172 .border_b_2()
173 .border_color(rgb(0x007acc))
174 .text_color(rgb(0xffffff))
175 .font_weight(FontWeight::SEMIBOLD);
176 } else {
177 tab_el = tab_el
178 .text_color(rgb(0x888888))
179 .hover(|s| s.text_color(rgb(0xcccccc)));
180 }
181 }
182 TabVariant::Enclosed => {
183 if is_selected {
184 tab_el = tab_el
185 .bg(rgb(0x3a3a3a))
186 .rounded_t_md()
187 .text_color(rgb(0xffffff));
188 } else {
189 tab_el = tab_el
190 .text_color(rgb(0x888888))
191 .hover(|s| s.bg(rgb(0x2a2a2a)).text_color(rgb(0xcccccc)));
192 }
193 }
194 TabVariant::Pills => {
195 if is_selected {
196 tab_el = tab_el
197 .bg(rgb(0x007acc))
198 .rounded_md()
199 .text_color(rgb(0xffffff));
200 } else {
201 tab_el = tab_el
202 .rounded_md()
203 .text_color(rgb(0x888888))
204 .hover(|s| s.bg(rgb(0x3a3a3a)).text_color(rgb(0xcccccc)));
205 }
206 }
207 }
208
209 if disabled {
210 tab_el = tab_el.opacity(0.5).cursor_not_allowed();
211 } else {
212 tab_el = tab_el.cursor_pointer();
213
214 if let Some(handler_ptr) = on_change {
216 let idx = index;
217 tab_el =
218 tab_el.on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
219 (*handler_ptr)(idx, window, cx);
220 });
221 }
222 }
223
224 if let Some(icon) = icon {
226 tab_el = tab_el.child(div().text_sm().child(icon));
227 }
228
229 tab_el = tab_el.child(div().text_sm().child(label));
231
232 if let Some(badge) = badge {
234 tab_el = tab_el.child(
235 div()
236 .text_xs()
237 .px_1()
238 .py(px(1.0))
239 .bg(rgb(0x555555))
240 .rounded(px(3.0))
241 .child(badge),
242 );
243 }
244
245 if closeable {
247 let id = tab_id.clone();
248 let mut close_btn = div()
249 .id(SharedString::from(format!("tab-close-{}", tab_id)))
250 .text_xs()
251 .text_color(rgb(0x888888))
252 .hover(|s| s.text_color(rgb(0xffffff)));
253
254 if let Some(handler_ptr) = on_close {
255 close_btn = close_btn.on_mouse_up(
256 MouseButton::Left,
257 move |_event, window, cx| unsafe {
258 (*handler_ptr)(&id, window, cx);
259 },
260 );
261 }
262
263 tab_el = tab_el.child(close_btn.child("×"));
264 }
265
266 container = container.child(tab_el);
267 }
268
269 container
270 }
271}
272
273impl Default for Tabs {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279impl IntoElement for Tabs {
280 type Element = Div;
281
282 fn into_element(self) -> Self::Element {
283 self.build()
284 }
285}