1use crate::config::{Config, TabBarMode};
6use crate::tab::{TabId, TabManager};
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum TabBarAction {
11 None,
13 SwitchTo(TabId),
15 Close(TabId),
17 NewTab,
19 #[allow(dead_code)]
21 Reorder(TabId, usize),
22}
23
24pub struct TabBarUI {
26 pub hovered_tab: Option<TabId>,
28 pub close_hovered: Option<TabId>,
30 #[allow(dead_code)]
32 drag_in_progress: bool,
33 #[allow(dead_code)]
35 dragging_tab: Option<TabId>,
36}
37
38impl TabBarUI {
39 pub fn new() -> Self {
41 Self {
42 hovered_tab: None,
43 close_hovered: None,
44 drag_in_progress: false,
45 dragging_tab: None,
46 }
47 }
48
49 pub fn should_show(&self, tab_count: usize, mode: TabBarMode) -> bool {
51 match mode {
52 TabBarMode::Always => true,
53 TabBarMode::WhenMultiple => tab_count > 1,
54 TabBarMode::Never => false,
55 }
56 }
57
58 pub fn render(
60 &mut self,
61 ctx: &egui::Context,
62 tabs: &TabManager,
63 config: &Config,
64 ) -> TabBarAction {
65 let tab_count = tabs.tab_count();
66
67 if !self.should_show(tab_count, config.tab_bar_mode) {
69 return TabBarAction::None;
70 }
71
72 let mut action = TabBarAction::None;
73 let active_tab_id = tabs.active_tab_id();
74
75 egui::TopBottomPanel::top("tab_bar")
77 .exact_height(config.tab_bar_height)
78 .frame(egui::Frame::NONE.fill(egui::Color32::from_rgb(40, 40, 40)))
79 .show(ctx, |ui| {
80 ui.horizontal(|ui| {
81 ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0);
83
84 for tab in tabs.tabs() {
86 let is_active = Some(tab.id) == active_tab_id;
87 let is_bell_active = tab.is_bell_active();
88 let tab_action = self.render_tab(
89 ui,
90 tab.id,
91 &tab.title,
92 is_active,
93 tab.has_activity,
94 is_bell_active,
95 config,
96 );
97
98 if tab_action != TabBarAction::None {
99 action = tab_action;
100 }
101 }
102
103 ui.add_space(4.0);
105 let new_tab_btn = ui.add(
106 egui::Button::new("+")
107 .min_size(egui::vec2(24.0, config.tab_bar_height - 4.0))
108 .fill(egui::Color32::TRANSPARENT),
109 );
110
111 if new_tab_btn.clicked() {
112 action = TabBarAction::NewTab;
113 }
114
115 if new_tab_btn.hovered() {
116 new_tab_btn.on_hover_text("New Tab (Cmd+T)");
117 }
118 });
119 });
120
121 action
122 }
123
124 #[allow(clippy::too_many_arguments)]
126 fn render_tab(
127 &mut self,
128 ui: &mut egui::Ui,
129 id: TabId,
130 title: &str,
131 is_active: bool,
132 has_activity: bool,
133 is_bell_active: bool,
134 config: &Config,
135 ) -> TabBarAction {
136 let mut action = TabBarAction::None;
137
138 let available_width = ui.available_width();
140 let tab_count = 10; let tab_width = (available_width / tab_count as f32).clamp(80.0, 200.0);
142
143 let bg_color = if is_active {
145 egui::Color32::from_rgb(60, 60, 60)
146 } else if self.hovered_tab == Some(id) {
147 egui::Color32::from_rgb(50, 50, 50)
148 } else {
149 egui::Color32::from_rgb(40, 40, 40)
150 };
151
152 let (tab_rect, tab_response) = ui.allocate_exact_size(
154 egui::vec2(tab_width, config.tab_bar_height),
155 egui::Sense::click(),
156 );
157
158 if ui.is_rect_visible(tab_rect) {
160 ui.painter().rect_filled(tab_rect, 0.0, bg_color);
161
162 let mut content_ui = ui.new_child(
164 egui::UiBuilder::new()
165 .max_rect(tab_rect.shrink2(egui::vec2(8.0, 4.0)))
166 .layout(egui::Layout::left_to_right(egui::Align::Center)),
167 );
168
169 content_ui.horizontal(|ui| {
170 if is_bell_active {
172 ui.colored_label(egui::Color32::from_rgb(255, 200, 100), "🔔");
173 ui.add_space(4.0);
174 } else if has_activity && !is_active {
175 ui.colored_label(egui::Color32::from_rgb(100, 180, 255), "•");
177 ui.add_space(4.0);
178 }
179
180 if config.tab_show_index {
182 }
184
185 let max_title_len = if config.tab_show_close_button { 15 } else { 20 };
187 let display_title = if title.len() > max_title_len {
188 format!("{}…", &title[..max_title_len - 1])
189 } else {
190 title.to_string()
191 };
192
193 let text_color = if is_active {
194 egui::Color32::WHITE
195 } else {
196 egui::Color32::from_rgb(180, 180, 180)
197 };
198
199 ui.label(egui::RichText::new(&display_title).color(text_color));
200
201 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
203 if config.tab_show_close_button {
205 let close_color = if self.close_hovered == Some(id) {
206 egui::Color32::from_rgb(255, 100, 100)
207 } else {
208 egui::Color32::from_rgb(150, 150, 150)
209 };
210
211 let close_btn = ui.add(
212 egui::Button::new(
213 egui::RichText::new("×").color(close_color).size(14.0),
214 )
215 .fill(egui::Color32::TRANSPARENT)
216 .frame(false),
217 );
218
219 if close_btn.hovered() {
220 self.close_hovered = Some(id);
221 } else if self.close_hovered == Some(id) {
222 self.close_hovered = None;
223 }
224
225 if close_btn.clicked() {
226 action = TabBarAction::Close(id);
227 }
228 }
229 });
230 });
231 }
232
233 if tab_response.clicked() && action == TabBarAction::None {
235 action = TabBarAction::SwitchTo(id);
236 }
237
238 if tab_response.hovered() {
240 self.hovered_tab = Some(id);
241 } else if self.hovered_tab == Some(id) {
242 self.hovered_tab = None;
243 }
244
245 if is_active {
247 ui.painter().hline(
248 tab_rect.left()..=tab_rect.right(),
249 tab_rect.bottom() - 2.0,
250 egui::Stroke::new(2.0, egui::Color32::from_rgb(100, 150, 255)),
251 );
252 }
253
254 action
255 }
256
257 #[allow(dead_code)]
259 pub fn get_height(&self, tab_count: usize, config: &Config) -> f32 {
260 if self.should_show(tab_count, config.tab_bar_mode) {
261 config.tab_bar_height
262 } else {
263 0.0
264 }
265 }
266}
267
268impl Default for TabBarUI {
269 fn default() -> Self {
270 Self::new()
271 }
272}