use gpui::prelude::*;
use gpui::{div, px, Context, EventEmitter, IntoElement, SharedString, Window};
use crate::theme::{theme, Size};
use crate::{ActionIcon, CloseButton};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TabBarEvent {
Select(usize),
Close(usize),
Add,
}
fn active_after_remove(active: usize, removed: usize, new_len: usize) -> usize {
if new_len == 0 {
return 0;
}
let shifted = if removed < active { active - 1 } else { active };
shifted.min(new_len - 1)
}
pub struct TabBar {
tabs: Vec<SharedString>,
active: usize,
hovered: Option<usize>,
with_add_button: bool,
}
impl EventEmitter<TabBarEvent> for TabBar {}
impl TabBar {
pub fn new(_cx: &mut Context<Self>) -> Self {
TabBar {
tabs: Vec::new(),
active: 0,
hovered: None,
with_add_button: true,
}
}
pub fn tabs<I, S>(mut self, tabs: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<SharedString>,
{
self.tabs = tabs.into_iter().map(Into::into).collect();
self
}
pub fn active(mut self, index: usize) -> Self {
self.active = index;
self
}
pub fn with_add_button(mut self, show: bool) -> Self {
self.with_add_button = show;
self
}
pub fn active_index(&self) -> usize {
self.active
}
pub fn len(&self) -> usize {
self.tabs.len()
}
pub fn is_empty(&self) -> bool {
self.tabs.is_empty()
}
pub fn add_tab(&mut self, label: impl Into<SharedString>, cx: &mut Context<Self>) {
self.tabs.push(label.into());
self.active = self.tabs.len() - 1;
cx.notify();
}
pub fn remove_tab(&mut self, index: usize, cx: &mut Context<Self>) {
if index >= self.tabs.len() {
return;
}
self.tabs.remove(index);
self.active = active_after_remove(self.active, index, self.tabs.len());
self.hovered = None;
cx.notify();
}
pub fn set_tabs(&mut self, tabs: Vec<SharedString>, cx: &mut Context<Self>) {
self.tabs = tabs;
self.active = self.active.min(self.tabs.len().saturating_sub(1));
self.hovered = None;
cx.notify();
}
pub fn set_active(&mut self, index: usize, cx: &mut Context<Self>) {
let clamped = index.min(self.tabs.len().saturating_sub(1));
if self.active != clamped {
self.active = clamped;
cx.notify();
}
}
}
impl Render for TabBar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let surface = t.surface().hsla();
let strip_bg = t.surface_hover().hsla();
let border = t.border().hsla();
let text = t.text().hsla();
let dimmed = t.dimmed().hsla();
let font = t.font_size(Size::Sm);
let count = self.tabs.len();
let active = if count == 0 {
0
} else {
self.active.min(count - 1)
};
let hovered = self.hovered;
let mut strip = div()
.id("guise-tabbar-strip")
.flex_1()
.min_w(px(0.0))
.flex()
.overflow_x_scroll();
for (i, label) in self.tabs.iter().enumerate() {
let is_active = i == active;
let show_close = is_active || hovered == Some(i);
let mut close_slot = div().flex_none();
if !show_close {
close_slot = close_slot.invisible();
}
close_slot = close_slot.child(
CloseButton::new(("guise-tabbar-close", i))
.size(Size::Xs)
.on_click(cx.listener(move |_this, _ev, _window, cx| {
cx.stop_propagation();
cx.emit(TabBarEvent::Close(i));
})),
);
let mut tab = div()
.id(("guise-tabbar-tab", i))
.flex_none()
.flex()
.items_center()
.gap(px(6.0))
.pl(px(12.0))
.pr(px(6.0))
.py(px(6.0))
.border_r_1()
.border_color(border)
.text_size(px(font))
.text_color(if is_active { text } else { dimmed })
.child(label.clone())
.child(close_slot)
.on_hover(cx.listener(move |this, entered: &bool, _window, cx| {
if *entered {
this.hovered = Some(i);
} else if this.hovered == Some(i) {
this.hovered = None;
}
cx.notify();
}))
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.active = i;
cx.emit(TabBarEvent::Select(i));
cx.notify();
}));
if is_active {
tab = tab.bg(surface);
} else {
tab = tab.hover(move |s| s.text_color(text));
}
strip = strip.child(tab);
}
let mut bar = div()
.flex()
.items_center()
.w_full()
.bg(strip_bg)
.border_b_1()
.border_color(border)
.child(strip);
if self.with_add_button {
bar = bar.child(
div().flex_none().px(px(4.0)).child(
ActionIcon::new("guise-tabbar-add", "+")
.size(Size::Sm)
.on_click(cx.listener(|_this, _ev, _window, cx| cx.emit(TabBarEvent::Add))),
),
);
}
bar
}
}
#[cfg(test)]
mod tests {
use super::active_after_remove;
#[test]
fn removing_before_active_shifts_it_left() {
assert_eq!(active_after_remove(2, 0, 3), 1);
assert_eq!(active_after_remove(3, 2, 3), 2);
}
#[test]
fn removing_the_active_tab_keeps_its_slot_clamped() {
assert_eq!(active_after_remove(1, 1, 2), 1);
assert_eq!(active_after_remove(2, 2, 2), 1);
}
#[test]
fn removing_after_active_leaves_it_alone() {
assert_eq!(active_after_remove(0, 2, 2), 0);
assert_eq!(active_after_remove(1, 3, 3), 1);
}
#[test]
fn emptying_the_bar_resets_to_zero() {
assert_eq!(active_after_remove(0, 0, 0), 0);
}
}