1use std::cell::Cell;
8use std::rc::Rc;
9use std::sync::Arc;
10
11use crate::color::Color;
12use crate::draw_ctx::DrawCtx;
13use crate::event::{Event, EventResult, MouseButton};
14use crate::geometry::{Point, Rect, Size};
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 {
79 self.tab_bar_height = h;
80 self
81 }
82 pub fn with_font_size(mut self, size: f64) -> Self {
83 self.font_size = size;
84 self
85 }
86
87 pub fn with_active_tab_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
92 self.active_tab_cell = Some(cell);
93 self
94 }
95
96 pub fn with_margin(mut self, m: Insets) -> Self {
97 self.base.margin = m;
98 self
99 }
100 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
101 self.base.h_anchor = h;
102 self
103 }
104 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
105 self.base.v_anchor = v;
106 self
107 }
108 pub fn with_min_size(mut self, s: Size) -> Self {
109 self.base.min_size = s;
110 self
111 }
112 pub fn with_max_size(mut self, s: Size) -> Self {
113 self.base.max_size = s;
114 self
115 }
116
117 pub fn with_action_button(
119 mut self,
120 label: impl Into<String>,
121 on_click: impl Fn() + 'static,
122 ) -> Self {
123 self.action_label = Some(label.into());
124 self.on_action = Some(Box::new(on_click));
125 self
126 }
127
128 pub fn set_action_active(&mut self, active: bool) {
130 self.action_active = active;
131 }
132
133 pub fn add_tab(mut self, label: impl Into<String>, content: Box<dyn Widget>) -> Self {
135 let idx = self.tab_labels.len();
136 self.tab_labels.push(label.into());
137 if idx == 0 {
138 self.children.insert(0, content);
140 self.tab_contents.push(Box::new(Spacer::new()));
141 } else {
142 self.tab_contents.push(content);
143 }
144 self
145 }
146
147 pub fn with_sidebar(mut self, widget: Box<dyn Widget>, show: Rc<Cell<bool>>) -> Self {
151 self.show_sidebar = Some(show);
152 self.children.push(widget); self
154 }
155
156 fn sidebar_showing(&self) -> bool {
159 self.show_sidebar.as_ref().map(|s| s.get()).unwrap_or(false)
160 }
161
162 fn content_height(&self) -> f64 {
163 (self.bounds.height - self.tab_bar_height).max(0.0)
164 }
165
166 fn tabs_width(&self) -> f64 {
167 if self.action_label.is_some() {
168 (self.bounds.width - ACTION_BTN_W).max(0.0)
169 } else {
170 self.bounds.width
171 }
172 }
173
174 fn divider_x(&self) -> f64 {
176 (self.bounds.width - self.sidebar_w - DIVIDER_W).max(0.0)
177 }
178
179 fn tab_index_at(&self, pos: Point) -> Option<usize> {
180 if pos.y < self.content_height() {
181 return None;
182 }
183 if pos.x >= self.tabs_width() {
184 return None;
185 }
186 let n = self.tab_labels.len().max(1);
187 let tab_w = self.tabs_width() / n as f64;
188 let i = (pos.x / tab_w) as usize;
189 if i < self.tab_labels.len() {
190 Some(i)
191 } else {
192 None
193 }
194 }
195
196 fn action_btn_hit(&self, pos: Point) -> bool {
197 self.action_label.is_some() && pos.y >= self.content_height() && pos.x >= self.tabs_width()
198 }
199
200 fn switch_to(&mut self, new_idx: usize) {
201 if new_idx == self.active_tab || new_idx >= self.tab_labels.len() {
202 return;
203 }
204 let old_sidebar = if self.children.len() > 1 {
207 self.children.pop()
208 } else {
209 None
210 };
211 if let Some(current) = self.children.pop() {
212 self.tab_contents[self.active_tab] = current;
213 }
214 let placeholder: Box<dyn Widget> = Box::new(Spacer::new());
215 let new_child = std::mem::replace(&mut self.tab_contents[new_idx], placeholder);
216 self.children.push(new_child); if let Some(s) = old_sidebar {
218 self.children.push(s);
219 } self.active_tab = new_idx;
221 if let Some(cell) = &self.active_tab_cell {
222 cell.set(new_idx);
223 }
224 }
225}
226
227impl Widget for TabView {
228 fn type_name(&self) -> &'static str {
229 "TabView"
230 }
231 fn bounds(&self) -> Rect {
232 self.bounds
233 }
234 fn set_bounds(&mut self, b: Rect) {
235 self.bounds = b;
236 }
237 fn children(&self) -> &[Box<dyn Widget>] {
238 &self.children
239 }
240 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
241 &mut self.children
242 }
243
244 fn margin(&self) -> Insets {
245 self.base.margin
246 }
247 fn h_anchor(&self) -> HAnchor {
248 self.base.h_anchor
249 }
250 fn v_anchor(&self) -> VAnchor {
251 self.base.v_anchor
252 }
253 fn min_size(&self) -> Size {
254 self.base.min_size
255 }
256 fn max_size(&self) -> Size {
257 self.base.max_size
258 }
259
260 fn layout(&mut self, available: Size) -> Size {
261 if let Some(cell) = self.active_tab_cell.clone() {
265 let want = cell.get();
266 if want != self.active_tab && want < self.tab_labels.len() {
267 self.switch_to(want);
268 }
269 }
270 let content_h = (available.height - self.tab_bar_height).max(0.0);
271 let showing = self.sidebar_showing();
272 let sw = if showing {
273 self.sidebar_w.clamp(MIN_SIDEBAR_W, available.width * 0.8)
274 } else {
275 0.0
276 };
277 let content_w = if showing {
278 (available.width - sw - DIVIDER_W).max(0.0)
279 } else {
280 available.width
281 };
282
283 if let Some(child) = self.children.get_mut(0) {
285 child.layout(Size::new(content_w, content_h));
286 child.set_bounds(Rect::new(0.0, 0.0, content_w, content_h));
287 }
288 if let Some(sidebar) = self.children.get_mut(1) {
290 if showing {
291 sidebar.layout(Size::new(sw, content_h));
292 sidebar.set_bounds(Rect::new(content_w + DIVIDER_W, 0.0, sw, content_h));
293 } else {
294 sidebar.layout(Size::new(0.0, 0.0));
295 sidebar.set_bounds(Rect::new(available.width + 1.0, 0.0, 0.0, 0.0));
297 }
298 }
299
300 available
301 }
302
303 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
304 let w = self.bounds.width;
305 let h = self.bounds.height;
306 let tab_h = self.tab_bar_height;
307 let content_h = self.content_height();
308 let tabs_w = self.tabs_width();
309 let n = self.tab_labels.len().max(1);
310 let tab_w = tabs_w / n as f64;
311 let bar_y = content_h;
312
313 let v = ctx.visuals();
314
315 ctx.set_fill_color(v.panel_fill);
317 ctx.begin_path();
318 ctx.rect(0.0, bar_y, w, tab_h);
319 ctx.fill();
320
321 ctx.set_stroke_color(v.separator);
323 ctx.set_line_width(1.0);
324 ctx.begin_path();
325 ctx.move_to(0.0, bar_y);
326 ctx.line_to(w, bar_y);
327 ctx.stroke();
328
329 let font =
332 crate::font_settings::current_system_font().unwrap_or_else(|| Arc::clone(&self.font));
333 ctx.set_font(Arc::clone(&font));
334 ctx.set_font_size(self.font_size);
335
336 for (i, label) in self.tab_labels.iter().enumerate() {
338 let tx = i as f64 * tab_w;
339 let is_active = i == self.active_tab;
340 let is_hovered = self.hovered_tab == Some(i);
341
342 if is_hovered && !is_active {
343 ctx.set_fill_color(v.widget_bg_hovered);
344 ctx.begin_path();
345 ctx.rect(tx, bar_y, tab_w, tab_h);
346 ctx.fill();
347 }
348 if is_active {
349 ctx.set_fill_color(v.accent);
350 ctx.begin_path();
351 ctx.rect(tx, h - 2.5, tab_w, 2.5);
352 ctx.fill();
353 }
354 let label_color = if is_active {
355 v.accent
356 } else if is_hovered {
357 v.text_color
358 } else {
359 v.text_dim
360 };
361 ctx.set_fill_color(label_color);
362 if let Some(m) = ctx.measure_text(label) {
363 let lx = tx + (tab_w - m.width) * 0.5;
364 let ly = bar_y + (tab_h - (m.ascent + m.descent)) * 0.5 + m.descent;
365 ctx.fill_text(label, lx, ly);
366 }
367 }
368
369 if let Some(ref label) = self.action_label.clone() {
371 let bx = tabs_w;
372 let bg = if self.action_active {
373 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.18)
374 } else if self.action_hovered {
375 v.widget_bg_hovered
376 } else {
377 Color::transparent()
378 };
379 if bg.a > 0.0 {
380 ctx.set_fill_color(bg);
381 ctx.begin_path();
382 ctx.rect(bx, bar_y, ACTION_BTN_W, tab_h);
383 ctx.fill();
384 }
385 ctx.set_stroke_color(v.separator);
386 ctx.set_line_width(1.0);
387 ctx.begin_path();
388 ctx.move_to(bx, bar_y + 6.0);
389 ctx.line_to(bx, h - 6.0);
390 ctx.stroke();
391
392 let lc = if self.action_active {
393 v.accent
394 } else {
395 v.text_dim
396 };
397 ctx.set_fill_color(lc);
398 if let Some(m) = ctx.measure_text(label) {
399 let lx = bx + (ACTION_BTN_W - m.width) * 0.5;
400 let ly = bar_y + (tab_h - (m.ascent + m.descent)) * 0.5 + m.descent;
401 ctx.fill_text(label, lx, ly);
402 }
403 }
404
405 if self.sidebar_showing() {
407 let div_x = self.divider_x();
408 let div_color = if self.sidebar_dragging {
409 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.55)
410 } else {
411 v.separator
412 };
413 ctx.set_fill_color(div_color);
414 ctx.begin_path();
415 ctx.rect(div_x, 0.0, DIVIDER_W, content_h);
416 ctx.fill();
417
418 if content_h > 30.0 {
420 let grip = if self.sidebar_dragging {
421 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.8)
422 } else {
423 v.text_dim
424 };
425 ctx.set_fill_color(grip);
426 let cx = div_x + DIVIDER_W * 0.5;
427 let cy = content_h * 0.5;
428 for i in -1i32..=1 {
429 ctx.begin_path();
430 ctx.circle(cx, cy + i as f64 * 5.0, 1.5);
431 ctx.fill();
432 }
433 }
434 }
435 }
436
437 fn hit_test(&self, local_pos: Point) -> bool {
438 if self.sidebar_dragging {
440 return true;
441 }
442 local_pos.x >= 0.0
443 && local_pos.x <= self.bounds.width
444 && local_pos.y >= 0.0
445 && local_pos.y <= self.bounds.height
446 }
447
448 fn on_event(&mut self, event: &Event) -> EventResult {
449 match event {
450 Event::MouseMove { pos } => {
451 let was_tab = self.hovered_tab;
452 let was_act = self.action_hovered;
453 self.hovered_tab = self.tab_index_at(*pos);
454 self.action_hovered = self.action_btn_hit(*pos);
455 if self.sidebar_dragging {
456 let new_w = self.bounds.width - pos.x;
458 self.sidebar_w = new_w.clamp(MIN_SIDEBAR_W, self.bounds.width * 0.8);
459 crate::animation::request_draw();
460 return EventResult::Consumed;
461 }
462 if was_tab != self.hovered_tab || was_act != self.action_hovered {
463 crate::animation::request_draw();
464 }
465 EventResult::Ignored
466 }
467 Event::MouseDown {
468 pos,
469 button: MouseButton::Left,
470 ..
471 } => {
472 if self.action_btn_hit(*pos) {
473 self.action_active = !self.action_active;
474 if let Some(ref cb) = self.on_action {
475 cb();
476 }
477 crate::animation::request_draw();
478 return EventResult::Consumed;
479 }
480 if self.sidebar_showing() && pos.y < self.content_height() {
482 let div_x = self.divider_x();
483 if pos.x >= div_x - 2.0 && pos.x <= div_x + DIVIDER_W + 2.0 {
484 self.sidebar_dragging = true;
485 crate::animation::request_draw();
486 return EventResult::Consumed;
487 }
488 }
489 if let Some(i) = self.tab_index_at(*pos) {
490 self.switch_to(i);
491 crate::animation::request_draw();
492 return EventResult::Consumed;
493 }
494 EventResult::Ignored
495 }
496 Event::MouseUp {
497 button: MouseButton::Left,
498 ..
499 } => {
500 if self.sidebar_dragging {
501 self.sidebar_dragging = false;
502 crate::animation::request_draw();
503 return EventResult::Consumed;
504 }
505 EventResult::Ignored
506 }
507 _ => EventResult::Ignored,
508 }
509 }
510}