1use presentar_core::{
4 widget::{
5 AccessibleRole, Brick, BrickAssertion, BrickBudget, BrickVerification, LayoutResult,
6 TextStyle,
7 },
8 Canvas, Color, Constraints, Event, MouseButton, Point, Rect, Size, TypeId, Widget,
9};
10use serde::{Deserialize, Serialize};
11use std::any::Any;
12use std::time::Duration;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Tab {
17 pub id: String,
19 pub label: String,
21 pub disabled: bool,
23 pub icon: Option<String>,
25}
26
27impl Tab {
28 #[must_use]
30 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
31 Self {
32 id: id.into(),
33 label: label.into(),
34 disabled: false,
35 icon: None,
36 }
37 }
38
39 #[must_use]
41 pub const fn disabled(mut self) -> Self {
42 self.disabled = true;
43 self
44 }
45
46 #[must_use]
48 pub fn icon(mut self, icon: impl Into<String>) -> Self {
49 self.icon = Some(icon.into());
50 self
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct TabChanged {
57 pub tab_id: String,
59 pub index: usize,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
65pub enum TabOrientation {
66 #[default]
68 Top,
69 Bottom,
71 Left,
73 Right,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct Tabs {
80 items: Vec<Tab>,
82 active: usize,
84 orientation: TabOrientation,
86 tab_size: f32,
88 min_tab_width: f32,
90 spacing: f32,
92 tab_bg: Color,
94 active_bg: Color,
96 inactive_color: Color,
98 active_color: Color,
100 disabled_color: Color,
102 border_color: Color,
104 show_border: bool,
106 accessible_name_value: Option<String>,
108 test_id_value: Option<String>,
110 #[serde(skip)]
112 bounds: Rect,
113}
114
115impl Default for Tabs {
116 fn default() -> Self {
117 Self {
118 items: Vec::new(),
119 active: 0,
120 orientation: TabOrientation::Top,
121 tab_size: 48.0,
122 min_tab_width: 80.0,
123 spacing: 0.0,
124 tab_bg: Color::new(0.95, 0.95, 0.95, 1.0),
125 active_bg: Color::WHITE,
126 inactive_color: Color::new(0.4, 0.4, 0.4, 1.0),
127 active_color: Color::new(0.2, 0.47, 0.96, 1.0),
128 disabled_color: Color::new(0.7, 0.7, 0.7, 1.0),
129 border_color: Color::new(0.85, 0.85, 0.85, 1.0),
130 show_border: true,
131 accessible_name_value: None,
132 test_id_value: None,
133 bounds: Rect::default(),
134 }
135 }
136}
137
138impl Tabs {
139 #[must_use]
141 pub fn new() -> Self {
142 Self::default()
143 }
144
145 #[must_use]
147 pub fn tab(mut self, tab: Tab) -> Self {
148 self.items.push(tab);
149 self
150 }
151
152 #[must_use]
154 pub fn tabs(mut self, tabs: impl IntoIterator<Item = Tab>) -> Self {
155 self.items.extend(tabs);
156 self
157 }
158
159 #[must_use]
161 pub const fn active(mut self, index: usize) -> Self {
162 self.active = index;
163 self
164 }
165
166 #[must_use]
168 pub fn active_id(mut self, id: &str) -> Self {
169 if let Some(index) = self.items.iter().position(|t| t.id == id) {
170 self.active = index;
171 }
172 self
173 }
174
175 #[must_use]
177 pub const fn orientation(mut self, orientation: TabOrientation) -> Self {
178 self.orientation = orientation;
179 self
180 }
181
182 #[must_use]
184 pub fn tab_size(mut self, size: f32) -> Self {
185 self.tab_size = size.max(24.0);
186 self
187 }
188
189 #[must_use]
191 pub fn min_tab_width(mut self, width: f32) -> Self {
192 self.min_tab_width = width.max(40.0);
193 self
194 }
195
196 #[must_use]
198 pub fn spacing(mut self, spacing: f32) -> Self {
199 self.spacing = spacing.max(0.0);
200 self
201 }
202
203 #[must_use]
205 pub const fn tab_bg(mut self, color: Color) -> Self {
206 self.tab_bg = color;
207 self
208 }
209
210 #[must_use]
212 pub const fn active_bg(mut self, color: Color) -> Self {
213 self.active_bg = color;
214 self
215 }
216
217 #[must_use]
219 pub const fn inactive_color(mut self, color: Color) -> Self {
220 self.inactive_color = color;
221 self
222 }
223
224 #[must_use]
226 pub const fn active_color(mut self, color: Color) -> Self {
227 self.active_color = color;
228 self
229 }
230
231 #[must_use]
233 pub const fn show_border(mut self, show: bool) -> Self {
234 self.show_border = show;
235 self
236 }
237
238 #[must_use]
240 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
241 self.accessible_name_value = Some(name.into());
242 self
243 }
244
245 #[must_use]
247 pub fn test_id(mut self, id: impl Into<String>) -> Self {
248 self.test_id_value = Some(id.into());
249 self
250 }
251
252 #[must_use]
254 pub fn tab_count(&self) -> usize {
255 self.items.len()
256 }
257
258 #[must_use]
260 pub fn get_tabs(&self) -> &[Tab] {
261 &self.items
262 }
263
264 #[must_use]
266 pub const fn get_active(&self) -> usize {
267 self.active
268 }
269
270 #[must_use]
272 pub fn get_active_tab(&self) -> Option<&Tab> {
273 self.items.get(self.active)
274 }
275
276 #[must_use]
278 pub fn get_active_id(&self) -> Option<&str> {
279 self.items.get(self.active).map(|t| t.id.as_str())
280 }
281
282 #[must_use]
284 pub const fn is_active(&self, index: usize) -> bool {
285 self.active == index
286 }
287
288 #[must_use]
290 pub fn is_empty(&self) -> bool {
291 self.items.is_empty()
292 }
293
294 pub fn set_active(&mut self, index: usize) {
296 if index < self.items.len() && !self.items[index].disabled {
297 self.active = index;
298 }
299 }
300
301 pub fn set_active_id(&mut self, id: &str) {
303 if let Some(index) = self.items.iter().position(|t| t.id == id) {
304 if !self.items[index].disabled {
305 self.active = index;
306 }
307 }
308 }
309
310 pub fn next_tab(&mut self) {
312 let mut next = (self.active + 1) % self.items.len();
313 let start = next;
314 loop {
315 if !self.items[next].disabled {
316 self.active = next;
317 return;
318 }
319 next = (next + 1) % self.items.len();
320 if next == start {
321 return; }
323 }
324 }
325
326 pub fn prev_tab(&mut self) {
328 if self.items.is_empty() {
329 return;
330 }
331 let mut prev = if self.active == 0 {
332 self.items.len() - 1
333 } else {
334 self.active - 1
335 };
336 let start = prev;
337 loop {
338 if !self.items[prev].disabled {
339 self.active = prev;
340 return;
341 }
342 prev = if prev == 0 {
343 self.items.len() - 1
344 } else {
345 prev - 1
346 };
347 if prev == start {
348 return; }
350 }
351 }
352
353 fn calculate_tab_width(&self, available_width: f32) -> f32 {
355 if self.items.is_empty() {
356 return self.min_tab_width;
357 }
358 let total_spacing = self.spacing * (self.items.len() - 1).max(0) as f32;
359 let per_tab = (available_width - total_spacing) / self.items.len() as f32;
360 per_tab.max(self.min_tab_width)
361 }
362
363 fn tab_rect(&self, index: usize, tab_width: f32) -> Rect {
365 match self.orientation {
366 TabOrientation::Top | TabOrientation::Bottom => {
367 let x = (index as f32).mul_add(tab_width + self.spacing, self.bounds.x);
368 let y = if self.orientation == TabOrientation::Top {
369 self.bounds.y
370 } else {
371 self.bounds.y + self.bounds.height - self.tab_size
372 };
373 Rect::new(x, y, tab_width, self.tab_size)
374 }
375 TabOrientation::Left | TabOrientation::Right => {
376 let y = (index as f32).mul_add(self.tab_size + self.spacing, self.bounds.y);
377 let x = if self.orientation == TabOrientation::Left {
378 self.bounds.x
379 } else {
380 self.bounds.x + self.bounds.width - self.min_tab_width
381 };
382 Rect::new(x, y, self.min_tab_width, self.tab_size)
383 }
384 }
385 }
386
387 fn tab_at_point(&self, x: f32, y: f32) -> Option<usize> {
389 let tab_width = self.calculate_tab_width(self.bounds.width);
390 for (i, _) in self.items.iter().enumerate() {
391 let rect = self.tab_rect(i, tab_width);
392 if x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height {
393 return Some(i);
394 }
395 }
396 None
397 }
398}
399
400impl Widget for Tabs {
401 fn type_id(&self) -> TypeId {
402 TypeId::of::<Self>()
403 }
404
405 fn measure(&self, constraints: Constraints) -> Size {
406 let is_horizontal = matches!(
407 self.orientation,
408 TabOrientation::Top | TabOrientation::Bottom
409 );
410
411 let preferred = if is_horizontal {
412 Size::new(self.items.len() as f32 * self.min_tab_width, self.tab_size)
413 } else {
414 Size::new(self.min_tab_width, self.items.len() as f32 * self.tab_size)
415 };
416
417 constraints.constrain(preferred)
418 }
419
420 fn layout(&mut self, bounds: Rect) -> LayoutResult {
421 self.bounds = bounds;
422 LayoutResult {
423 size: bounds.size(),
424 }
425 }
426
427 fn paint(&self, canvas: &mut dyn Canvas) {
428 canvas.fill_rect(self.bounds, self.tab_bg);
430
431 let tab_width = self.calculate_tab_width(self.bounds.width);
432
433 for (i, tab) in self.items.iter().enumerate() {
435 let rect = self.tab_rect(i, tab_width);
436
437 let bg_color = if i == self.active {
439 self.active_bg
440 } else {
441 self.tab_bg
442 };
443 canvas.fill_rect(rect, bg_color);
444
445 let text_color = if tab.disabled {
447 self.disabled_color
448 } else if i == self.active {
449 self.active_color
450 } else {
451 self.inactive_color
452 };
453
454 let text_style = TextStyle {
455 size: 14.0,
456 color: text_color,
457 ..TextStyle::default()
458 };
459
460 canvas.draw_text(
461 &tab.label,
462 Point::new(rect.x + 12.0, rect.y + rect.height / 2.0),
463 &text_style,
464 );
465
466 if i == self.active && self.show_border {
468 let indicator_rect = match self.orientation {
469 TabOrientation::Top => {
470 Rect::new(rect.x, rect.y + rect.height - 2.0, rect.width, 2.0)
471 }
472 TabOrientation::Bottom => Rect::new(rect.x, rect.y, rect.width, 2.0),
473 TabOrientation::Left => {
474 Rect::new(rect.x + rect.width - 2.0, rect.y, 2.0, rect.height)
475 }
476 TabOrientation::Right => Rect::new(rect.x, rect.y, 2.0, rect.height),
477 };
478 canvas.fill_rect(indicator_rect, self.active_color);
479 }
480 }
481
482 if self.show_border {
484 let border_rect = match self.orientation {
485 TabOrientation::Top => Rect::new(
486 self.bounds.x,
487 self.bounds.y + self.tab_size - 1.0,
488 self.bounds.width,
489 1.0,
490 ),
491 TabOrientation::Bottom => {
492 Rect::new(self.bounds.x, self.bounds.y, self.bounds.width, 1.0)
493 }
494 TabOrientation::Left => Rect::new(
495 self.bounds.x + self.min_tab_width - 1.0,
496 self.bounds.y,
497 1.0,
498 self.bounds.height,
499 ),
500 TabOrientation::Right => {
501 Rect::new(self.bounds.x, self.bounds.y, 1.0, self.bounds.height)
502 }
503 };
504 canvas.fill_rect(border_rect, self.border_color);
505 }
506 }
507
508 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
509 if let Event::MouseDown {
510 position,
511 button: MouseButton::Left,
512 } = event
513 {
514 if let Some(index) = self.tab_at_point(position.x, position.y) {
515 if !self.items[index].disabled && index != self.active {
516 self.active = index;
517 return Some(Box::new(TabChanged {
518 tab_id: self.items[index].id.clone(),
519 index,
520 }));
521 }
522 }
523 }
524 None
525 }
526
527 fn children(&self) -> &[Box<dyn Widget>] {
528 &[]
529 }
530
531 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
532 &mut []
533 }
534
535 fn is_interactive(&self) -> bool {
536 !self.items.is_empty()
537 }
538
539 fn is_focusable(&self) -> bool {
540 !self.items.is_empty()
541 }
542
543 fn accessible_name(&self) -> Option<&str> {
544 self.accessible_name_value.as_deref()
545 }
546
547 fn accessible_role(&self) -> AccessibleRole {
548 AccessibleRole::Tab
549 }
550
551 fn test_id(&self) -> Option<&str> {
552 self.test_id_value.as_deref()
553 }
554}
555
556impl Brick for Tabs {
558 fn brick_name(&self) -> &'static str {
559 "Tabs"
560 }
561
562 fn assertions(&self) -> &[BrickAssertion] {
563 &[BrickAssertion::MaxLatencyMs(16)]
564 }
565
566 fn budget(&self) -> BrickBudget {
567 BrickBudget::uniform(16)
568 }
569
570 fn verify(&self) -> BrickVerification {
571 BrickVerification {
572 passed: self.assertions().to_vec(),
573 failed: vec![],
574 verification_time: Duration::from_micros(10),
575 }
576 }
577
578 fn to_html(&self) -> String {
579 r#"<div class="brick-tabs"></div>"#.to_string()
580 }
581
582 fn to_css(&self) -> String {
583 ".brick-tabs { display: flex; flex-direction: column; }".to_string()
584 }
585}
586
587#[cfg(test)]
588#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
589mod tests {
590 use super::*;
591
592 #[test]
595 fn test_tab_new() {
596 let tab = Tab::new("home", "Home");
597 assert_eq!(tab.id, "home");
598 assert_eq!(tab.label, "Home");
599 assert!(!tab.disabled);
600 assert!(tab.icon.is_none());
601 }
602
603 #[test]
604 fn test_tab_disabled() {
605 let tab = Tab::new("settings", "Settings").disabled();
606 assert!(tab.disabled);
607 }
608
609 #[test]
610 fn test_tab_icon() {
611 let tab = Tab::new("profile", "Profile").icon("user");
612 assert_eq!(tab.icon, Some("user".to_string()));
613 }
614
615 #[test]
618 fn test_tab_changed() {
619 let msg = TabChanged {
620 tab_id: "settings".to_string(),
621 index: 2,
622 };
623 assert_eq!(msg.tab_id, "settings");
624 assert_eq!(msg.index, 2);
625 }
626
627 #[test]
630 fn test_tab_orientation_default() {
631 assert_eq!(TabOrientation::default(), TabOrientation::Top);
632 }
633
634 #[test]
637 fn test_tabs_new() {
638 let tabs = Tabs::new();
639 assert_eq!(tabs.tab_count(), 0);
640 assert!(tabs.is_empty());
641 }
642
643 #[test]
644 fn test_tabs_builder() {
645 let tabs = Tabs::new()
646 .tab(Tab::new("home", "Home"))
647 .tab(Tab::new("about", "About"))
648 .tab(Tab::new("contact", "Contact"))
649 .active(1)
650 .orientation(TabOrientation::Top)
651 .tab_size(50.0)
652 .min_tab_width(100.0)
653 .spacing(4.0)
654 .show_border(true)
655 .accessible_name("Main navigation")
656 .test_id("main-tabs");
657
658 assert_eq!(tabs.tab_count(), 3);
659 assert_eq!(tabs.get_active(), 1);
660 assert_eq!(tabs.get_active_id(), Some("about"));
661 assert_eq!(Widget::accessible_name(&tabs), Some("Main navigation"));
662 assert_eq!(Widget::test_id(&tabs), Some("main-tabs"));
663 }
664
665 #[test]
666 fn test_tabs_multiple() {
667 let tab_list = vec![Tab::new("a", "A"), Tab::new("b", "B"), Tab::new("c", "C")];
668 let tabs = Tabs::new().tabs(tab_list);
669 assert_eq!(tabs.tab_count(), 3);
670 }
671
672 #[test]
673 fn test_tabs_active_id() {
674 let tabs = Tabs::new()
675 .tab(Tab::new("first", "First"))
676 .tab(Tab::new("second", "Second"))
677 .active_id("second");
678
679 assert_eq!(tabs.get_active(), 1);
680 }
681
682 #[test]
683 fn test_tabs_active_id_not_found() {
684 let tabs = Tabs::new()
685 .tab(Tab::new("first", "First"))
686 .active_id("nonexistent");
687
688 assert_eq!(tabs.get_active(), 0);
689 }
690
691 #[test]
694 fn test_tabs_get_active_tab() {
695 let tabs = Tabs::new()
696 .tab(Tab::new("home", "Home"))
697 .tab(Tab::new("about", "About"))
698 .active(1);
699
700 let active = tabs.get_active_tab().unwrap();
701 assert_eq!(active.id, "about");
702 }
703
704 #[test]
705 fn test_tabs_get_active_tab_empty() {
706 let tabs = Tabs::new();
707 assert!(tabs.get_active_tab().is_none());
708 }
709
710 #[test]
711 fn test_tabs_is_active() {
712 let tabs = Tabs::new()
713 .tab(Tab::new("a", "A"))
714 .tab(Tab::new("b", "B"))
715 .active(1);
716
717 assert!(!tabs.is_active(0));
718 assert!(tabs.is_active(1));
719 }
720
721 #[test]
724 fn test_tabs_set_active() {
725 let mut tabs = Tabs::new()
726 .tab(Tab::new("a", "A"))
727 .tab(Tab::new("b", "B"))
728 .tab(Tab::new("c", "C"));
729
730 tabs.set_active(2);
731 assert_eq!(tabs.get_active(), 2);
732 }
733
734 #[test]
735 fn test_tabs_set_active_out_of_bounds() {
736 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
737
738 tabs.set_active(10);
739 assert_eq!(tabs.get_active(), 0); }
741
742 #[test]
743 fn test_tabs_set_active_disabled() {
744 let mut tabs = Tabs::new()
745 .tab(Tab::new("a", "A"))
746 .tab(Tab::new("b", "B").disabled());
747
748 tabs.set_active(1);
749 assert_eq!(tabs.get_active(), 0); }
751
752 #[test]
753 fn test_tabs_set_active_id() {
754 let mut tabs = Tabs::new()
755 .tab(Tab::new("home", "Home"))
756 .tab(Tab::new("settings", "Settings"));
757
758 tabs.set_active_id("settings");
759 assert_eq!(tabs.get_active(), 1);
760 }
761
762 #[test]
765 fn test_tabs_next_tab() {
766 let mut tabs = Tabs::new()
767 .tab(Tab::new("a", "A"))
768 .tab(Tab::new("b", "B"))
769 .tab(Tab::new("c", "C"))
770 .active(0);
771
772 tabs.next_tab();
773 assert_eq!(tabs.get_active(), 1);
774
775 tabs.next_tab();
776 assert_eq!(tabs.get_active(), 2);
777
778 tabs.next_tab(); assert_eq!(tabs.get_active(), 0);
780 }
781
782 #[test]
783 fn test_tabs_next_tab_skip_disabled() {
784 let mut tabs = Tabs::new()
785 .tab(Tab::new("a", "A"))
786 .tab(Tab::new("b", "B").disabled())
787 .tab(Tab::new("c", "C"))
788 .active(0);
789
790 tabs.next_tab();
791 assert_eq!(tabs.get_active(), 2); }
793
794 #[test]
795 fn test_tabs_prev_tab() {
796 let mut tabs = Tabs::new()
797 .tab(Tab::new("a", "A"))
798 .tab(Tab::new("b", "B"))
799 .tab(Tab::new("c", "C"))
800 .active(2);
801
802 tabs.prev_tab();
803 assert_eq!(tabs.get_active(), 1);
804
805 tabs.prev_tab();
806 assert_eq!(tabs.get_active(), 0);
807
808 tabs.prev_tab(); assert_eq!(tabs.get_active(), 2);
810 }
811
812 #[test]
813 fn test_tabs_prev_tab_skip_disabled() {
814 let mut tabs = Tabs::new()
815 .tab(Tab::new("a", "A"))
816 .tab(Tab::new("b", "B").disabled())
817 .tab(Tab::new("c", "C"))
818 .active(2);
819
820 tabs.prev_tab();
821 assert_eq!(tabs.get_active(), 0); }
823
824 #[test]
827 fn test_tabs_tab_size_min() {
828 let tabs = Tabs::new().tab_size(10.0);
829 assert_eq!(tabs.tab_size, 24.0);
830 }
831
832 #[test]
833 fn test_tabs_min_tab_width_min() {
834 let tabs = Tabs::new().min_tab_width(20.0);
835 assert_eq!(tabs.min_tab_width, 40.0);
836 }
837
838 #[test]
839 fn test_tabs_spacing_min() {
840 let tabs = Tabs::new().spacing(-5.0);
841 assert_eq!(tabs.spacing, 0.0);
842 }
843
844 #[test]
845 fn test_tabs_calculate_tab_width() {
846 let tabs = Tabs::new()
847 .tab(Tab::new("a", "A"))
848 .tab(Tab::new("b", "B"))
849 .min_tab_width(50.0)
850 .spacing(0.0);
851
852 assert_eq!(tabs.calculate_tab_width(200.0), 100.0);
853 }
854
855 #[test]
856 fn test_tabs_calculate_tab_width_with_spacing() {
857 let tabs = Tabs::new()
858 .tab(Tab::new("a", "A"))
859 .tab(Tab::new("b", "B"))
860 .min_tab_width(50.0)
861 .spacing(10.0);
862
863 assert_eq!(tabs.calculate_tab_width(200.0), 95.0);
865 }
866
867 #[test]
870 fn test_tabs_type_id() {
871 let tabs = Tabs::new();
872 assert_eq!(Widget::type_id(&tabs), TypeId::of::<Tabs>());
873 }
874
875 #[test]
876 fn test_tabs_measure_horizontal() {
877 let tabs = Tabs::new()
878 .tab(Tab::new("a", "A"))
879 .tab(Tab::new("b", "B"))
880 .orientation(TabOrientation::Top)
881 .min_tab_width(100.0)
882 .tab_size(48.0);
883
884 let size = tabs.measure(Constraints::loose(Size::new(500.0, 500.0)));
885 assert_eq!(size.width, 200.0);
886 assert_eq!(size.height, 48.0);
887 }
888
889 #[test]
890 fn test_tabs_measure_vertical() {
891 let tabs = Tabs::new()
892 .tab(Tab::new("a", "A"))
893 .tab(Tab::new("b", "B"))
894 .orientation(TabOrientation::Left)
895 .min_tab_width(100.0)
896 .tab_size(48.0);
897
898 let size = tabs.measure(Constraints::loose(Size::new(500.0, 500.0)));
899 assert_eq!(size.width, 100.0);
900 assert_eq!(size.height, 96.0);
901 }
902
903 #[test]
904 fn test_tabs_layout() {
905 let mut tabs = Tabs::new().tab(Tab::new("a", "A"));
906 let bounds = Rect::new(10.0, 20.0, 300.0, 48.0);
907 let result = tabs.layout(bounds);
908 assert_eq!(result.size, Size::new(300.0, 48.0));
909 assert_eq!(tabs.bounds, bounds);
910 }
911
912 #[test]
913 fn test_tabs_children() {
914 let tabs = Tabs::new();
915 assert!(tabs.children().is_empty());
916 }
917
918 #[test]
919 fn test_tabs_is_interactive() {
920 let tabs = Tabs::new();
921 assert!(!tabs.is_interactive()); let tabs = Tabs::new().tab(Tab::new("a", "A"));
924 assert!(tabs.is_interactive());
925 }
926
927 #[test]
928 fn test_tabs_is_focusable() {
929 let tabs = Tabs::new();
930 assert!(!tabs.is_focusable()); let tabs = Tabs::new().tab(Tab::new("a", "A"));
933 assert!(tabs.is_focusable());
934 }
935
936 #[test]
937 fn test_tabs_accessible_role() {
938 let tabs = Tabs::new();
939 assert_eq!(tabs.accessible_role(), AccessibleRole::Tab);
940 }
941
942 #[test]
943 fn test_tabs_accessible_name() {
944 let tabs = Tabs::new().accessible_name("Section tabs");
945 assert_eq!(Widget::accessible_name(&tabs), Some("Section tabs"));
946 }
947
948 #[test]
949 fn test_tabs_test_id() {
950 let tabs = Tabs::new().test_id("nav-tabs");
951 assert_eq!(Widget::test_id(&tabs), Some("nav-tabs"));
952 }
953
954 #[test]
957 fn test_tab_rect_top() {
958 let mut tabs = Tabs::new()
959 .tab(Tab::new("a", "A"))
960 .tab(Tab::new("b", "B"))
961 .orientation(TabOrientation::Top)
962 .tab_size(48.0);
963 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
964
965 let rect0 = tabs.tab_rect(0, 100.0);
966 assert_eq!(rect0.x, 0.0);
967 assert_eq!(rect0.y, 0.0);
968 assert_eq!(rect0.width, 100.0);
969 assert_eq!(rect0.height, 48.0);
970
971 let rect1 = tabs.tab_rect(1, 100.0);
972 assert_eq!(rect1.x, 100.0);
973 }
974
975 #[test]
976 fn test_tab_rect_bottom() {
977 let mut tabs = Tabs::new()
978 .tab(Tab::new("a", "A"))
979 .orientation(TabOrientation::Bottom)
980 .tab_size(48.0);
981 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
982
983 let rect = tabs.tab_rect(0, 100.0);
984 assert_eq!(rect.y, 52.0); }
986
987 #[test]
990 fn test_tabs_click_changes_active() {
991 let mut tabs = Tabs::new()
992 .tab(Tab::new("a", "A"))
993 .tab(Tab::new("b", "B"))
994 .tab_size(48.0)
995 .min_tab_width(100.0);
996 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
997
998 let event = Event::MouseDown {
1000 position: Point::new(150.0, 24.0),
1001 button: MouseButton::Left,
1002 };
1003
1004 let result = tabs.event(&event);
1005 assert!(result.is_some());
1006 assert_eq!(tabs.get_active(), 1);
1007
1008 let msg = result.unwrap().downcast::<TabChanged>().unwrap();
1009 assert_eq!(msg.tab_id, "b");
1010 assert_eq!(msg.index, 1);
1011 }
1012
1013 #[test]
1014 fn test_tabs_click_disabled_no_change() {
1015 let mut tabs = Tabs::new()
1016 .tab(Tab::new("a", "A"))
1017 .tab(Tab::new("b", "B").disabled())
1018 .tab_size(48.0)
1019 .min_tab_width(100.0);
1020 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1021
1022 let event = Event::MouseDown {
1024 position: Point::new(150.0, 24.0),
1025 button: MouseButton::Left,
1026 };
1027
1028 let result = tabs.event(&event);
1029 assert!(result.is_none());
1030 assert_eq!(tabs.get_active(), 0);
1031 }
1032
1033 #[test]
1034 fn test_tabs_click_same_tab_no_event() {
1035 let mut tabs = Tabs::new()
1036 .tab(Tab::new("a", "A"))
1037 .tab(Tab::new("b", "B"))
1038 .active(0)
1039 .tab_size(48.0)
1040 .min_tab_width(100.0);
1041 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1042
1043 let event = Event::MouseDown {
1045 position: Point::new(50.0, 24.0),
1046 button: MouseButton::Left,
1047 };
1048
1049 let result = tabs.event(&event);
1050 assert!(result.is_none());
1051 }
1052
1053 #[test]
1056 fn test_tab_rect_left() {
1057 let mut tabs = Tabs::new()
1058 .tab(Tab::new("a", "A"))
1059 .tab(Tab::new("b", "B"))
1060 .orientation(TabOrientation::Left)
1061 .tab_size(48.0)
1062 .min_tab_width(80.0);
1063 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
1064
1065 let rect0 = tabs.tab_rect(0, 80.0);
1066 assert_eq!(rect0.x, 0.0);
1067 assert_eq!(rect0.y, 0.0);
1068 assert_eq!(rect0.width, 80.0);
1069 assert_eq!(rect0.height, 48.0);
1070
1071 let rect1 = tabs.tab_rect(1, 80.0);
1072 assert_eq!(rect1.y, 48.0);
1073 }
1074
1075 #[test]
1076 fn test_tab_rect_right() {
1077 let mut tabs = Tabs::new()
1078 .tab(Tab::new("a", "A"))
1079 .orientation(TabOrientation::Right)
1080 .tab_size(48.0)
1081 .min_tab_width(80.0);
1082 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
1083
1084 let rect = tabs.tab_rect(0, 80.0);
1085 assert_eq!(rect.x, 120.0); }
1087
1088 #[test]
1089 fn test_tab_rect_with_spacing() {
1090 let mut tabs = Tabs::new()
1091 .tab(Tab::new("a", "A"))
1092 .tab(Tab::new("b", "B"))
1093 .orientation(TabOrientation::Top)
1094 .spacing(10.0)
1095 .tab_size(48.0);
1096 tabs.bounds = Rect::new(0.0, 0.0, 300.0, 48.0);
1097
1098 let rect0 = tabs.tab_rect(0, 100.0);
1099 assert_eq!(rect0.x, 0.0);
1100
1101 let rect1 = tabs.tab_rect(1, 100.0);
1102 assert_eq!(rect1.x, 110.0); }
1104
1105 #[test]
1108 fn test_calculate_tab_width_empty() {
1109 let tabs = Tabs::new().min_tab_width(80.0);
1110 assert_eq!(tabs.calculate_tab_width(500.0), 80.0);
1111 }
1112
1113 #[test]
1114 fn test_calculate_tab_width_narrow_space() {
1115 let tabs = Tabs::new()
1116 .tab(Tab::new("a", "A"))
1117 .tab(Tab::new("b", "B"))
1118 .min_tab_width(100.0);
1119
1120 assert_eq!(tabs.calculate_tab_width(50.0), 100.0);
1122 }
1123
1124 #[test]
1127 fn test_tab_at_point_found() {
1128 let mut tabs = Tabs::new()
1129 .tab(Tab::new("a", "A"))
1130 .tab(Tab::new("b", "B"))
1131 .min_tab_width(100.0)
1132 .tab_size(48.0);
1133 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1134
1135 assert_eq!(tabs.tab_at_point(50.0, 24.0), Some(0));
1136 assert_eq!(tabs.tab_at_point(150.0, 24.0), Some(1));
1137 }
1138
1139 #[test]
1140 fn test_tab_at_point_not_found() {
1141 let mut tabs = Tabs::new()
1142 .tab(Tab::new("a", "A"))
1143 .min_tab_width(100.0)
1144 .tab_size(48.0);
1145 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1146
1147 assert_eq!(tabs.tab_at_point(50.0, 100.0), None);
1149 assert_eq!(tabs.tab_at_point(-10.0, 24.0), None);
1150 }
1151
1152 #[test]
1155 fn test_tabs_next_tab_all_disabled() {
1156 let mut tabs = Tabs::new()
1157 .tab(Tab::new("a", "A").disabled())
1158 .tab(Tab::new("b", "B").disabled())
1159 .tab(Tab::new("c", "C").disabled());
1160 tabs.active = 0; tabs.next_tab();
1163 assert_eq!(tabs.get_active(), 0); }
1165
1166 #[test]
1167 fn test_tabs_prev_tab_all_disabled() {
1168 let mut tabs = Tabs::new()
1169 .tab(Tab::new("a", "A").disabled())
1170 .tab(Tab::new("b", "B").disabled());
1171 tabs.active = 0;
1172
1173 tabs.prev_tab();
1174 assert_eq!(tabs.get_active(), 0); }
1176
1177 #[test]
1178 fn test_tabs_prev_tab_empty() {
1179 let mut tabs = Tabs::new();
1180 tabs.prev_tab(); assert_eq!(tabs.get_active(), 0);
1182 }
1183
1184 #[test]
1185 fn test_tabs_set_active_id_not_found() {
1186 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
1187
1188 tabs.set_active_id("nonexistent");
1189 assert_eq!(tabs.get_active(), 0); }
1191
1192 #[test]
1193 fn test_tabs_set_active_id_disabled() {
1194 let mut tabs = Tabs::new()
1195 .tab(Tab::new("a", "A"))
1196 .tab(Tab::new("b", "B").disabled());
1197
1198 tabs.set_active_id("b");
1199 assert_eq!(tabs.get_active(), 0); }
1201
1202 #[test]
1205 fn test_tabs_tab_bg() {
1206 let tabs = Tabs::new().tab_bg(Color::RED);
1207 assert_eq!(tabs.tab_bg, Color::RED);
1208 }
1209
1210 #[test]
1211 fn test_tabs_active_bg() {
1212 let tabs = Tabs::new().active_bg(Color::BLUE);
1213 assert_eq!(tabs.active_bg, Color::BLUE);
1214 }
1215
1216 #[test]
1217 fn test_tabs_inactive_color() {
1218 let tabs = Tabs::new().inactive_color(Color::GREEN);
1219 assert_eq!(tabs.inactive_color, Color::GREEN);
1220 }
1221
1222 #[test]
1223 fn test_tabs_active_color() {
1224 let tabs = Tabs::new().active_color(Color::WHITE);
1225 assert_eq!(tabs.active_color, Color::WHITE);
1226 }
1227
1228 #[test]
1231 fn test_tabs_get_tabs() {
1232 let tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
1233
1234 let items = tabs.get_tabs();
1235 assert_eq!(items.len(), 2);
1236 assert_eq!(items[0].id, "a");
1237 assert_eq!(items[1].id, "b");
1238 }
1239
1240 #[test]
1241 fn test_tabs_get_active_id_none() {
1242 let tabs = Tabs::new();
1243 assert!(tabs.get_active_id().is_none());
1244 }
1245
1246 #[test]
1249 fn test_tabs_paint() {
1250 use presentar_core::RecordingCanvas;
1251
1252 let mut tabs = Tabs::new()
1253 .tab(Tab::new("a", "A"))
1254 .tab(Tab::new("b", "B"))
1255 .orientation(TabOrientation::Top)
1256 .show_border(true);
1257 tabs.layout(Rect::new(0.0, 0.0, 200.0, 48.0));
1258
1259 let mut canvas = RecordingCanvas::new();
1260 tabs.paint(&mut canvas);
1261
1262 assert!(canvas.command_count() >= 5);
1264 }
1265
1266 #[test]
1267 fn test_tabs_paint_no_border() {
1268 use presentar_core::RecordingCanvas;
1269
1270 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).show_border(false);
1271 tabs.layout(Rect::new(0.0, 0.0, 200.0, 48.0));
1272
1273 let mut canvas = RecordingCanvas::new();
1274 tabs.paint(&mut canvas);
1275
1276 assert!(canvas.command_count() >= 2);
1278 }
1279
1280 #[test]
1281 fn test_tabs_paint_disabled_tab() {
1282 use presentar_core::RecordingCanvas;
1283
1284 let mut tabs = Tabs::new()
1285 .tab(Tab::new("a", "A"))
1286 .tab(Tab::new("b", "B").disabled())
1287 .active(0);
1288 tabs.layout(Rect::new(0.0, 0.0, 200.0, 48.0));
1289
1290 let mut canvas = RecordingCanvas::new();
1291 tabs.paint(&mut canvas);
1292
1293 assert!(canvas.command_count() >= 4);
1295 }
1296
1297 #[test]
1298 fn test_tabs_paint_vertical_left() {
1299 use presentar_core::RecordingCanvas;
1300
1301 let mut tabs = Tabs::new()
1302 .tab(Tab::new("a", "A"))
1303 .tab(Tab::new("b", "B"))
1304 .orientation(TabOrientation::Left)
1305 .show_border(true);
1306 tabs.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
1307
1308 let mut canvas = RecordingCanvas::new();
1309 tabs.paint(&mut canvas);
1310
1311 assert!(canvas.command_count() >= 5);
1312 }
1313
1314 #[test]
1315 fn test_tabs_paint_vertical_right() {
1316 use presentar_core::RecordingCanvas;
1317
1318 let mut tabs = Tabs::new()
1319 .tab(Tab::new("a", "A"))
1320 .tab(Tab::new("b", "B"))
1321 .orientation(TabOrientation::Right)
1322 .show_border(true);
1323 tabs.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
1324
1325 let mut canvas = RecordingCanvas::new();
1326 tabs.paint(&mut canvas);
1327
1328 assert!(canvas.command_count() >= 5);
1329 }
1330
1331 #[test]
1332 fn test_tabs_paint_bottom() {
1333 use presentar_core::RecordingCanvas;
1334
1335 let mut tabs = Tabs::new()
1336 .tab(Tab::new("a", "A"))
1337 .tab(Tab::new("b", "B"))
1338 .orientation(TabOrientation::Bottom)
1339 .show_border(true);
1340 tabs.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
1341
1342 let mut canvas = RecordingCanvas::new();
1343 tabs.paint(&mut canvas);
1344
1345 assert!(canvas.command_count() >= 5);
1346 }
1347
1348 #[test]
1351 fn test_tabs_brick_name() {
1352 let tabs = Tabs::new();
1353 assert_eq!(tabs.brick_name(), "Tabs");
1354 }
1355
1356 #[test]
1357 fn test_tabs_brick_assertions() {
1358 let tabs = Tabs::new();
1359 let assertions = tabs.assertions();
1360 assert_eq!(assertions.len(), 1);
1361 assert!(assertions.contains(&BrickAssertion::MaxLatencyMs(16)));
1362 }
1363
1364 #[test]
1365 fn test_tabs_brick_budget() {
1366 let tabs = Tabs::new();
1367 let budget = tabs.budget();
1368 assert!(budget.measure_ms > 0);
1370 assert!(budget.layout_ms > 0);
1371 assert!(budget.paint_ms > 0);
1372 }
1373
1374 #[test]
1375 fn test_tabs_brick_verify() {
1376 let tabs = Tabs::new();
1377 let verification = tabs.verify();
1378 assert!(verification.failed.is_empty());
1379 assert!(!verification.passed.is_empty());
1380 }
1381
1382 #[test]
1383 fn test_tabs_to_html() {
1384 let tabs = Tabs::new();
1385 let html = tabs.to_html();
1386 assert!(html.contains("brick-tabs"));
1387 }
1388
1389 #[test]
1390 fn test_tabs_to_css() {
1391 let tabs = Tabs::new();
1392 let css = tabs.to_css();
1393 assert!(css.contains("brick-tabs"));
1394 assert!(css.contains("display: flex"));
1395 }
1396
1397 #[test]
1400 fn test_tabs_children_mut() {
1401 let mut tabs = Tabs::new();
1402 assert!(tabs.children_mut().is_empty());
1403 }
1404
1405 #[test]
1408 fn test_tabs_click_outside() {
1409 let mut tabs = Tabs::new()
1410 .tab(Tab::new("a", "A"))
1411 .min_tab_width(100.0)
1412 .tab_size(48.0);
1413 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1414
1415 let event = Event::MouseDown {
1417 position: Point::new(500.0, 24.0),
1418 button: MouseButton::Left,
1419 };
1420
1421 let result = tabs.event(&event);
1422 assert!(result.is_none());
1423 }
1424
1425 #[test]
1426 fn test_tabs_right_click_no_event() {
1427 let mut tabs = Tabs::new()
1428 .tab(Tab::new("a", "A"))
1429 .tab(Tab::new("b", "B"))
1430 .min_tab_width(100.0)
1431 .tab_size(48.0);
1432 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1433
1434 let event = Event::MouseDown {
1436 position: Point::new(150.0, 24.0),
1437 button: MouseButton::Right,
1438 };
1439
1440 let result = tabs.event(&event);
1441 assert!(result.is_none());
1442 }
1443
1444 #[test]
1445 fn test_tabs_other_event_no_effect() {
1446 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
1447 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1448
1449 let result = tabs.event(&Event::MouseEnter);
1450 assert!(result.is_none());
1451 }
1452}