1use gpui::{
2 div, prelude::FluentBuilder as _, px, AnyElement, App, Corner, Div, Edges, ElementId,
3 InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, ScrollHandle, Stateful,
4 StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
5};
6use smallvec::SmallVec;
7use std::rc::Rc;
8
9use super::{Tab, TabVariant};
10use crate::button::{Button, ButtonVariants as _};
11use crate::menu::{DropdownMenu as _, PopupMenuItem};
12use crate::{h_flex, ActiveTheme, IconName, Selectable, Sizable, Size, StyledExt};
13
14#[derive(IntoElement)]
15pub struct TabBar {
16 base: Stateful<Div>,
17 style: StyleRefinement,
18 scroll_handle: Option<ScrollHandle>,
19 prefix: Option<AnyElement>,
20 suffix: Option<AnyElement>,
21 children: SmallVec<[Tab; 2]>,
22 last_empty_space: AnyElement,
23 selected_index: Option<usize>,
24 variant: TabVariant,
25 size: Size,
26 menu: bool,
27 on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
28 tab_item_top_offset: Pixels,
30}
31
32impl TabBar {
33 pub fn new(id: impl Into<ElementId>) -> Self {
35 Self {
36 base: div().id(id).px(px(-1.)),
37 style: StyleRefinement::default(),
38 children: SmallVec::new(),
39 scroll_handle: None,
40 prefix: None,
41 suffix: None,
42 variant: TabVariant::default(),
43 size: Size::default(),
44 last_empty_space: div().w_3().into_any_element(),
45 selected_index: None,
46 on_click: None,
47 menu: false,
48 tab_item_top_offset: px(0.),
49 }
50 }
51
52 pub fn with_variant(mut self, variant: TabVariant) -> Self {
54 self.variant = variant;
55 self
56 }
57
58 pub fn pill(mut self) -> Self {
60 self.variant = TabVariant::Pill;
61 self
62 }
63
64 pub fn outline(mut self) -> Self {
66 self.variant = TabVariant::Outline;
67 self
68 }
69
70 pub fn segmented(mut self) -> Self {
72 self.variant = TabVariant::Segmented;
73 self
74 }
75
76 pub fn underline(mut self) -> Self {
78 self.variant = TabVariant::Underline;
79 self
80 }
81
82 pub fn with_menu(mut self, menu: bool) -> Self {
84 self.menu = menu;
85 self
86 }
87
88 pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
90 self.scroll_handle = Some(scroll_handle.clone());
91 self
92 }
93
94 pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
96 self.prefix = Some(prefix.into_any_element());
97 self
98 }
99
100 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
102 self.suffix = Some(suffix.into_any_element());
103 self
104 }
105
106 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
109 self.children.extend(children.into_iter().map(Into::into));
110 self
111 }
112
113 pub fn child(mut self, child: impl Into<Tab>) -> Self {
115 self.children.push(child.into());
116 self
117 }
118
119 pub fn selected_index(mut self, index: usize) -> Self {
121 self.selected_index = Some(index);
122 self
123 }
124
125 pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
127 self.last_empty_space = last_empty_space.into_any_element();
128 self
129 }
130
131 pub fn on_click<F>(mut self, on_click: F) -> Self
135 where
136 F: Fn(&usize, &mut Window, &mut App) + 'static,
137 {
138 self.on_click = Some(Rc::new(on_click));
139 self
140 }
141
142 pub(crate) fn tab_item_top_offset(mut self, offset: impl Into<Pixels>) -> Self {
143 self.tab_item_top_offset = offset.into();
144 self
145 }
146}
147
148impl Styled for TabBar {
149 fn style(&mut self) -> &mut StyleRefinement {
150 &mut self.style
151 }
152}
153
154impl Sizable for TabBar {
155 fn with_size(mut self, size: impl Into<Size>) -> Self {
156 self.size = size.into();
157 self
158 }
159}
160
161impl RenderOnce for TabBar {
162 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
163 let default_gap = match self.size {
164 Size::Small | Size::XSmall => px(8.),
165 Size::Large => px(16.),
166 _ => px(12.),
167 };
168 let (bg, paddings, gap) = match self.variant {
169 TabVariant::Tab => {
170 let padding = Edges::all(px(0.));
171 (cx.theme().tab_bar, padding, px(0.))
172 }
173 TabVariant::Outline => {
174 let padding = Edges::all(px(0.));
175 (cx.theme().transparent, padding, default_gap)
176 }
177 TabVariant::Pill => {
178 let padding = Edges::all(px(0.));
179 (cx.theme().transparent, padding, px(4.))
180 }
181 TabVariant::Segmented => {
182 let padding_x = match self.size {
183 Size::XSmall => px(2.),
184 Size::Small => px(2.),
185 Size::Large => px(6.),
186 _ => px(5.),
187 };
188 let padding = Edges {
189 left: padding_x,
190 right: padding_x,
191 ..Default::default()
192 };
193
194 (cx.theme().tab_bar_segmented, padding, px(2.))
195 }
196 TabVariant::Underline => {
197 let gap = match self.size {
199 Size::XSmall => px(10.),
200 Size::Small => px(12.),
201 Size::Large => px(20.),
202 _ => px(16.),
203 };
204
205 (cx.theme().transparent, Edges::all(px(0.)), gap)
206 }
207 };
208
209 let mut item_labels = Vec::new();
210 let selected_index = self.selected_index;
211 let on_click = self.on_click.clone();
212
213 self.base
214 .group("tab-bar")
215 .relative()
216 .flex()
217 .items_center()
218 .bg(bg)
219 .text_color(cx.theme().tab_foreground)
220 .when(
221 self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
222 |this| {
223 this.child(
224 div()
225 .id("border-b")
226 .absolute()
227 .left_0()
228 .bottom_0()
229 .size_full()
230 .border_b_1()
231 .border_color(cx.theme().border),
232 )
233 },
234 )
235 .when(
236 self.variant == TabVariant::Pill || self.variant == TabVariant::Segmented,
237 |this| this.rounded(cx.theme().radius),
238 )
239 .paddings(paddings)
240 .refine_style(&self.style)
241 .when_some(self.prefix, |this, prefix| this.child(prefix))
242 .child(
243 h_flex()
244 .id("tabs")
245 .flex_1()
246 .overflow_x_scroll()
247 .when_some(self.scroll_handle, |this, scroll_handle| {
248 this.track_scroll(&scroll_handle)
249 })
250 .gap(gap)
251 .children(self.children.into_iter().enumerate().map(|(ix, child)| {
252 item_labels.push((child.label.clone(), child.disabled));
253 child
254 .id(ix)
255 .mt(self.tab_item_top_offset)
256 .with_variant(self.variant)
257 .with_size(self.size)
258 .when_some(self.selected_index, |this, selected_ix| {
259 this.selected(selected_ix == ix)
260 })
261 .when_some(self.on_click.clone(), move |this, on_click| {
262 this.on_click(move |_, window, cx| on_click(&ix, window, cx))
263 })
264 }))
265 .when(self.suffix.is_some() || self.menu, |this| {
266 this.child(self.last_empty_space)
267 }),
268 )
269 .when(self.menu, |this| {
270 this.child(
271 Button::new("more")
272 .xsmall()
273 .ghost()
274 .icon(IconName::ChevronDown)
275 .dropdown_menu(move |mut this, _, _| {
276 this = this.scrollable();
277 for (ix, (label, disabled)) in item_labels.iter().enumerate() {
278 this = this.item(
279 PopupMenuItem::new(label.clone().unwrap_or_default())
280 .checked(selected_index == Some(ix))
281 .disabled(*disabled)
282 .when_some(on_click.clone(), |this, on_click| {
283 this.on_click(move |_, window, cx| {
284 on_click(&ix, window, cx)
285 })
286 }),
287 )
288 }
289
290 this
291 })
292 .anchor(Corner::TopRight),
293 )
294 })
295 .when_some(self.suffix, |this, suffix| this.child(suffix))
296 }
297}