use gpui::prelude::*;
use gpui::*;
use crate::theme::{get_theme_or, Theme};
use super::focus_navigation::{with_focus_actions, EnabledCursorExt};
use super::selection::SelectionItem;
actions!(ccf_tab_bar, [SelectPreviousTab, SelectNextTab]);
pub fn register_keybindings(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("left", SelectPreviousTab, Some("CcfTabBar")),
KeyBinding::new("right", SelectNextTab, Some("CcfTabBar")),
]);
}
#[derive(Debug, Clone)]
pub enum TabBarEvent<T> {
Change(T),
ContextMenu {
tab: T,
position: Point<Pixels>,
},
}
pub struct TabBar<T: SelectionItem> {
tabs: Vec<T>,
selected: T,
focus_handle: FocusHandle,
custom_theme: Option<Theme>,
enabled: bool,
previous_focus: Option<FocusHandle>,
tab_row_padding: Pixels,
}
impl<T: SelectionItem> TabBar<T> {
pub fn new(tabs: Vec<T>, selected: T, cx: &mut Context<Self>) -> Self {
Self {
tabs,
selected,
focus_handle: cx.focus_handle().tab_stop(true),
custom_theme: None,
enabled: true,
previous_focus: None,
tab_row_padding: px(0.0),
}
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.custom_theme = Some(theme);
self
}
#[must_use]
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub fn tab_row_padding(mut self, padding: Pixels) -> Self {
self.tab_row_padding = padding;
self
}
pub fn selected(&self) -> &T {
&self.selected
}
pub fn selected_index(&self) -> usize {
self.tabs.iter().position(|t| *t == self.selected).unwrap_or(0)
}
pub fn set_selected(&mut self, tab: T, cx: &mut Context<Self>) {
self.selected = tab;
cx.notify();
}
pub fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
if let Some(tab) = self.tabs.get(index).cloned() {
self.selected = tab;
cx.notify();
}
}
pub fn focus_handle(&self) -> &FocusHandle {
&self.focus_handle
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
if self.enabled != enabled {
self.enabled = enabled;
cx.notify();
}
}
fn select_previous(&mut self, cx: &mut Context<Self>) {
if self.tabs.is_empty() {
return;
}
let current_index = self.tabs.iter().position(|t| *t == self.selected).unwrap_or(0);
let new_index = if current_index == 0 {
self.tabs.len() - 1
} else {
current_index - 1
};
if let Some(tab) = self.tabs.get(new_index) {
self.selected = tab.clone();
cx.emit(TabBarEvent::Change(self.selected.clone()));
cx.notify();
}
}
fn select_next(&mut self, cx: &mut Context<Self>) {
if self.tabs.is_empty() {
return;
}
let current_index = self.tabs.iter().position(|t| *t == self.selected).unwrap_or(0);
let new_index = if current_index >= self.tabs.len() - 1 {
0
} else {
current_index + 1
};
if let Some(tab) = self.tabs.get(new_index) {
self.selected = tab.clone();
cx.emit(TabBarEvent::Change(self.selected.clone()));
cx.notify();
}
}
}
impl<T: SelectionItem> EventEmitter<TabBarEvent<T>> for TabBar<T> {}
impl<T: SelectionItem> Focusable for TabBar<T> {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl<T: SelectionItem> Render for TabBar<T> {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let theme = get_theme_or(cx, self.custom_theme.as_ref());
let selected_tab = self.selected.clone();
let is_focused = self.focus_handle.is_focused(window);
let enabled = self.enabled;
with_focus_actions(
div()
.id("ccf_tab_bar")
.key_context("CcfTabBar")
.track_focus(&self.focus_handle)
.tab_stop(enabled),
cx,
)
.flex()
.flex_row()
.when(enabled, |d| d.bg(rgb(theme.bg_secondary)))
.when(!enabled, |d| d.bg(rgb(theme.disabled_bg)))
.on_action(cx.listener(|this, _: &SelectPreviousTab, _window, cx| {
if this.enabled {
this.select_previous(cx);
}
}))
.on_action(cx.listener(|this, _: &SelectNextTab, _window, cx| {
if this.enabled {
this.select_next(cx);
}
}))
.when(self.tab_row_padding > px(0.0), |d| {
d.child(
div()
.w(self.tab_row_padding)
.when(enabled, |d| {
d.bg(rgb(theme.bg_secondary))
.border_b_1()
.border_color(rgb(theme.border_default))
})
.when(!enabled, |d| {
d.bg(rgb(theme.disabled_bg))
.border_b_1()
.border_color(rgb(theme.disabled_bg))
})
)
})
.children(self.tabs.iter().map(|tab| {
let tab = tab.clone();
let is_selected = tab == selected_tab;
let show_focus = is_selected && is_focused && enabled;
div()
.id(tab.id())
.cursor_for_enabled(enabled)
.when(enabled, |d| {
let tab_clone = tab.clone();
d.on_mouse_down(MouseButton::Left, {
cx.listener(move |this, _event: &MouseDownEvent, window, cx| {
this.previous_focus = window.focused(cx);
cx.notify();
})
})
.on_click({
let tab = tab.clone();
cx.listener(move |this, _event: &ClickEvent, window, cx| {
this.selected = tab.clone();
cx.emit(TabBarEvent::Change(tab.clone()));
if let Some(focus_handle) = this.previous_focus.take() {
focus_handle.focus(window);
} else {
window.blur();
}
cx.notify();
})
})
.on_mouse_down(MouseButton::Right, {
cx.listener(move |_this, event: &MouseDownEvent, _window, cx| {
cx.emit(TabBarEvent::ContextMenu {
tab: tab_clone.clone(),
position: event.position,
});
})
})
})
.child(
div()
.px_4()
.pb_2()
.when(is_selected, |d| {
d.pt_2() .border_t_2()
})
.when(!is_selected, |d| {
d.pt(px(10.0)) .border_r_1()
.border_b_1()
})
.when(is_selected && enabled, |d| {
d.bg(rgb(theme.bg_primary))
.text_color(rgb(theme.text_primary))
.border_color(rgb(theme.border_focus)) })
.when(is_selected && !enabled, |d| {
d.bg(rgb(theme.disabled_bg))
.text_color(rgb(theme.disabled_text))
.border_color(rgb(theme.disabled_bg))
})
.when(!is_selected && enabled, |d| {
d.bg(rgb(theme.bg_input))
.text_color(rgb(theme.text_dimmed))
.border_color(rgb(theme.border_default))
.hover(|d| {
d.bg(rgb(theme.bg_tab_hover))
.text_color(rgb(theme.text_muted))
})
})
.when(!is_selected && !enabled, |d| {
d.bg(rgb(theme.disabled_bg))
.text_color(rgb(theme.disabled_text))
.border_color(rgb(theme.disabled_bg))
})
.child(
div()
.px_1()
.border_1()
.rounded_sm()
.when(show_focus, |d| d.border_color(rgb(theme.border_focus)))
.when(!show_focus, |d| d.border_color(rgba(0x00000000)))
.child(tab.label())
)
)
}))
.child(
div()
.flex_1()
.when(enabled, |d| {
d.bg(rgb(theme.bg_secondary))
.border_b_1()
.border_color(rgb(theme.border_default))
})
.when(!enabled, |d| {
d.bg(rgb(theme.disabled_bg))
.border_b_1()
.border_color(rgb(theme.disabled_bg))
})
)
.when(self.tab_row_padding > px(0.0), |d| {
d.child(
div()
.w(self.tab_row_padding)
.when(enabled, |d| {
d.bg(rgb(theme.bg_secondary))
.border_b_1()
.border_color(rgb(theme.border_default))
})
.when(!enabled, |d| {
d.bg(rgb(theme.disabled_bg))
.border_b_1()
.border_color(rgb(theme.disabled_bg))
})
)
})
}
}