1use std::cell::Cell;
8use std::rc::Rc;
9use std::sync::Arc;
10
11use crate::color::Color;
12use crate::event::{Event, EventResult, MouseButton};
13use crate::geometry::{Point, Rect, Size};
14use crate::draw_ctx::DrawCtx;
15use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
16use crate::text::Font;
17use crate::widget::Widget;
18use crate::widgets::primitives::Spacer;
19
20const ACTION_BTN_W: f64 = 100.0;
21const DIVIDER_W: f64 = 6.0;
22const MIN_SIDEBAR_W: f64 = 160.0;
23
24pub struct TabView {
29 bounds: Rect,
30 children: Vec<Box<dyn Widget>>,
32 base: WidgetBase,
33 tab_contents: Vec<Box<dyn Widget>>,
34 tab_labels: Vec<String>,
35 active_tab: usize,
36 tab_bar_height: f64,
37 font: Arc<Font>,
38 font_size: f64,
39 hovered_tab: Option<usize>,
40 action_label: Option<String>,
41 action_hovered: bool,
42 on_action: Option<Box<dyn Fn()>>,
43 action_active: bool,
44 show_sidebar: Option<Rc<Cell<bool>>>,
46 sidebar_w: f64,
47 sidebar_dragging: bool,
48 active_tab_cell: Option<Rc<Cell<usize>>>,
52}
53
54impl TabView {
55 pub fn new(font: Arc<Font>) -> Self {
56 Self {
57 bounds: Rect::default(),
58 children: Vec::new(),
59 base: WidgetBase::new(),
60 tab_contents: Vec::new(),
61 tab_labels: Vec::new(),
62 active_tab: 0,
63 tab_bar_height: 36.0,
64 font,
65 font_size: 13.0,
66 hovered_tab: None,
67 action_label: None,
68 action_hovered: false,
69 on_action: None,
70 action_active: false,
71 show_sidebar: None,
72 sidebar_w: 320.0,
73 sidebar_dragging: false,
74 active_tab_cell: None,
75 }
76 }
77
78 pub fn with_tab_bar_height(mut self, h: f64) -> Self { self.tab_bar_height = h; self }
79 pub fn with_font_size(mut self, size: f64) -> Self { self.font_size = size; self }
80
81 pub fn with_active_tab_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
86 self.active_tab_cell = Some(cell);
87 self
88 }
89
90 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
91 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
92 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
93 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
94 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
95
96 pub fn with_action_button(
98 mut self,
99 label: impl Into<String>,
100 on_click: impl Fn() + 'static,
101 ) -> Self {
102 self.action_label = Some(label.into());
103 self.on_action = Some(Box::new(on_click));
104 self
105 }
106
107 pub fn set_action_active(&mut self, active: bool) { self.action_active = active; }
109
110 pub fn add_tab(mut self, label: impl Into<String>, content: Box<dyn Widget>) -> Self {
112 let idx = self.tab_labels.len();
113 self.tab_labels.push(label.into());
114 if idx == 0 {
115 self.children.insert(0, content);
117 self.tab_contents.push(Box::new(Spacer::new()));
118 } else {
119 self.tab_contents.push(content);
120 }
121 self
122 }
123
124 pub fn with_sidebar(
128 mut self,
129 widget: Box<dyn Widget>,
130 show: Rc<Cell<bool>>,
131 ) -> Self {
132 self.show_sidebar = Some(show);
133 self.children.push(widget); self
135 }
136
137 fn sidebar_showing(&self) -> bool {
140 self.show_sidebar.as_ref().map(|s| s.get()).unwrap_or(false)
141 }
142
143 fn content_height(&self) -> f64 {
144 (self.bounds.height - self.tab_bar_height).max(0.0)
145 }
146
147 fn tabs_width(&self) -> f64 {
148 if self.action_label.is_some() {
149 (self.bounds.width - ACTION_BTN_W).max(0.0)
150 } else {
151 self.bounds.width
152 }
153 }
154
155 fn divider_x(&self) -> f64 {
157 (self.bounds.width - self.sidebar_w - DIVIDER_W).max(0.0)
158 }
159
160 fn tab_index_at(&self, pos: Point) -> Option<usize> {
161 if pos.y < self.content_height() { return None; }
162 if pos.x >= self.tabs_width() { return None; }
163 let n = self.tab_labels.len().max(1);
164 let tab_w = self.tabs_width() / n as f64;
165 let i = (pos.x / tab_w) as usize;
166 if i < self.tab_labels.len() { Some(i) } else { None }
167 }
168
169 fn action_btn_hit(&self, pos: Point) -> bool {
170 self.action_label.is_some()
171 && pos.y >= self.content_height()
172 && pos.x >= self.tabs_width()
173 }
174
175 fn switch_to(&mut self, new_idx: usize) {
176 if new_idx == self.active_tab || new_idx >= self.tab_labels.len() { return; }
177 let old_sidebar = if self.children.len() > 1 { self.children.pop() } else { None };
180 if let Some(current) = self.children.pop() {
181 self.tab_contents[self.active_tab] = current;
182 }
183 let placeholder: Box<dyn Widget> = Box::new(Spacer::new());
184 let new_child = std::mem::replace(&mut self.tab_contents[new_idx], placeholder);
185 self.children.push(new_child); if let Some(s) = old_sidebar { self.children.push(s); } self.active_tab = new_idx;
188 if let Some(cell) = &self.active_tab_cell {
189 cell.set(new_idx);
190 }
191 }
192}
193
194impl Widget for TabView {
195 fn type_name(&self) -> &'static str { "TabView" }
196 fn bounds(&self) -> Rect { self.bounds }
197 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
198 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
199 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
200
201 fn margin(&self) -> Insets { self.base.margin }
202 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
203 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
204 fn min_size(&self) -> Size { self.base.min_size }
205 fn max_size(&self) -> Size { self.base.max_size }
206
207 fn layout(&mut self, available: Size) -> Size {
208 if let Some(cell) = self.active_tab_cell.clone() {
212 let want = cell.get();
213 if want != self.active_tab && want < self.tab_labels.len() {
214 self.switch_to(want);
215 }
216 }
217 let content_h = (available.height - self.tab_bar_height).max(0.0);
218 let showing = self.sidebar_showing();
219 let sw = if showing {
220 self.sidebar_w.clamp(MIN_SIDEBAR_W, available.width * 0.8)
221 } else {
222 0.0
223 };
224 let content_w = if showing {
225 (available.width - sw - DIVIDER_W).max(0.0)
226 } else {
227 available.width
228 };
229
230 if let Some(child) = self.children.get_mut(0) {
232 child.layout(Size::new(content_w, content_h));
233 child.set_bounds(Rect::new(0.0, 0.0, content_w, content_h));
234 }
235 if let Some(sidebar) = self.children.get_mut(1) {
237 if showing {
238 sidebar.layout(Size::new(sw, content_h));
239 sidebar.set_bounds(Rect::new(content_w + DIVIDER_W, 0.0, sw, content_h));
240 } else {
241 sidebar.layout(Size::new(0.0, 0.0));
242 sidebar.set_bounds(Rect::new(available.width + 1.0, 0.0, 0.0, 0.0));
244 }
245 }
246
247 available
248 }
249
250 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
251 let w = self.bounds.width;
252 let h = self.bounds.height;
253 let tab_h = self.tab_bar_height;
254 let content_h = self.content_height();
255 let tabs_w = self.tabs_width();
256 let n = self.tab_labels.len().max(1);
257 let tab_w = tabs_w / n as f64;
258 let bar_y = content_h;
259
260 let v = ctx.visuals();
261
262 ctx.set_fill_color(v.panel_fill);
264 ctx.begin_path();
265 ctx.rect(0.0, bar_y, w, tab_h);
266 ctx.fill();
267
268 ctx.set_stroke_color(v.separator);
270 ctx.set_line_width(1.0);
271 ctx.begin_path();
272 ctx.move_to(0.0, bar_y);
273 ctx.line_to(w, bar_y);
274 ctx.stroke();
275
276 let font = crate::font_settings::current_system_font()
279 .unwrap_or_else(|| Arc::clone(&self.font));
280 ctx.set_font(Arc::clone(&font));
281 ctx.set_font_size(self.font_size);
282
283 for (i, label) in self.tab_labels.iter().enumerate() {
285 let tx = i as f64 * tab_w;
286 let is_active = i == self.active_tab;
287 let is_hovered = self.hovered_tab == Some(i);
288
289 if is_hovered && !is_active {
290 ctx.set_fill_color(v.widget_bg_hovered);
291 ctx.begin_path();
292 ctx.rect(tx, bar_y, tab_w, tab_h);
293 ctx.fill();
294 }
295 if is_active {
296 ctx.set_fill_color(v.accent);
297 ctx.begin_path();
298 ctx.rect(tx, h - 2.5, tab_w, 2.5);
299 ctx.fill();
300 }
301 let label_color = if is_active {
302 v.accent
303 } else if is_hovered {
304 v.text_color
305 } else {
306 v.text_dim
307 };
308 ctx.set_fill_color(label_color);
309 if let Some(m) = ctx.measure_text(label) {
310 let lx = tx + (tab_w - m.width) * 0.5;
311 let ly = bar_y + (tab_h - (m.ascent + m.descent)) * 0.5 + m.descent;
312 ctx.fill_text(label, lx, ly);
313 }
314 }
315
316 if let Some(ref label) = self.action_label.clone() {
318 let bx = tabs_w;
319 let bg = if self.action_active {
320 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.18)
321 } else if self.action_hovered {
322 v.widget_bg_hovered
323 } else {
324 Color::transparent()
325 };
326 if bg.a > 0.0 {
327 ctx.set_fill_color(bg);
328 ctx.begin_path();
329 ctx.rect(bx, bar_y, ACTION_BTN_W, tab_h);
330 ctx.fill();
331 }
332 ctx.set_stroke_color(v.separator);
333 ctx.set_line_width(1.0);
334 ctx.begin_path();
335 ctx.move_to(bx, bar_y + 6.0);
336 ctx.line_to(bx, h - 6.0);
337 ctx.stroke();
338
339 let lc = if self.action_active { v.accent } else { v.text_dim };
340 ctx.set_fill_color(lc);
341 if let Some(m) = ctx.measure_text(label) {
342 let lx = bx + (ACTION_BTN_W - m.width) * 0.5;
343 let ly = bar_y + (tab_h - (m.ascent + m.descent)) * 0.5 + m.descent;
344 ctx.fill_text(label, lx, ly);
345 }
346 }
347
348 if self.sidebar_showing() {
350 let div_x = self.divider_x();
351 let div_color = if self.sidebar_dragging {
352 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.55)
353 } else {
354 v.separator
355 };
356 ctx.set_fill_color(div_color);
357 ctx.begin_path();
358 ctx.rect(div_x, 0.0, DIVIDER_W, content_h);
359 ctx.fill();
360
361 if content_h > 30.0 {
363 let grip = if self.sidebar_dragging {
364 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.8)
365 } else {
366 v.text_dim
367 };
368 ctx.set_fill_color(grip);
369 let cx = div_x + DIVIDER_W * 0.5;
370 let cy = content_h * 0.5;
371 for i in -1i32..=1 {
372 ctx.begin_path();
373 ctx.circle(cx, cy + i as f64 * 5.0, 1.5);
374 ctx.fill();
375 }
376 }
377 }
378 }
379
380 fn hit_test(&self, local_pos: Point) -> bool {
381 if self.sidebar_dragging { return true; }
383 local_pos.x >= 0.0 && local_pos.x <= self.bounds.width
384 && local_pos.y >= 0.0 && local_pos.y <= self.bounds.height
385 }
386
387 fn on_event(&mut self, event: &Event) -> EventResult {
388 match event {
389 Event::MouseMove { pos } => {
390 let was_tab = self.hovered_tab;
391 let was_act = self.action_hovered;
392 self.hovered_tab = self.tab_index_at(*pos);
393 self.action_hovered = self.action_btn_hit(*pos);
394 if self.sidebar_dragging {
395 let new_w = self.bounds.width - pos.x;
397 self.sidebar_w = new_w.clamp(MIN_SIDEBAR_W, self.bounds.width * 0.8);
398 crate::animation::request_tick();
399 return EventResult::Consumed;
400 }
401 if was_tab != self.hovered_tab || was_act != self.action_hovered {
402 crate::animation::request_tick();
403 }
404 EventResult::Ignored
405 }
406 Event::MouseDown { pos, button: MouseButton::Left, .. } => {
407 if self.action_btn_hit(*pos) {
408 self.action_active = !self.action_active;
409 if let Some(ref cb) = self.on_action { cb(); }
410 crate::animation::request_tick();
411 return EventResult::Consumed;
412 }
413 if self.sidebar_showing() && pos.y < self.content_height() {
415 let div_x = self.divider_x();
416 if pos.x >= div_x - 2.0 && pos.x <= div_x + DIVIDER_W + 2.0 {
417 self.sidebar_dragging = true;
418 crate::animation::request_tick();
419 return EventResult::Consumed;
420 }
421 }
422 if let Some(i) = self.tab_index_at(*pos) {
423 self.switch_to(i);
424 crate::animation::request_tick();
425 return EventResult::Consumed;
426 }
427 EventResult::Ignored
428 }
429 Event::MouseUp { button: MouseButton::Left, .. } => {
430 if self.sidebar_dragging {
431 self.sidebar_dragging = false;
432 crate::animation::request_tick();
433 return EventResult::Consumed;
434 }
435 EventResult::Ignored
436 }
437 _ => EventResult::Ignored,
438 }
439 }
440}