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)]
16pub struct TabBar {
17 base: Stateful<Div>,
18 style: StyleRefinement,
19 scroll_handle: Option<ScrollHandle>,
20 prefix: Option<AnyElement>,
21 suffix: Option<AnyElement>,
22 children: SmallVec<[Tab; 2]>,
23 last_empty_space: AnyElement,
24 selected_index: Option<usize>,
25 variant: TabVariant,
26 size: Size,
27 menu: bool,
28 on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
29 tab_item_top_offset: Pixels,
31}
32
33impl TabBar {
34 pub fn new(id: impl Into<ElementId>) -> Self {
36 Self {
37 base: div().id(id).px(px(-1.)),
38 style: StyleRefinement::default(),
39 children: SmallVec::new(),
40 scroll_handle: None,
41 prefix: None,
42 suffix: None,
43 variant: TabVariant::default(),
44 size: Size::default(),
45 last_empty_space: div().w_3().into_any_element(),
46 selected_index: None,
47 on_click: None,
48 menu: false,
49 tab_item_top_offset: px(0.),
50 }
51 }
52
53 pub fn with_variant(mut self, variant: TabVariant) -> Self {
55 self.variant = variant;
56 self
57 }
58
59 pub fn pill(mut self) -> Self {
61 self.variant = TabVariant::Pill;
62 self
63 }
64
65 pub fn outline(mut self) -> Self {
67 self.variant = TabVariant::Outline;
68 self
69 }
70
71 pub fn segmented(mut self) -> Self {
73 self.variant = TabVariant::Segmented;
74 self
75 }
76
77 pub fn underline(mut self) -> Self {
79 self.variant = TabVariant::Underline;
80 self
81 }
82
83 pub fn menu(mut self, menu: bool) -> Self {
85 self.menu = menu;
86 self
87 }
88
89 pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
91 self.scroll_handle = Some(scroll_handle.clone());
92 self
93 }
94
95 pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
97 self.prefix = Some(prefix.into_any_element());
98 self
99 }
100
101 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
103 self.suffix = Some(suffix.into_any_element());
104 self
105 }
106
107 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(true);
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}