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)]
588mod tests {
589 use super::*;
590
591 #[test]
594 fn test_tab_new() {
595 let tab = Tab::new("home", "Home");
596 assert_eq!(tab.id, "home");
597 assert_eq!(tab.label, "Home");
598 assert!(!tab.disabled);
599 assert!(tab.icon.is_none());
600 }
601
602 #[test]
603 fn test_tab_disabled() {
604 let tab = Tab::new("settings", "Settings").disabled();
605 assert!(tab.disabled);
606 }
607
608 #[test]
609 fn test_tab_icon() {
610 let tab = Tab::new("profile", "Profile").icon("user");
611 assert_eq!(tab.icon, Some("user".to_string()));
612 }
613
614 #[test]
617 fn test_tab_changed() {
618 let msg = TabChanged {
619 tab_id: "settings".to_string(),
620 index: 2,
621 };
622 assert_eq!(msg.tab_id, "settings");
623 assert_eq!(msg.index, 2);
624 }
625
626 #[test]
629 fn test_tab_orientation_default() {
630 assert_eq!(TabOrientation::default(), TabOrientation::Top);
631 }
632
633 #[test]
636 fn test_tabs_new() {
637 let tabs = Tabs::new();
638 assert_eq!(tabs.tab_count(), 0);
639 assert!(tabs.is_empty());
640 }
641
642 #[test]
643 fn test_tabs_builder() {
644 let tabs = Tabs::new()
645 .tab(Tab::new("home", "Home"))
646 .tab(Tab::new("about", "About"))
647 .tab(Tab::new("contact", "Contact"))
648 .active(1)
649 .orientation(TabOrientation::Top)
650 .tab_size(50.0)
651 .min_tab_width(100.0)
652 .spacing(4.0)
653 .show_border(true)
654 .accessible_name("Main navigation")
655 .test_id("main-tabs");
656
657 assert_eq!(tabs.tab_count(), 3);
658 assert_eq!(tabs.get_active(), 1);
659 assert_eq!(tabs.get_active_id(), Some("about"));
660 assert_eq!(Widget::accessible_name(&tabs), Some("Main navigation"));
661 assert_eq!(Widget::test_id(&tabs), Some("main-tabs"));
662 }
663
664 #[test]
665 fn test_tabs_multiple() {
666 let tab_list = vec![Tab::new("a", "A"), Tab::new("b", "B"), Tab::new("c", "C")];
667 let tabs = Tabs::new().tabs(tab_list);
668 assert_eq!(tabs.tab_count(), 3);
669 }
670
671 #[test]
672 fn test_tabs_active_id() {
673 let tabs = Tabs::new()
674 .tab(Tab::new("first", "First"))
675 .tab(Tab::new("second", "Second"))
676 .active_id("second");
677
678 assert_eq!(tabs.get_active(), 1);
679 }
680
681 #[test]
682 fn test_tabs_active_id_not_found() {
683 let tabs = Tabs::new()
684 .tab(Tab::new("first", "First"))
685 .active_id("nonexistent");
686
687 assert_eq!(tabs.get_active(), 0);
688 }
689
690 #[test]
693 fn test_tabs_get_active_tab() {
694 let tabs = Tabs::new()
695 .tab(Tab::new("home", "Home"))
696 .tab(Tab::new("about", "About"))
697 .active(1);
698
699 let active = tabs.get_active_tab().unwrap();
700 assert_eq!(active.id, "about");
701 }
702
703 #[test]
704 fn test_tabs_get_active_tab_empty() {
705 let tabs = Tabs::new();
706 assert!(tabs.get_active_tab().is_none());
707 }
708
709 #[test]
710 fn test_tabs_is_active() {
711 let tabs = Tabs::new()
712 .tab(Tab::new("a", "A"))
713 .tab(Tab::new("b", "B"))
714 .active(1);
715
716 assert!(!tabs.is_active(0));
717 assert!(tabs.is_active(1));
718 }
719
720 #[test]
723 fn test_tabs_set_active() {
724 let mut tabs = Tabs::new()
725 .tab(Tab::new("a", "A"))
726 .tab(Tab::new("b", "B"))
727 .tab(Tab::new("c", "C"));
728
729 tabs.set_active(2);
730 assert_eq!(tabs.get_active(), 2);
731 }
732
733 #[test]
734 fn test_tabs_set_active_out_of_bounds() {
735 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
736
737 tabs.set_active(10);
738 assert_eq!(tabs.get_active(), 0); }
740
741 #[test]
742 fn test_tabs_set_active_disabled() {
743 let mut tabs = Tabs::new()
744 .tab(Tab::new("a", "A"))
745 .tab(Tab::new("b", "B").disabled());
746
747 tabs.set_active(1);
748 assert_eq!(tabs.get_active(), 0); }
750
751 #[test]
752 fn test_tabs_set_active_id() {
753 let mut tabs = Tabs::new()
754 .tab(Tab::new("home", "Home"))
755 .tab(Tab::new("settings", "Settings"));
756
757 tabs.set_active_id("settings");
758 assert_eq!(tabs.get_active(), 1);
759 }
760
761 #[test]
764 fn test_tabs_next_tab() {
765 let mut tabs = Tabs::new()
766 .tab(Tab::new("a", "A"))
767 .tab(Tab::new("b", "B"))
768 .tab(Tab::new("c", "C"))
769 .active(0);
770
771 tabs.next_tab();
772 assert_eq!(tabs.get_active(), 1);
773
774 tabs.next_tab();
775 assert_eq!(tabs.get_active(), 2);
776
777 tabs.next_tab(); assert_eq!(tabs.get_active(), 0);
779 }
780
781 #[test]
782 fn test_tabs_next_tab_skip_disabled() {
783 let mut tabs = Tabs::new()
784 .tab(Tab::new("a", "A"))
785 .tab(Tab::new("b", "B").disabled())
786 .tab(Tab::new("c", "C"))
787 .active(0);
788
789 tabs.next_tab();
790 assert_eq!(tabs.get_active(), 2); }
792
793 #[test]
794 fn test_tabs_prev_tab() {
795 let mut tabs = Tabs::new()
796 .tab(Tab::new("a", "A"))
797 .tab(Tab::new("b", "B"))
798 .tab(Tab::new("c", "C"))
799 .active(2);
800
801 tabs.prev_tab();
802 assert_eq!(tabs.get_active(), 1);
803
804 tabs.prev_tab();
805 assert_eq!(tabs.get_active(), 0);
806
807 tabs.prev_tab(); assert_eq!(tabs.get_active(), 2);
809 }
810
811 #[test]
812 fn test_tabs_prev_tab_skip_disabled() {
813 let mut tabs = Tabs::new()
814 .tab(Tab::new("a", "A"))
815 .tab(Tab::new("b", "B").disabled())
816 .tab(Tab::new("c", "C"))
817 .active(2);
818
819 tabs.prev_tab();
820 assert_eq!(tabs.get_active(), 0); }
822
823 #[test]
826 fn test_tabs_tab_size_min() {
827 let tabs = Tabs::new().tab_size(10.0);
828 assert_eq!(tabs.tab_size, 24.0);
829 }
830
831 #[test]
832 fn test_tabs_min_tab_width_min() {
833 let tabs = Tabs::new().min_tab_width(20.0);
834 assert_eq!(tabs.min_tab_width, 40.0);
835 }
836
837 #[test]
838 fn test_tabs_spacing_min() {
839 let tabs = Tabs::new().spacing(-5.0);
840 assert_eq!(tabs.spacing, 0.0);
841 }
842
843 #[test]
844 fn test_tabs_calculate_tab_width() {
845 let tabs = Tabs::new()
846 .tab(Tab::new("a", "A"))
847 .tab(Tab::new("b", "B"))
848 .min_tab_width(50.0)
849 .spacing(0.0);
850
851 assert_eq!(tabs.calculate_tab_width(200.0), 100.0);
852 }
853
854 #[test]
855 fn test_tabs_calculate_tab_width_with_spacing() {
856 let tabs = Tabs::new()
857 .tab(Tab::new("a", "A"))
858 .tab(Tab::new("b", "B"))
859 .min_tab_width(50.0)
860 .spacing(10.0);
861
862 assert_eq!(tabs.calculate_tab_width(200.0), 95.0);
864 }
865
866 #[test]
869 fn test_tabs_type_id() {
870 let tabs = Tabs::new();
871 assert_eq!(Widget::type_id(&tabs), TypeId::of::<Tabs>());
872 }
873
874 #[test]
875 fn test_tabs_measure_horizontal() {
876 let tabs = Tabs::new()
877 .tab(Tab::new("a", "A"))
878 .tab(Tab::new("b", "B"))
879 .orientation(TabOrientation::Top)
880 .min_tab_width(100.0)
881 .tab_size(48.0);
882
883 let size = tabs.measure(Constraints::loose(Size::new(500.0, 500.0)));
884 assert_eq!(size.width, 200.0);
885 assert_eq!(size.height, 48.0);
886 }
887
888 #[test]
889 fn test_tabs_measure_vertical() {
890 let tabs = Tabs::new()
891 .tab(Tab::new("a", "A"))
892 .tab(Tab::new("b", "B"))
893 .orientation(TabOrientation::Left)
894 .min_tab_width(100.0)
895 .tab_size(48.0);
896
897 let size = tabs.measure(Constraints::loose(Size::new(500.0, 500.0)));
898 assert_eq!(size.width, 100.0);
899 assert_eq!(size.height, 96.0);
900 }
901
902 #[test]
903 fn test_tabs_layout() {
904 let mut tabs = Tabs::new().tab(Tab::new("a", "A"));
905 let bounds = Rect::new(10.0, 20.0, 300.0, 48.0);
906 let result = tabs.layout(bounds);
907 assert_eq!(result.size, Size::new(300.0, 48.0));
908 assert_eq!(tabs.bounds, bounds);
909 }
910
911 #[test]
912 fn test_tabs_children() {
913 let tabs = Tabs::new();
914 assert!(tabs.children().is_empty());
915 }
916
917 #[test]
918 fn test_tabs_is_interactive() {
919 let tabs = Tabs::new();
920 assert!(!tabs.is_interactive()); let tabs = Tabs::new().tab(Tab::new("a", "A"));
923 assert!(tabs.is_interactive());
924 }
925
926 #[test]
927 fn test_tabs_is_focusable() {
928 let tabs = Tabs::new();
929 assert!(!tabs.is_focusable()); let tabs = Tabs::new().tab(Tab::new("a", "A"));
932 assert!(tabs.is_focusable());
933 }
934
935 #[test]
936 fn test_tabs_accessible_role() {
937 let tabs = Tabs::new();
938 assert_eq!(tabs.accessible_role(), AccessibleRole::Tab);
939 }
940
941 #[test]
942 fn test_tabs_accessible_name() {
943 let tabs = Tabs::new().accessible_name("Section tabs");
944 assert_eq!(Widget::accessible_name(&tabs), Some("Section tabs"));
945 }
946
947 #[test]
948 fn test_tabs_test_id() {
949 let tabs = Tabs::new().test_id("nav-tabs");
950 assert_eq!(Widget::test_id(&tabs), Some("nav-tabs"));
951 }
952
953 #[test]
956 fn test_tab_rect_top() {
957 let mut tabs = Tabs::new()
958 .tab(Tab::new("a", "A"))
959 .tab(Tab::new("b", "B"))
960 .orientation(TabOrientation::Top)
961 .tab_size(48.0);
962 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
963
964 let rect0 = tabs.tab_rect(0, 100.0);
965 assert_eq!(rect0.x, 0.0);
966 assert_eq!(rect0.y, 0.0);
967 assert_eq!(rect0.width, 100.0);
968 assert_eq!(rect0.height, 48.0);
969
970 let rect1 = tabs.tab_rect(1, 100.0);
971 assert_eq!(rect1.x, 100.0);
972 }
973
974 #[test]
975 fn test_tab_rect_bottom() {
976 let mut tabs = Tabs::new()
977 .tab(Tab::new("a", "A"))
978 .orientation(TabOrientation::Bottom)
979 .tab_size(48.0);
980 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
981
982 let rect = tabs.tab_rect(0, 100.0);
983 assert_eq!(rect.y, 52.0); }
985
986 #[test]
989 fn test_tabs_click_changes_active() {
990 let mut tabs = Tabs::new()
991 .tab(Tab::new("a", "A"))
992 .tab(Tab::new("b", "B"))
993 .tab_size(48.0)
994 .min_tab_width(100.0);
995 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
996
997 let event = Event::MouseDown {
999 position: Point::new(150.0, 24.0),
1000 button: MouseButton::Left,
1001 };
1002
1003 let result = tabs.event(&event);
1004 assert!(result.is_some());
1005 assert_eq!(tabs.get_active(), 1);
1006
1007 let msg = result.unwrap().downcast::<TabChanged>().unwrap();
1008 assert_eq!(msg.tab_id, "b");
1009 assert_eq!(msg.index, 1);
1010 }
1011
1012 #[test]
1013 fn test_tabs_click_disabled_no_change() {
1014 let mut tabs = Tabs::new()
1015 .tab(Tab::new("a", "A"))
1016 .tab(Tab::new("b", "B").disabled())
1017 .tab_size(48.0)
1018 .min_tab_width(100.0);
1019 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1020
1021 let event = Event::MouseDown {
1023 position: Point::new(150.0, 24.0),
1024 button: MouseButton::Left,
1025 };
1026
1027 let result = tabs.event(&event);
1028 assert!(result.is_none());
1029 assert_eq!(tabs.get_active(), 0);
1030 }
1031
1032 #[test]
1033 fn test_tabs_click_same_tab_no_event() {
1034 let mut tabs = Tabs::new()
1035 .tab(Tab::new("a", "A"))
1036 .tab(Tab::new("b", "B"))
1037 .active(0)
1038 .tab_size(48.0)
1039 .min_tab_width(100.0);
1040 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1041
1042 let event = Event::MouseDown {
1044 position: Point::new(50.0, 24.0),
1045 button: MouseButton::Left,
1046 };
1047
1048 let result = tabs.event(&event);
1049 assert!(result.is_none());
1050 }
1051
1052 #[test]
1055 fn test_tab_rect_left() {
1056 let mut tabs = Tabs::new()
1057 .tab(Tab::new("a", "A"))
1058 .tab(Tab::new("b", "B"))
1059 .orientation(TabOrientation::Left)
1060 .tab_size(48.0)
1061 .min_tab_width(80.0);
1062 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
1063
1064 let rect0 = tabs.tab_rect(0, 80.0);
1065 assert_eq!(rect0.x, 0.0);
1066 assert_eq!(rect0.y, 0.0);
1067 assert_eq!(rect0.width, 80.0);
1068 assert_eq!(rect0.height, 48.0);
1069
1070 let rect1 = tabs.tab_rect(1, 80.0);
1071 assert_eq!(rect1.y, 48.0);
1072 }
1073
1074 #[test]
1075 fn test_tab_rect_right() {
1076 let mut tabs = Tabs::new()
1077 .tab(Tab::new("a", "A"))
1078 .orientation(TabOrientation::Right)
1079 .tab_size(48.0)
1080 .min_tab_width(80.0);
1081 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
1082
1083 let rect = tabs.tab_rect(0, 80.0);
1084 assert_eq!(rect.x, 120.0); }
1086
1087 #[test]
1088 fn test_tab_rect_with_spacing() {
1089 let mut tabs = Tabs::new()
1090 .tab(Tab::new("a", "A"))
1091 .tab(Tab::new("b", "B"))
1092 .orientation(TabOrientation::Top)
1093 .spacing(10.0)
1094 .tab_size(48.0);
1095 tabs.bounds = Rect::new(0.0, 0.0, 300.0, 48.0);
1096
1097 let rect0 = tabs.tab_rect(0, 100.0);
1098 assert_eq!(rect0.x, 0.0);
1099
1100 let rect1 = tabs.tab_rect(1, 100.0);
1101 assert_eq!(rect1.x, 110.0); }
1103
1104 #[test]
1107 fn test_calculate_tab_width_empty() {
1108 let tabs = Tabs::new().min_tab_width(80.0);
1109 assert_eq!(tabs.calculate_tab_width(500.0), 80.0);
1110 }
1111
1112 #[test]
1113 fn test_calculate_tab_width_narrow_space() {
1114 let tabs = Tabs::new()
1115 .tab(Tab::new("a", "A"))
1116 .tab(Tab::new("b", "B"))
1117 .min_tab_width(100.0);
1118
1119 assert_eq!(tabs.calculate_tab_width(50.0), 100.0);
1121 }
1122
1123 #[test]
1126 fn test_tab_at_point_found() {
1127 let mut tabs = Tabs::new()
1128 .tab(Tab::new("a", "A"))
1129 .tab(Tab::new("b", "B"))
1130 .min_tab_width(100.0)
1131 .tab_size(48.0);
1132 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1133
1134 assert_eq!(tabs.tab_at_point(50.0, 24.0), Some(0));
1135 assert_eq!(tabs.tab_at_point(150.0, 24.0), Some(1));
1136 }
1137
1138 #[test]
1139 fn test_tab_at_point_not_found() {
1140 let mut tabs = Tabs::new()
1141 .tab(Tab::new("a", "A"))
1142 .min_tab_width(100.0)
1143 .tab_size(48.0);
1144 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1145
1146 assert_eq!(tabs.tab_at_point(50.0, 100.0), None);
1148 assert_eq!(tabs.tab_at_point(-10.0, 24.0), None);
1149 }
1150
1151 #[test]
1154 fn test_tabs_next_tab_all_disabled() {
1155 let mut tabs = Tabs::new()
1156 .tab(Tab::new("a", "A").disabled())
1157 .tab(Tab::new("b", "B").disabled())
1158 .tab(Tab::new("c", "C").disabled());
1159 tabs.active = 0; tabs.next_tab();
1162 assert_eq!(tabs.get_active(), 0); }
1164
1165 #[test]
1166 fn test_tabs_prev_tab_all_disabled() {
1167 let mut tabs = Tabs::new()
1168 .tab(Tab::new("a", "A").disabled())
1169 .tab(Tab::new("b", "B").disabled());
1170 tabs.active = 0;
1171
1172 tabs.prev_tab();
1173 assert_eq!(tabs.get_active(), 0); }
1175
1176 #[test]
1177 fn test_tabs_prev_tab_empty() {
1178 let mut tabs = Tabs::new();
1179 tabs.prev_tab(); assert_eq!(tabs.get_active(), 0);
1181 }
1182
1183 #[test]
1184 fn test_tabs_set_active_id_not_found() {
1185 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
1186
1187 tabs.set_active_id("nonexistent");
1188 assert_eq!(tabs.get_active(), 0); }
1190
1191 #[test]
1192 fn test_tabs_set_active_id_disabled() {
1193 let mut tabs = Tabs::new()
1194 .tab(Tab::new("a", "A"))
1195 .tab(Tab::new("b", "B").disabled());
1196
1197 tabs.set_active_id("b");
1198 assert_eq!(tabs.get_active(), 0); }
1200
1201 #[test]
1204 fn test_tabs_tab_bg() {
1205 let tabs = Tabs::new().tab_bg(Color::RED);
1206 assert_eq!(tabs.tab_bg, Color::RED);
1207 }
1208
1209 #[test]
1210 fn test_tabs_active_bg() {
1211 let tabs = Tabs::new().active_bg(Color::BLUE);
1212 assert_eq!(tabs.active_bg, Color::BLUE);
1213 }
1214
1215 #[test]
1216 fn test_tabs_inactive_color() {
1217 let tabs = Tabs::new().inactive_color(Color::GREEN);
1218 assert_eq!(tabs.inactive_color, Color::GREEN);
1219 }
1220
1221 #[test]
1222 fn test_tabs_active_color() {
1223 let tabs = Tabs::new().active_color(Color::WHITE);
1224 assert_eq!(tabs.active_color, Color::WHITE);
1225 }
1226
1227 #[test]
1230 fn test_tabs_get_tabs() {
1231 let tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
1232
1233 let items = tabs.get_tabs();
1234 assert_eq!(items.len(), 2);
1235 assert_eq!(items[0].id, "a");
1236 assert_eq!(items[1].id, "b");
1237 }
1238
1239 #[test]
1240 fn test_tabs_get_active_id_none() {
1241 let tabs = Tabs::new();
1242 assert!(tabs.get_active_id().is_none());
1243 }
1244
1245 #[test]
1248 fn test_tabs_paint() {
1249 use presentar_core::RecordingCanvas;
1250
1251 let mut tabs = Tabs::new()
1252 .tab(Tab::new("a", "A"))
1253 .tab(Tab::new("b", "B"))
1254 .orientation(TabOrientation::Top)
1255 .show_border(true);
1256 tabs.layout(Rect::new(0.0, 0.0, 200.0, 48.0));
1257
1258 let mut canvas = RecordingCanvas::new();
1259 tabs.paint(&mut canvas);
1260
1261 assert!(canvas.command_count() >= 5);
1263 }
1264
1265 #[test]
1266 fn test_tabs_paint_no_border() {
1267 use presentar_core::RecordingCanvas;
1268
1269 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).show_border(false);
1270 tabs.layout(Rect::new(0.0, 0.0, 200.0, 48.0));
1271
1272 let mut canvas = RecordingCanvas::new();
1273 tabs.paint(&mut canvas);
1274
1275 assert!(canvas.command_count() >= 2);
1277 }
1278
1279 #[test]
1280 fn test_tabs_paint_disabled_tab() {
1281 use presentar_core::RecordingCanvas;
1282
1283 let mut tabs = Tabs::new()
1284 .tab(Tab::new("a", "A"))
1285 .tab(Tab::new("b", "B").disabled())
1286 .active(0);
1287 tabs.layout(Rect::new(0.0, 0.0, 200.0, 48.0));
1288
1289 let mut canvas = RecordingCanvas::new();
1290 tabs.paint(&mut canvas);
1291
1292 assert!(canvas.command_count() >= 4);
1294 }
1295
1296 #[test]
1297 fn test_tabs_paint_vertical_left() {
1298 use presentar_core::RecordingCanvas;
1299
1300 let mut tabs = Tabs::new()
1301 .tab(Tab::new("a", "A"))
1302 .tab(Tab::new("b", "B"))
1303 .orientation(TabOrientation::Left)
1304 .show_border(true);
1305 tabs.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
1306
1307 let mut canvas = RecordingCanvas::new();
1308 tabs.paint(&mut canvas);
1309
1310 assert!(canvas.command_count() >= 5);
1311 }
1312
1313 #[test]
1314 fn test_tabs_paint_vertical_right() {
1315 use presentar_core::RecordingCanvas;
1316
1317 let mut tabs = Tabs::new()
1318 .tab(Tab::new("a", "A"))
1319 .tab(Tab::new("b", "B"))
1320 .orientation(TabOrientation::Right)
1321 .show_border(true);
1322 tabs.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
1323
1324 let mut canvas = RecordingCanvas::new();
1325 tabs.paint(&mut canvas);
1326
1327 assert!(canvas.command_count() >= 5);
1328 }
1329
1330 #[test]
1331 fn test_tabs_paint_bottom() {
1332 use presentar_core::RecordingCanvas;
1333
1334 let mut tabs = Tabs::new()
1335 .tab(Tab::new("a", "A"))
1336 .tab(Tab::new("b", "B"))
1337 .orientation(TabOrientation::Bottom)
1338 .show_border(true);
1339 tabs.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
1340
1341 let mut canvas = RecordingCanvas::new();
1342 tabs.paint(&mut canvas);
1343
1344 assert!(canvas.command_count() >= 5);
1345 }
1346
1347 #[test]
1350 fn test_tabs_brick_name() {
1351 let tabs = Tabs::new();
1352 assert_eq!(tabs.brick_name(), "Tabs");
1353 }
1354
1355 #[test]
1356 fn test_tabs_brick_assertions() {
1357 let tabs = Tabs::new();
1358 let assertions = tabs.assertions();
1359 assert_eq!(assertions.len(), 1);
1360 assert!(assertions.contains(&BrickAssertion::MaxLatencyMs(16)));
1361 }
1362
1363 #[test]
1364 fn test_tabs_brick_budget() {
1365 let tabs = Tabs::new();
1366 let budget = tabs.budget();
1367 assert!(budget.measure_ms > 0);
1369 assert!(budget.layout_ms > 0);
1370 assert!(budget.paint_ms > 0);
1371 }
1372
1373 #[test]
1374 fn test_tabs_brick_verify() {
1375 let tabs = Tabs::new();
1376 let verification = tabs.verify();
1377 assert!(verification.failed.is_empty());
1378 assert!(!verification.passed.is_empty());
1379 }
1380
1381 #[test]
1382 fn test_tabs_to_html() {
1383 let tabs = Tabs::new();
1384 let html = tabs.to_html();
1385 assert!(html.contains("brick-tabs"));
1386 }
1387
1388 #[test]
1389 fn test_tabs_to_css() {
1390 let tabs = Tabs::new();
1391 let css = tabs.to_css();
1392 assert!(css.contains("brick-tabs"));
1393 assert!(css.contains("display: flex"));
1394 }
1395
1396 #[test]
1399 fn test_tabs_children_mut() {
1400 let mut tabs = Tabs::new();
1401 assert!(tabs.children_mut().is_empty());
1402 }
1403
1404 #[test]
1407 fn test_tabs_click_outside() {
1408 let mut tabs = Tabs::new()
1409 .tab(Tab::new("a", "A"))
1410 .min_tab_width(100.0)
1411 .tab_size(48.0);
1412 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1413
1414 let event = Event::MouseDown {
1416 position: Point::new(500.0, 24.0),
1417 button: MouseButton::Left,
1418 };
1419
1420 let result = tabs.event(&event);
1421 assert!(result.is_none());
1422 }
1423
1424 #[test]
1425 fn test_tabs_right_click_no_event() {
1426 let mut tabs = Tabs::new()
1427 .tab(Tab::new("a", "A"))
1428 .tab(Tab::new("b", "B"))
1429 .min_tab_width(100.0)
1430 .tab_size(48.0);
1431 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1432
1433 let event = Event::MouseDown {
1435 position: Point::new(150.0, 24.0),
1436 button: MouseButton::Right,
1437 };
1438
1439 let result = tabs.event(&event);
1440 assert!(result.is_none());
1441 }
1442
1443 #[test]
1444 fn test_tabs_other_event_no_effect() {
1445 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
1446 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1447
1448 let result = tabs.event(&Event::MouseEnter);
1449 assert!(result.is_none());
1450 }
1451}