1use std::{cell::RefCell, rc::Rc, time::Duration};
2
3use rgpui::{
4 Anchor, Animation, AnimationExt as _, AnyElement, App, Bounds, Div, Edges, ElementId,
5 InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, ScrollHandle, SharedString,
6 Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, div,
7 prelude::FluentBuilder as _, px,
8};
9use rust_i18n::t;
10use smallvec::SmallVec;
11
12use super::{Tab, TabVariant};
13use crate::animation::{Lerp, ease_in_out_cubic};
14use crate::button::{Button, ButtonVariants as _};
15use crate::menu::{DropdownMenu as _, PopupMenuItem};
16use crate::{
17 ActiveTheme, ElementExt, Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex,
18};
19
20struct TabIndicatorBounds {
21 container: Bounds<Pixels>,
22 tabs: Vec<Bounds<Pixels>>,
23}
24
25impl TabIndicatorBounds {
26 fn new(num_tabs: usize) -> Self {
27 Self {
28 container: Bounds::default(),
29 tabs: vec![Bounds::default(); num_tabs],
30 }
31 }
32
33 fn resize(&mut self, num_tabs: usize) {
34 self.tabs.resize(num_tabs, Bounds::default());
35 }
36}
37
38#[derive(IntoElement)]
40pub struct TabBar {
41 id: ElementId,
42 base: Stateful<Div>,
43 style: StyleRefinement,
44 scroll_handle: Option<ScrollHandle>,
45 prefix: Option<AnyElement>,
46 suffix: Option<AnyElement>,
47 children: SmallVec<[Tab; 2]>,
48 last_empty_space: AnyElement,
49 selected_index: Option<usize>,
50 variant: TabVariant,
51 size: Size,
52 menu: bool,
53 on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
54}
55
56impl TabBar {
57 pub fn new(id: impl Into<ElementId>) -> Self {
59 let id = id.into();
60 Self {
61 id: id.clone(),
62 base: div().id(id).px(px(-1.)),
63 style: StyleRefinement::default(),
64 children: SmallVec::new(),
65 scroll_handle: None,
66 prefix: None,
67 suffix: None,
68 variant: TabVariant::default(),
69 size: Size::default(),
70 last_empty_space: div().w_3().into_any_element(),
71 selected_index: None,
72 on_click: None,
73 menu: false,
74 }
75 }
76
77 pub fn with_variant(mut self, variant: TabVariant) -> Self {
79 self.variant = variant;
80 self
81 }
82
83 pub fn pill(mut self) -> Self {
85 self.variant = TabVariant::Pill;
86 self
87 }
88
89 pub fn outline(mut self) -> Self {
91 self.variant = TabVariant::Outline;
92 self
93 }
94
95 pub fn segmented(mut self) -> Self {
97 self.variant = TabVariant::Segmented;
98 self
99 }
100
101 pub fn underline(mut self) -> Self {
103 self.variant = TabVariant::Underline;
104 self
105 }
106
107 pub fn menu(mut self, menu: bool) -> Self {
109 self.menu = menu;
110 self
111 }
112
113 pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
115 self.scroll_handle = Some(scroll_handle.clone());
116 self
117 }
118
119 pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
121 self.prefix = Some(prefix.into_any_element());
122 self
123 }
124
125 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
127 self.suffix = Some(suffix.into_any_element());
128 self
129 }
130
131 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
133 self.children.extend(children.into_iter().map(Into::into));
134 self
135 }
136
137 pub fn child(mut self, child: impl Into<Tab>) -> Self {
139 self.children.push(child.into());
140 self
141 }
142
143 pub fn selected_index(mut self, index: usize) -> Self {
145 self.selected_index = Some(index);
146 self
147 }
148
149 pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
151 self.last_empty_space = last_empty_space.into_any_element();
152 self
153 }
154
155 pub fn on_click<F>(mut self, on_click: F) -> Self
159 where
160 F: Fn(&usize, &mut Window, &mut App) + 'static,
161 {
162 self.on_click = Some(Rc::new(on_click));
163 self
164 }
165
166 fn render_indicator(
168 &self,
169 bounds_rc: &Option<Rc<RefCell<TabIndicatorBounds>>>,
170 window: &mut Window,
171 cx: &mut App,
172 ) -> Option<AnyElement> {
173 let has_indicator = matches!(
174 self.variant,
175 TabVariant::Segmented | TabVariant::Pill | TabVariant::Underline
176 );
177 let num_tabs = self.children.len();
178 let selected_ix = self.selected_index.unwrap_or(usize::MAX);
179
180 if !(has_indicator && num_tabs > 0 && selected_ix < num_tabs) {
181 return None;
182 }
183
184 let prev_key = format!("{}-tab-prev", self.id);
185 let anim_key = format!("{}-tab-anim", self.id);
186 let init_key = format!("{}-tab-init", self.id);
187
188 let prev_selected = window.use_keyed_state(prev_key, cx, |_, _| selected_ix);
189 let anim_params =
191 window.use_keyed_state(anim_key, cx, |_, _| (px(0.), px(0.), px(0.), px(0.), 0u64));
192 let initialized = window.use_keyed_state(init_key, cx, |_, _| false);
193
194 if !*initialized.read(cx) {
196 initialized.update(cx, |v, _| *v = true);
197 }
198
199 self.update_anim_params(selected_ix, bounds_rc, &prev_selected, &anim_params, cx);
200
201 let (from_left, from_width, to_left, to_width, epoch) = *anim_params.read(cx);
202 if to_width <= px(0.) {
203 return None;
204 }
205
206 let variant = self.variant;
207 let size = self.size;
208 let inner_height = variant.inner_height(size);
209 let inner_radius = variant.inner_radius(size, cx);
210
211 let indicator = div()
212 .absolute()
213 .top_0()
214 .bottom_0()
215 .map(|el| match variant {
216 TabVariant::Segmented => el.flex().items_center().child(
217 div()
218 .w_full()
219 .h(inner_height)
220 .bg(cx.theme().background)
221 .rounded(inner_radius)
222 .shadow_xs(),
223 ),
224 TabVariant::Pill => el
225 .flex()
226 .items_center()
227 .child(div().size_full().bg(cx.theme().primary).rounded(px(99.))),
228 TabVariant::Underline => el.child(
229 div()
230 .absolute()
231 .left_0()
232 .right_0()
233 .bottom_0()
234 .h(px(2.))
235 .bg(cx.theme().primary),
236 ),
237 _ => el,
238 })
239 .with_animation(
240 ElementId::NamedInteger("tab-ind".into(), epoch),
241 Animation::new(Duration::from_millis(200)).with_easing(ease_in_out_cubic),
242 move |el, delta| {
243 let left = Lerp::lerp(&from_left, &to_left, delta);
244 let width = Lerp::lerp(&from_width, &to_width, delta);
245 el.left(left).w(width)
246 },
247 );
248
249 Some(indicator.into_any_element())
250 }
251
252 fn update_anim_params(
254 &self,
255 selected_ix: usize,
256 bounds_rc: &Option<Rc<RefCell<TabIndicatorBounds>>>,
257 prev_selected: &rgpui::Entity<usize>,
258 anim_params: &rgpui::Entity<(Pixels, Pixels, Pixels, Pixels, u64)>,
259 cx: &mut App,
260 ) {
261 let rc = match bounds_rc {
262 Some(rc) => rc,
263 None => return,
264 };
265
266 let prev_ix = *prev_selected.read(cx);
267 let bounds = rc.borrow();
268 let container = bounds.container;
269
270 if container.size.width == px(0.) {
271 if prev_ix != selected_ix {
272 prev_selected.update(cx, |v, _| *v = selected_ix);
273 }
274 return;
275 }
276
277 if prev_ix != selected_ix {
278 let from_b = bounds.tabs.get(prev_ix);
279 let to_b = bounds.tabs.get(selected_ix);
280 match (from_b, to_b) {
281 (Some(from_b), Some(to_b)) => {
282 let from_left = from_b.origin.x - container.origin.x;
283 let from_width = from_b.size.width;
284 let to_left = to_b.origin.x - container.origin.x;
285 let to_width = to_b.size.width;
286 let epoch = anim_params.read(cx).4 + 1;
287 anim_params.update(cx, |v, _| {
288 *v = (from_left, from_width, to_left, to_width, epoch)
289 });
290 }
291 (None, Some(to_b)) => {
292 let left = to_b.origin.x - container.origin.x;
293 let width = to_b.size.width;
294 anim_params.update(cx, |v, _| *v = (left, width, left, width, v.4));
295 }
296 _ => {}
297 }
298 drop(bounds);
299 prev_selected.update(cx, |v, _| *v = selected_ix);
300 return;
301 }
302
303 if let Some(to_b) = bounds.tabs.get(selected_ix) {
304 let left = to_b.origin.x - container.origin.x;
305 let width = to_b.size.width;
306 let (_, _, to_left, to_width, epoch) = *anim_params.read(cx);
307
308 if to_width == px(0.) {
309 anim_params.update(cx, |v, _| *v = (left, width, left, width, epoch));
310 return;
311 }
312
313 if left != to_left || width != to_width {
314 anim_params.update(cx, |v, _| *v = (left, width, left, width, epoch));
315 }
316 }
317 }
318}
319
320impl Styled for TabBar {
321 fn style(&mut self) -> &mut StyleRefinement {
322 &mut self.style
323 }
324}
325
326impl Sizable for TabBar {
327 fn with_size(mut self, size: impl Into<Size>) -> Self {
328 self.size = size.into();
329 self
330 }
331}
332
333impl RenderOnce for TabBar {
334 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
335 let default_gap = match self.size {
336 Size::Small | Size::XSmall => px(8.),
337 Size::Large => px(16.),
338 _ => px(12.),
339 };
340 let (bg, paddings, gap) = match self.variant {
341 TabVariant::Tab => {
342 let padding = Edges::all(px(0.));
343 (cx.theme().tab_bar, padding, px(0.))
344 }
345 TabVariant::Outline => {
346 let padding = Edges::all(px(0.));
347 (cx.theme().transparent, padding, default_gap)
348 }
349 TabVariant::Pill => {
350 let padding = Edges::all(px(0.));
351 (cx.theme().transparent, padding, px(4.))
352 }
353 TabVariant::Segmented => {
354 let padding_x = match self.size {
355 Size::XSmall => px(2.),
356 Size::Small => px(3.),
357 _ => px(4.),
358 };
359 let padding = Edges {
360 left: padding_x,
361 right: padding_x,
362 ..Default::default()
363 };
364
365 (cx.theme().tab_bar_segmented, padding, px(2.))
366 }
367 TabVariant::Underline => {
368 let gap = match self.size {
370 Size::XSmall => px(10.),
371 Size::Small => px(12.),
372 Size::Large => px(20.),
373 _ => px(16.),
374 };
375
376 (cx.theme().transparent, Edges::all(px(0.)), gap)
377 }
378 };
379
380 let has_indicator = matches!(
381 self.variant,
382 TabVariant::Segmented | TabVariant::Pill | TabVariant::Underline
383 );
384 let num_tabs = self.children.len();
385
386 let bounds_rc = if has_indicator && num_tabs > 0 {
389 let rc: Rc<RefCell<TabIndicatorBounds>> = window
390 .use_keyed_state(format!("{}-tab-bounds", self.id), cx, |_, _| {
391 Rc::new(RefCell::new(TabIndicatorBounds::new(num_tabs)))
392 })
393 .read(cx)
394 .clone();
395 rc.borrow_mut().resize(num_tabs);
396 Some(rc)
397 } else {
398 None
399 };
400
401 let indicator_element = self.render_indicator(&bounds_rc, window, cx);
402 let indicator_ready = indicator_element.is_some();
403
404 let has_suffix_or_menu = self.suffix.is_some() || self.menu;
405 let mut item_metas: Vec<(Option<SharedString>, Option<Icon>, bool)> = Vec::new();
406 let selected_index = self.selected_index;
407 let on_click = self.on_click.clone();
408
409 self.base
410 .group("tab-bar")
411 .relative()
412 .flex()
413 .items_center()
414 .bg(bg)
415 .text_color(cx.theme().tab_foreground)
416 .when(
417 self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
418 |this| {
419 this.child(
420 div()
421 .id("border-b")
422 .absolute()
423 .left_0()
424 .bottom_0()
425 .size_full()
426 .border_b_1()
427 .border_color(cx.theme().border),
428 )
429 },
430 )
431 .rounded(self.variant.tab_bar_radius(self.size, cx))
432 .paddings(paddings)
433 .refine_style(&self.style)
434 .when_some(self.prefix, |this, prefix| this.child(prefix))
435 .child(
436 h_flex().id("tabs").flex_1().overflow_x_hidden().child(
437 h_flex()
438 .id("tabs-inner")
439 .relative()
440 .gap(gap)
441 .overflow_x_scroll()
442 .when_some(self.scroll_handle, |this, scroll_handle| {
443 this.track_scroll(&scroll_handle)
444 })
445 .when_some(bounds_rc.clone(), |this, rc| {
446 this.on_prepaint(move |bounds, _, _| {
447 rc.borrow_mut().container = bounds;
448 })
449 })
450 .when_some(indicator_element, |this, ind| this.child(ind))
451 .children(self.children.into_iter().enumerate().map(|(ix, child)| {
452 item_metas.push((
453 child.label.clone(),
454 child.icon.clone(),
455 child.disabled,
456 ));
457 let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
458 let mut tab = child
459 .ix(ix)
460 .tab_bar_prefix(tab_bar_prefix)
461 .with_variant(self.variant)
462 .with_size(self.size);
463 tab.indicator_active = has_indicator;
464 tab.indicator_ready = indicator_ready;
465 let tab = tab
466 .when_some(self.selected_index, |this, selected_ix| {
467 this.selected(selected_ix == ix)
468 })
469 .when_some(self.on_click.clone(), move |this, on_click| {
470 this.on_click(move |_, window, cx| on_click(&ix, window, cx))
471 });
472
473 if let Some(ref rc) = bounds_rc {
474 let rc = rc.clone();
475 div()
476 .on_prepaint(move |bounds, _, _| {
477 if let Some(slot) = rc.borrow_mut().tabs.get_mut(ix) {
478 *slot = bounds;
479 }
480 })
481 .child(tab)
482 .into_any_element()
483 } else {
484 tab.into_any_element()
485 }
486 }))
487 .when(has_suffix_or_menu, |this| this.child(self.last_empty_space)),
488 ),
489 )
490 .when(self.menu, |this| {
491 this.child(
492 Button::new("more")
493 .xsmall()
494 .ghost()
495 .icon(IconName::ChevronDown)
496 .dropdown_menu(move |mut this, _, _| {
497 this = this.scrollable(true);
498 for (ix, (label, icon, disabled)) in item_metas.iter().enumerate() {
499 let base = if let Some(label) = label.clone() {
500 PopupMenuItem::new(label)
501 } else if let Some(icon) = icon.clone() {
502 PopupMenuItem::element(move |_, _| icon.clone())
503 } else {
504 PopupMenuItem::new(t!("Dock.Unnamed"))
505 };
506 this = this.item(
507 base.checked(selected_index == Some(ix))
508 .disabled(*disabled)
509 .when_some(on_click.clone(), |this, on_click| {
510 this.on_click(move |_, window, cx| {
511 on_click(&ix, window, cx)
512 })
513 }),
514 );
515 }
516
517 this
518 })
519 .anchor(Anchor::TopRight),
520 )
521 })
522 .when_some(self.suffix, |this, suffix| this.child(suffix))
523 }
524}