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 widget_base(&self) -> Option<&WidgetBase> {
248 Some(&self.base)
249 }
250 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
251 Some(&mut self.base)
252 }
253 fn h_anchor(&self) -> HAnchor {
254 self.base.h_anchor
255 }
256 fn v_anchor(&self) -> VAnchor {
257 self.base.v_anchor
258 }
259 fn min_size(&self) -> Size {
260 self.base.min_size
261 }
262 fn max_size(&self) -> Size {
263 self.base.max_size
264 }
265
266 fn layout(&mut self, available: Size) -> Size {
267 if let Some(cell) = self.active_tab_cell.clone() {
271 let want = cell.get();
272 if want != self.active_tab && want < self.tab_labels.len() {
273 self.switch_to(want);
274 }
275 }
276 let content_h = (available.height - self.tab_bar_height).max(0.0);
277 let showing = self.sidebar_showing();
278 let sw = if showing {
279 self.sidebar_w.clamp(MIN_SIDEBAR_W, available.width * 0.8)
280 } else {
281 0.0
282 };
283 let content_w = if showing {
284 (available.width - sw - DIVIDER_W).max(0.0)
285 } else {
286 available.width
287 };
288
289 if let Some(child) = self.children.get_mut(0) {
291 child.layout(Size::new(content_w, content_h));
292 child.set_bounds(Rect::new(0.0, 0.0, content_w, content_h));
293 }
294 if let Some(sidebar) = self.children.get_mut(1) {
296 if showing {
297 sidebar.layout(Size::new(sw, content_h));
298 sidebar.set_bounds(Rect::new(content_w + DIVIDER_W, 0.0, sw, content_h));
299 } else {
300 sidebar.layout(Size::new(0.0, 0.0));
301 sidebar.set_bounds(Rect::new(available.width + 1.0, 0.0, 0.0, 0.0));
303 }
304 }
305
306 available
307 }
308
309 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
310 let w = self.bounds.width;
311 let h = self.bounds.height;
312 let tab_h = self.tab_bar_height;
313 let content_h = self.content_height();
314 let tabs_w = self.tabs_width();
315 let n = self.tab_labels.len().max(1);
316 let tab_w = tabs_w / n as f64;
317 let bar_y = content_h;
318
319 let v = ctx.visuals();
320
321 ctx.set_fill_color(v.panel_fill);
323 ctx.begin_path();
324 ctx.rect(0.0, bar_y, w, tab_h);
325 ctx.fill();
326
327 ctx.set_stroke_color(v.separator);
329 ctx.set_line_width(1.0);
330 ctx.begin_path();
331 ctx.move_to(0.0, bar_y);
332 ctx.line_to(w, bar_y);
333 ctx.stroke();
334
335 let font =
338 crate::font_settings::current_system_font().unwrap_or_else(|| Arc::clone(&self.font));
339 ctx.set_font(Arc::clone(&font));
340 ctx.set_font_size(self.font_size);
341
342 for (i, label) in self.tab_labels.iter().enumerate() {
344 let tx = i as f64 * tab_w;
345 let is_active = i == self.active_tab;
346 let is_hovered = self.hovered_tab == Some(i);
347
348 if is_hovered && !is_active {
349 ctx.set_fill_color(v.widget_bg_hovered);
350 ctx.begin_path();
351 ctx.rect(tx, bar_y, tab_w, tab_h);
352 ctx.fill();
353 }
354 if is_active {
355 ctx.set_fill_color(v.accent);
356 ctx.begin_path();
357 ctx.rect(tx, h - 2.5, tab_w, 2.5);
358 ctx.fill();
359 }
360 let label_color = if is_active {
361 v.accent
362 } else if is_hovered {
363 v.text_color
364 } else {
365 v.text_dim
366 };
367 ctx.set_fill_color(label_color);
368 if let Some(m) = ctx.measure_text(label) {
369 let lx = tx + (tab_w - m.width) * 0.5;
370 let ly = bar_y + (tab_h - (m.ascent + m.descent)) * 0.5 + m.descent;
371 ctx.fill_text(label, lx, ly);
372 }
373 }
374
375 if let Some(ref label) = self.action_label.clone() {
377 let bx = tabs_w;
378 let bg = if self.action_active {
379 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.18)
380 } else if self.action_hovered {
381 v.widget_bg_hovered
382 } else {
383 Color::transparent()
384 };
385 if bg.a > 0.0 {
386 ctx.set_fill_color(bg);
387 ctx.begin_path();
388 ctx.rect(bx, bar_y, ACTION_BTN_W, tab_h);
389 ctx.fill();
390 }
391 ctx.set_stroke_color(v.separator);
392 ctx.set_line_width(1.0);
393 ctx.begin_path();
394 ctx.move_to(bx, bar_y + 6.0);
395 ctx.line_to(bx, h - 6.0);
396 ctx.stroke();
397
398 let lc = if self.action_active {
399 v.accent
400 } else {
401 v.text_dim
402 };
403 ctx.set_fill_color(lc);
404 if let Some(m) = ctx.measure_text(label) {
405 let lx = bx + (ACTION_BTN_W - m.width) * 0.5;
406 let ly = bar_y + (tab_h - (m.ascent + m.descent)) * 0.5 + m.descent;
407 ctx.fill_text(label, lx, ly);
408 }
409 }
410
411 if self.sidebar_showing() {
413 let div_x = self.divider_x();
414 let div_color = if self.sidebar_dragging {
415 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.55)
416 } else {
417 v.separator
418 };
419 ctx.set_fill_color(div_color);
420 ctx.begin_path();
421 ctx.rect(div_x, 0.0, DIVIDER_W, content_h);
422 ctx.fill();
423
424 if content_h > 30.0 {
426 let grip = if self.sidebar_dragging {
427 Color::rgba(v.accent.r, v.accent.g, v.accent.b, 0.8)
428 } else {
429 v.text_dim
430 };
431 ctx.set_fill_color(grip);
432 let cx = div_x + DIVIDER_W * 0.5;
433 let cy = content_h * 0.5;
434 for i in -1i32..=1 {
435 ctx.begin_path();
436 ctx.circle(cx, cy + i as f64 * 5.0, 1.5);
437 ctx.fill();
438 }
439 }
440 }
441 }
442
443 fn hit_test(&self, local_pos: Point) -> bool {
444 if self.sidebar_dragging {
446 return true;
447 }
448 local_pos.x >= 0.0
449 && local_pos.x <= self.bounds.width
450 && local_pos.y >= 0.0
451 && local_pos.y <= self.bounds.height
452 }
453
454 fn on_event(&mut self, event: &Event) -> EventResult {
455 match event {
456 Event::MouseMove { pos } => {
457 let was_tab = self.hovered_tab;
458 let was_act = self.action_hovered;
459 self.hovered_tab = self.tab_index_at(*pos);
460 self.action_hovered = self.action_btn_hit(*pos);
461 if self.sidebar_dragging {
462 let new_w = self.bounds.width - pos.x;
464 self.sidebar_w = new_w.clamp(MIN_SIDEBAR_W, self.bounds.width * 0.8);
465 crate::animation::request_draw();
466 return EventResult::Consumed;
467 }
468 if was_tab != self.hovered_tab || was_act != self.action_hovered {
469 crate::animation::request_draw();
470 }
471 EventResult::Ignored
472 }
473 Event::MouseDown {
474 pos,
475 button: MouseButton::Left,
476 ..
477 } => {
478 if self.action_btn_hit(*pos) {
479 self.action_active = !self.action_active;
480 if let Some(ref cb) = self.on_action {
481 cb();
482 }
483 crate::animation::request_draw();
484 return EventResult::Consumed;
485 }
486 if self.sidebar_showing() && pos.y < self.content_height() {
488 let div_x = self.divider_x();
489 if pos.x >= div_x - 2.0 && pos.x <= div_x + DIVIDER_W + 2.0 {
490 self.sidebar_dragging = true;
491 crate::animation::request_draw();
492 return EventResult::Consumed;
493 }
494 }
495 if let Some(i) = self.tab_index_at(*pos) {
496 self.switch_to(i);
497 crate::animation::request_draw();
498 return EventResult::Consumed;
499 }
500 EventResult::Ignored
501 }
502 Event::MouseUp {
503 button: MouseButton::Left,
504 ..
505 } => {
506 if self.sidebar_dragging {
507 self.sidebar_dragging = false;
508 crate::animation::request_draw();
509 return EventResult::Consumed;
510 }
511 EventResult::Ignored
512 }
513 _ => EventResult::Ignored,
514 }
515 }
516}