mod context_menu;
mod drag_drop;
mod horizontal;
mod profile_menu;
mod state;
mod tab_painter;
mod tab_rendering;
mod title_utils;
pub use state::TabBarUI;
use crate::config::{Config, TabBarMode, TabBarPosition};
use crate::tab::{TabId, TabManager};
use crate::ui_constants::{TAB_DRAW_SHRINK_Y, TAB_SPACING};
use tab_rendering::TabRenderParams;
pub(super) const CHEVRON_RESERVED: f32 = 28.0;
#[derive(Debug, Clone, PartialEq)]
pub enum TabBarAction {
None,
SwitchTo(TabId),
Close(TabId),
NewTab,
NewTabWithProfile(crate::profile::ProfileId),
Reorder(TabId, usize),
SetColor(TabId, [u8; 3]),
ClearColor(TabId),
Duplicate(TabId),
RenameTab(TabId, String),
SetTabIcon(TabId, Option<String>),
ToggleAssistantPanel,
}
impl TabBarUI {
pub fn should_show(&self, tab_count: usize, mode: TabBarMode) -> bool {
match mode {
TabBarMode::Always => true,
TabBarMode::WhenMultiple => tab_count > 1,
TabBarMode::Never => false,
}
}
pub fn is_dragging(&self) -> bool {
self.drag_in_progress
}
pub fn render(
&mut self,
ctx: &egui::Context,
tabs: &TabManager,
config: &Config,
profiles: &crate::profile::ProfileManager,
right_reserved_width: f32,
) -> TabBarAction {
let tab_count = tabs.tab_count();
if !self.should_show(tab_count, config.tab_bar_mode) {
return TabBarAction::None;
}
match config.tab_bar_position {
TabBarPosition::Left => self.render_vertical(ctx, tabs, config, profiles),
_ => self.render_horizontal(ctx, tabs, config, profiles, right_reserved_width),
}
}
fn render_vertical(
&mut self,
ctx: &egui::Context,
tabs: &TabManager,
config: &Config,
profiles: &crate::profile::ProfileManager,
) -> TabBarAction {
let tab_count = tabs.tab_count();
self.tab_rects.clear();
let mut action = TabBarAction::None;
let active_tab_id = tabs.active_tab_id();
let bar_bg = config.tab_bar_background;
let tab_spacing = TAB_SPACING;
let tab_height = config.tab_bar_height;
egui::SidePanel::left("tab_bar")
.exact_width(config.tab_bar_width)
.frame(egui::Frame::NONE.fill(egui::Color32::from_rgb(bar_bg[0], bar_bg[1], bar_bg[2])))
.show(ctx, |ui| {
egui::ScrollArea::vertical()
.scroll_bar_visibility(
egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded,
)
.show(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = egui::vec2(0.0, tab_spacing);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
let show_chevron_v = !profiles.is_empty()
|| config.ai_inspector.ai_inspector_enabled;
let chevron_space = if show_chevron_v {
CHEVRON_RESERVED
} else {
0.0
};
let plus_btn = ui.add(
egui::Button::new("+")
.min_size(egui::vec2(
ui.available_width() - chevron_space,
tab_height - TAB_DRAW_SHRINK_Y * 2.0,
))
.fill(egui::Color32::TRANSPARENT),
);
if plus_btn.clicked_by(egui::PointerButton::Primary) {
action = TabBarAction::NewTab;
}
if plus_btn.hovered() {
#[cfg(target_os = "macos")]
plus_btn.on_hover_text("New Tab (Cmd+T)");
#[cfg(not(target_os = "macos"))]
plus_btn.on_hover_text("New Tab (Ctrl+Shift+T)");
}
if show_chevron_v {
let chevron_btn = ui.add(
egui::Button::new("⏷")
.min_size(egui::vec2(
CHEVRON_RESERVED / 2.0,
tab_height - TAB_DRAW_SHRINK_Y * 2.0,
))
.fill(egui::Color32::TRANSPARENT),
);
if chevron_btn.clicked_by(egui::PointerButton::Primary) {
self.show_new_tab_profile_menu =
!self.show_new_tab_profile_menu;
}
if chevron_btn.hovered() {
chevron_btn.on_hover_text("New tab from profile");
}
}
});
for (index, tab) in tabs.tabs().iter().enumerate() {
let is_active = Some(tab.id) == active_tab_id;
let is_bell_active = tab.is_bell_active();
let (tab_action, tab_rect) = self.render_vertical_tab(
ui,
TabRenderParams {
id: tab.id,
index,
title: &tab.title,
profile_icon: tab
.custom_icon
.as_deref()
.or(tab.profile.profile_icon.as_deref()),
custom_icon: tab.custom_icon.as_deref(),
is_active,
has_activity: tab.activity.has_activity,
is_bell_active,
custom_color: tab.custom_color,
config,
tab_size: tab_height,
tab_count,
},
);
self.tab_rects.push((tab.id, tab_rect));
if tab_action != TabBarAction::None {
action = tab_action;
}
}
});
});
if self.drag_in_progress {
let drag_action = self.render_vertical_drag_feedback(ui, config);
if drag_action != TabBarAction::None {
action = drag_action;
}
}
});
if self.drag_in_progress && self.dragging_tab.is_some() {
self.render_ghost_tab(ctx, config);
}
if let Some(context_tab_id) = self.context_menu_tab {
let menu_action = self.render_context_menu(ctx, context_tab_id);
if menu_action != TabBarAction::None {
action = menu_action;
}
}
let menu_action = self.render_new_tab_profile_menu(ctx, profiles, config);
if menu_action != TabBarAction::None {
action = menu_action;
}
action
}
pub fn get_height(&self, tab_count: usize, config: &Config) -> f32 {
if self.should_show(tab_count, config.tab_bar_mode)
&& config.tab_bar_position.is_horizontal()
{
config.tab_bar_height
} else {
0.0
}
}
pub fn get_width(&self, tab_count: usize, config: &Config) -> f32 {
if self.should_show(tab_count, config.tab_bar_mode)
&& config.tab_bar_position == TabBarPosition::Left
{
config.tab_bar_width
} else {
0.0
}
}
pub fn is_context_menu_open(&self) -> bool {
self.context_menu_tab.is_some()
}
pub fn is_renaming(&self) -> bool {
self.renaming_tab && self.context_menu_tab.is_some()
}
pub fn calculate_drop_target_horizontal(
tab_rects: &[(TabId, egui::Rect)],
drag_source_index: Option<usize>,
pointer_x: f32,
) -> Option<usize> {
let mut insert_index = tab_rects.len();
for (i, (_id, rect)) in tab_rects.iter().enumerate() {
if pointer_x < rect.center().x {
insert_index = i;
break;
}
}
let is_noop =
drag_source_index.is_some_and(|src| insert_index == src || insert_index == src + 1);
if is_noop { None } else { Some(insert_index) }
}
pub fn insertion_to_target_index(
insert_index: usize,
drag_source_index: Option<usize>,
) -> usize {
if let Some(src) = drag_source_index {
if insert_index > src {
insert_index - 1
} else {
insert_index
}
} else {
insert_index
}
}
pub fn test_set_drag_state(&mut self, tab_id: Option<TabId>, in_progress: bool) {
self.drag_in_progress = in_progress;
self.dragging_tab = tab_id;
}
pub fn test_set_drop_target(&mut self, index: Option<usize>) {
self.drop_target_index = index;
}
pub fn test_drop_target_index(&self) -> Option<usize> {
self.drop_target_index
}
pub fn test_dragging_tab(&self) -> Option<TabId> {
self.dragging_tab
}
pub fn test_open_context_menu(&mut self, tab_id: TabId) {
self.context_menu_tab = Some(tab_id);
self.context_menu_opened_frame = 0;
self.renaming_tab = false;
self.picking_icon = false;
}
pub fn test_close_context_menu(&mut self) {
self.context_menu_tab = None;
self.renaming_tab = false;
self.picking_icon = false;
}
pub fn test_context_menu_tab(&self) -> Option<TabId> {
self.context_menu_tab
}
pub fn test_set_renaming(&mut self, value: bool) {
self.renaming_tab = value;
}
}