1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Canvas, Color, Constraints, Event, MouseButton, Point, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Tab {
13 pub id: String,
15 pub label: String,
17 pub disabled: bool,
19 pub icon: Option<String>,
21}
22
23impl Tab {
24 #[must_use]
26 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
27 Self {
28 id: id.into(),
29 label: label.into(),
30 disabled: false,
31 icon: None,
32 }
33 }
34
35 #[must_use]
37 pub const fn disabled(mut self) -> Self {
38 self.disabled = true;
39 self
40 }
41
42 #[must_use]
44 pub fn icon(mut self, icon: impl Into<String>) -> Self {
45 self.icon = Some(icon.into());
46 self
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct TabChanged {
53 pub tab_id: String,
55 pub index: usize,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
61pub enum TabOrientation {
62 #[default]
64 Top,
65 Bottom,
67 Left,
69 Right,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Tabs {
76 items: Vec<Tab>,
78 active: usize,
80 orientation: TabOrientation,
82 tab_size: f32,
84 min_tab_width: f32,
86 spacing: f32,
88 tab_bg: Color,
90 active_bg: Color,
92 inactive_color: Color,
94 active_color: Color,
96 disabled_color: Color,
98 border_color: Color,
100 show_border: bool,
102 accessible_name_value: Option<String>,
104 test_id_value: Option<String>,
106 #[serde(skip)]
108 bounds: Rect,
109}
110
111impl Default for Tabs {
112 fn default() -> Self {
113 Self {
114 items: Vec::new(),
115 active: 0,
116 orientation: TabOrientation::Top,
117 tab_size: 48.0,
118 min_tab_width: 80.0,
119 spacing: 0.0,
120 tab_bg: Color::new(0.95, 0.95, 0.95, 1.0),
121 active_bg: Color::WHITE,
122 inactive_color: Color::new(0.4, 0.4, 0.4, 1.0),
123 active_color: Color::new(0.2, 0.47, 0.96, 1.0),
124 disabled_color: Color::new(0.7, 0.7, 0.7, 1.0),
125 border_color: Color::new(0.85, 0.85, 0.85, 1.0),
126 show_border: true,
127 accessible_name_value: None,
128 test_id_value: None,
129 bounds: Rect::default(),
130 }
131 }
132}
133
134impl Tabs {
135 #[must_use]
137 pub fn new() -> Self {
138 Self::default()
139 }
140
141 #[must_use]
143 pub fn tab(mut self, tab: Tab) -> Self {
144 self.items.push(tab);
145 self
146 }
147
148 #[must_use]
150 pub fn tabs(mut self, tabs: impl IntoIterator<Item = Tab>) -> Self {
151 self.items.extend(tabs);
152 self
153 }
154
155 #[must_use]
157 pub const fn active(mut self, index: usize) -> Self {
158 self.active = index;
159 self
160 }
161
162 #[must_use]
164 pub fn active_id(mut self, id: &str) -> Self {
165 if let Some(index) = self.items.iter().position(|t| t.id == id) {
166 self.active = index;
167 }
168 self
169 }
170
171 #[must_use]
173 pub const fn orientation(mut self, orientation: TabOrientation) -> Self {
174 self.orientation = orientation;
175 self
176 }
177
178 #[must_use]
180 pub fn tab_size(mut self, size: f32) -> Self {
181 self.tab_size = size.max(24.0);
182 self
183 }
184
185 #[must_use]
187 pub fn min_tab_width(mut self, width: f32) -> Self {
188 self.min_tab_width = width.max(40.0);
189 self
190 }
191
192 #[must_use]
194 pub fn spacing(mut self, spacing: f32) -> Self {
195 self.spacing = spacing.max(0.0);
196 self
197 }
198
199 #[must_use]
201 pub const fn tab_bg(mut self, color: Color) -> Self {
202 self.tab_bg = color;
203 self
204 }
205
206 #[must_use]
208 pub const fn active_bg(mut self, color: Color) -> Self {
209 self.active_bg = color;
210 self
211 }
212
213 #[must_use]
215 pub const fn inactive_color(mut self, color: Color) -> Self {
216 self.inactive_color = color;
217 self
218 }
219
220 #[must_use]
222 pub const fn active_color(mut self, color: Color) -> Self {
223 self.active_color = color;
224 self
225 }
226
227 #[must_use]
229 pub const fn show_border(mut self, show: bool) -> Self {
230 self.show_border = show;
231 self
232 }
233
234 #[must_use]
236 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
237 self.accessible_name_value = Some(name.into());
238 self
239 }
240
241 #[must_use]
243 pub fn test_id(mut self, id: impl Into<String>) -> Self {
244 self.test_id_value = Some(id.into());
245 self
246 }
247
248 #[must_use]
250 pub fn tab_count(&self) -> usize {
251 self.items.len()
252 }
253
254 #[must_use]
256 pub fn get_tabs(&self) -> &[Tab] {
257 &self.items
258 }
259
260 #[must_use]
262 pub const fn get_active(&self) -> usize {
263 self.active
264 }
265
266 #[must_use]
268 pub fn get_active_tab(&self) -> Option<&Tab> {
269 self.items.get(self.active)
270 }
271
272 #[must_use]
274 pub fn get_active_id(&self) -> Option<&str> {
275 self.items.get(self.active).map(|t| t.id.as_str())
276 }
277
278 #[must_use]
280 pub const fn is_active(&self, index: usize) -> bool {
281 self.active == index
282 }
283
284 #[must_use]
286 pub fn is_empty(&self) -> bool {
287 self.items.is_empty()
288 }
289
290 pub fn set_active(&mut self, index: usize) {
292 if index < self.items.len() && !self.items[index].disabled {
293 self.active = index;
294 }
295 }
296
297 pub fn set_active_id(&mut self, id: &str) {
299 if let Some(index) = self.items.iter().position(|t| t.id == id) {
300 if !self.items[index].disabled {
301 self.active = index;
302 }
303 }
304 }
305
306 pub fn next_tab(&mut self) {
308 let mut next = (self.active + 1) % self.items.len();
309 let start = next;
310 loop {
311 if !self.items[next].disabled {
312 self.active = next;
313 return;
314 }
315 next = (next + 1) % self.items.len();
316 if next == start {
317 return; }
319 }
320 }
321
322 pub fn prev_tab(&mut self) {
324 if self.items.is_empty() {
325 return;
326 }
327 let mut prev = if self.active == 0 {
328 self.items.len() - 1
329 } else {
330 self.active - 1
331 };
332 let start = prev;
333 loop {
334 if !self.items[prev].disabled {
335 self.active = prev;
336 return;
337 }
338 prev = if prev == 0 {
339 self.items.len() - 1
340 } else {
341 prev - 1
342 };
343 if prev == start {
344 return; }
346 }
347 }
348
349 fn calculate_tab_width(&self, available_width: f32) -> f32 {
351 if self.items.is_empty() {
352 return self.min_tab_width;
353 }
354 let total_spacing = self.spacing * (self.items.len() - 1).max(0) as f32;
355 let per_tab = (available_width - total_spacing) / self.items.len() as f32;
356 per_tab.max(self.min_tab_width)
357 }
358
359 fn tab_rect(&self, index: usize, tab_width: f32) -> Rect {
361 match self.orientation {
362 TabOrientation::Top | TabOrientation::Bottom => {
363 let x = (index as f32).mul_add(tab_width + self.spacing, self.bounds.x);
364 let y = if self.orientation == TabOrientation::Top {
365 self.bounds.y
366 } else {
367 self.bounds.y + self.bounds.height - self.tab_size
368 };
369 Rect::new(x, y, tab_width, self.tab_size)
370 }
371 TabOrientation::Left | TabOrientation::Right => {
372 let y = (index as f32).mul_add(self.tab_size + self.spacing, self.bounds.y);
373 let x = if self.orientation == TabOrientation::Left {
374 self.bounds.x
375 } else {
376 self.bounds.x + self.bounds.width - self.min_tab_width
377 };
378 Rect::new(x, y, self.min_tab_width, self.tab_size)
379 }
380 }
381 }
382
383 fn tab_at_point(&self, x: f32, y: f32) -> Option<usize> {
385 let tab_width = self.calculate_tab_width(self.bounds.width);
386 for (i, _) in self.items.iter().enumerate() {
387 let rect = self.tab_rect(i, tab_width);
388 if x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height {
389 return Some(i);
390 }
391 }
392 None
393 }
394}
395
396impl Widget for Tabs {
397 fn type_id(&self) -> TypeId {
398 TypeId::of::<Self>()
399 }
400
401 fn measure(&self, constraints: Constraints) -> Size {
402 let is_horizontal = matches!(
403 self.orientation,
404 TabOrientation::Top | TabOrientation::Bottom
405 );
406
407 let preferred = if is_horizontal {
408 Size::new(self.items.len() as f32 * self.min_tab_width, self.tab_size)
409 } else {
410 Size::new(self.min_tab_width, self.items.len() as f32 * self.tab_size)
411 };
412
413 constraints.constrain(preferred)
414 }
415
416 fn layout(&mut self, bounds: Rect) -> LayoutResult {
417 self.bounds = bounds;
418 LayoutResult {
419 size: bounds.size(),
420 }
421 }
422
423 fn paint(&self, canvas: &mut dyn Canvas) {
424 canvas.fill_rect(self.bounds, self.tab_bg);
426
427 let tab_width = self.calculate_tab_width(self.bounds.width);
428
429 for (i, tab) in self.items.iter().enumerate() {
431 let rect = self.tab_rect(i, tab_width);
432
433 let bg_color = if i == self.active {
435 self.active_bg
436 } else {
437 self.tab_bg
438 };
439 canvas.fill_rect(rect, bg_color);
440
441 let text_color = if tab.disabled {
443 self.disabled_color
444 } else if i == self.active {
445 self.active_color
446 } else {
447 self.inactive_color
448 };
449
450 let text_style = TextStyle {
451 size: 14.0,
452 color: text_color,
453 ..TextStyle::default()
454 };
455
456 canvas.draw_text(
457 &tab.label,
458 Point::new(rect.x + 12.0, rect.y + rect.height / 2.0),
459 &text_style,
460 );
461
462 if i == self.active && self.show_border {
464 let indicator_rect = match self.orientation {
465 TabOrientation::Top => {
466 Rect::new(rect.x, rect.y + rect.height - 2.0, rect.width, 2.0)
467 }
468 TabOrientation::Bottom => Rect::new(rect.x, rect.y, rect.width, 2.0),
469 TabOrientation::Left => {
470 Rect::new(rect.x + rect.width - 2.0, rect.y, 2.0, rect.height)
471 }
472 TabOrientation::Right => Rect::new(rect.x, rect.y, 2.0, rect.height),
473 };
474 canvas.fill_rect(indicator_rect, self.active_color);
475 }
476 }
477
478 if self.show_border {
480 let border_rect = match self.orientation {
481 TabOrientation::Top => Rect::new(
482 self.bounds.x,
483 self.bounds.y + self.tab_size - 1.0,
484 self.bounds.width,
485 1.0,
486 ),
487 TabOrientation::Bottom => {
488 Rect::new(self.bounds.x, self.bounds.y, self.bounds.width, 1.0)
489 }
490 TabOrientation::Left => Rect::new(
491 self.bounds.x + self.min_tab_width - 1.0,
492 self.bounds.y,
493 1.0,
494 self.bounds.height,
495 ),
496 TabOrientation::Right => {
497 Rect::new(self.bounds.x, self.bounds.y, 1.0, self.bounds.height)
498 }
499 };
500 canvas.fill_rect(border_rect, self.border_color);
501 }
502 }
503
504 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
505 if let Event::MouseDown {
506 position,
507 button: MouseButton::Left,
508 } = event
509 {
510 if let Some(index) = self.tab_at_point(position.x, position.y) {
511 if !self.items[index].disabled && index != self.active {
512 self.active = index;
513 return Some(Box::new(TabChanged {
514 tab_id: self.items[index].id.clone(),
515 index,
516 }));
517 }
518 }
519 }
520 None
521 }
522
523 fn children(&self) -> &[Box<dyn Widget>] {
524 &[]
525 }
526
527 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
528 &mut []
529 }
530
531 fn is_interactive(&self) -> bool {
532 !self.items.is_empty()
533 }
534
535 fn is_focusable(&self) -> bool {
536 !self.items.is_empty()
537 }
538
539 fn accessible_name(&self) -> Option<&str> {
540 self.accessible_name_value.as_deref()
541 }
542
543 fn accessible_role(&self) -> AccessibleRole {
544 AccessibleRole::Tab
545 }
546
547 fn test_id(&self) -> Option<&str> {
548 self.test_id_value.as_deref()
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
559 fn test_tab_new() {
560 let tab = Tab::new("home", "Home");
561 assert_eq!(tab.id, "home");
562 assert_eq!(tab.label, "Home");
563 assert!(!tab.disabled);
564 assert!(tab.icon.is_none());
565 }
566
567 #[test]
568 fn test_tab_disabled() {
569 let tab = Tab::new("settings", "Settings").disabled();
570 assert!(tab.disabled);
571 }
572
573 #[test]
574 fn test_tab_icon() {
575 let tab = Tab::new("profile", "Profile").icon("user");
576 assert_eq!(tab.icon, Some("user".to_string()));
577 }
578
579 #[test]
582 fn test_tab_changed() {
583 let msg = TabChanged {
584 tab_id: "settings".to_string(),
585 index: 2,
586 };
587 assert_eq!(msg.tab_id, "settings");
588 assert_eq!(msg.index, 2);
589 }
590
591 #[test]
594 fn test_tab_orientation_default() {
595 assert_eq!(TabOrientation::default(), TabOrientation::Top);
596 }
597
598 #[test]
601 fn test_tabs_new() {
602 let tabs = Tabs::new();
603 assert_eq!(tabs.tab_count(), 0);
604 assert!(tabs.is_empty());
605 }
606
607 #[test]
608 fn test_tabs_builder() {
609 let tabs = Tabs::new()
610 .tab(Tab::new("home", "Home"))
611 .tab(Tab::new("about", "About"))
612 .tab(Tab::new("contact", "Contact"))
613 .active(1)
614 .orientation(TabOrientation::Top)
615 .tab_size(50.0)
616 .min_tab_width(100.0)
617 .spacing(4.0)
618 .show_border(true)
619 .accessible_name("Main navigation")
620 .test_id("main-tabs");
621
622 assert_eq!(tabs.tab_count(), 3);
623 assert_eq!(tabs.get_active(), 1);
624 assert_eq!(tabs.get_active_id(), Some("about"));
625 assert_eq!(Widget::accessible_name(&tabs), Some("Main navigation"));
626 assert_eq!(Widget::test_id(&tabs), Some("main-tabs"));
627 }
628
629 #[test]
630 fn test_tabs_multiple() {
631 let tab_list = vec![Tab::new("a", "A"), Tab::new("b", "B"), Tab::new("c", "C")];
632 let tabs = Tabs::new().tabs(tab_list);
633 assert_eq!(tabs.tab_count(), 3);
634 }
635
636 #[test]
637 fn test_tabs_active_id() {
638 let tabs = Tabs::new()
639 .tab(Tab::new("first", "First"))
640 .tab(Tab::new("second", "Second"))
641 .active_id("second");
642
643 assert_eq!(tabs.get_active(), 1);
644 }
645
646 #[test]
647 fn test_tabs_active_id_not_found() {
648 let tabs = Tabs::new()
649 .tab(Tab::new("first", "First"))
650 .active_id("nonexistent");
651
652 assert_eq!(tabs.get_active(), 0);
653 }
654
655 #[test]
658 fn test_tabs_get_active_tab() {
659 let tabs = Tabs::new()
660 .tab(Tab::new("home", "Home"))
661 .tab(Tab::new("about", "About"))
662 .active(1);
663
664 let active = tabs.get_active_tab().unwrap();
665 assert_eq!(active.id, "about");
666 }
667
668 #[test]
669 fn test_tabs_get_active_tab_empty() {
670 let tabs = Tabs::new();
671 assert!(tabs.get_active_tab().is_none());
672 }
673
674 #[test]
675 fn test_tabs_is_active() {
676 let tabs = Tabs::new()
677 .tab(Tab::new("a", "A"))
678 .tab(Tab::new("b", "B"))
679 .active(1);
680
681 assert!(!tabs.is_active(0));
682 assert!(tabs.is_active(1));
683 }
684
685 #[test]
688 fn test_tabs_set_active() {
689 let mut tabs = Tabs::new()
690 .tab(Tab::new("a", "A"))
691 .tab(Tab::new("b", "B"))
692 .tab(Tab::new("c", "C"));
693
694 tabs.set_active(2);
695 assert_eq!(tabs.get_active(), 2);
696 }
697
698 #[test]
699 fn test_tabs_set_active_out_of_bounds() {
700 let mut tabs = Tabs::new().tab(Tab::new("a", "A")).tab(Tab::new("b", "B"));
701
702 tabs.set_active(10);
703 assert_eq!(tabs.get_active(), 0); }
705
706 #[test]
707 fn test_tabs_set_active_disabled() {
708 let mut tabs = Tabs::new()
709 .tab(Tab::new("a", "A"))
710 .tab(Tab::new("b", "B").disabled());
711
712 tabs.set_active(1);
713 assert_eq!(tabs.get_active(), 0); }
715
716 #[test]
717 fn test_tabs_set_active_id() {
718 let mut tabs = Tabs::new()
719 .tab(Tab::new("home", "Home"))
720 .tab(Tab::new("settings", "Settings"));
721
722 tabs.set_active_id("settings");
723 assert_eq!(tabs.get_active(), 1);
724 }
725
726 #[test]
729 fn test_tabs_next_tab() {
730 let mut tabs = Tabs::new()
731 .tab(Tab::new("a", "A"))
732 .tab(Tab::new("b", "B"))
733 .tab(Tab::new("c", "C"))
734 .active(0);
735
736 tabs.next_tab();
737 assert_eq!(tabs.get_active(), 1);
738
739 tabs.next_tab();
740 assert_eq!(tabs.get_active(), 2);
741
742 tabs.next_tab(); assert_eq!(tabs.get_active(), 0);
744 }
745
746 #[test]
747 fn test_tabs_next_tab_skip_disabled() {
748 let mut tabs = Tabs::new()
749 .tab(Tab::new("a", "A"))
750 .tab(Tab::new("b", "B").disabled())
751 .tab(Tab::new("c", "C"))
752 .active(0);
753
754 tabs.next_tab();
755 assert_eq!(tabs.get_active(), 2); }
757
758 #[test]
759 fn test_tabs_prev_tab() {
760 let mut tabs = Tabs::new()
761 .tab(Tab::new("a", "A"))
762 .tab(Tab::new("b", "B"))
763 .tab(Tab::new("c", "C"))
764 .active(2);
765
766 tabs.prev_tab();
767 assert_eq!(tabs.get_active(), 1);
768
769 tabs.prev_tab();
770 assert_eq!(tabs.get_active(), 0);
771
772 tabs.prev_tab(); assert_eq!(tabs.get_active(), 2);
774 }
775
776 #[test]
777 fn test_tabs_prev_tab_skip_disabled() {
778 let mut tabs = Tabs::new()
779 .tab(Tab::new("a", "A"))
780 .tab(Tab::new("b", "B").disabled())
781 .tab(Tab::new("c", "C"))
782 .active(2);
783
784 tabs.prev_tab();
785 assert_eq!(tabs.get_active(), 0); }
787
788 #[test]
791 fn test_tabs_tab_size_min() {
792 let tabs = Tabs::new().tab_size(10.0);
793 assert_eq!(tabs.tab_size, 24.0);
794 }
795
796 #[test]
797 fn test_tabs_min_tab_width_min() {
798 let tabs = Tabs::new().min_tab_width(20.0);
799 assert_eq!(tabs.min_tab_width, 40.0);
800 }
801
802 #[test]
803 fn test_tabs_spacing_min() {
804 let tabs = Tabs::new().spacing(-5.0);
805 assert_eq!(tabs.spacing, 0.0);
806 }
807
808 #[test]
809 fn test_tabs_calculate_tab_width() {
810 let tabs = Tabs::new()
811 .tab(Tab::new("a", "A"))
812 .tab(Tab::new("b", "B"))
813 .min_tab_width(50.0)
814 .spacing(0.0);
815
816 assert_eq!(tabs.calculate_tab_width(200.0), 100.0);
817 }
818
819 #[test]
820 fn test_tabs_calculate_tab_width_with_spacing() {
821 let tabs = Tabs::new()
822 .tab(Tab::new("a", "A"))
823 .tab(Tab::new("b", "B"))
824 .min_tab_width(50.0)
825 .spacing(10.0);
826
827 assert_eq!(tabs.calculate_tab_width(200.0), 95.0);
829 }
830
831 #[test]
834 fn test_tabs_type_id() {
835 let tabs = Tabs::new();
836 assert_eq!(Widget::type_id(&tabs), TypeId::of::<Tabs>());
837 }
838
839 #[test]
840 fn test_tabs_measure_horizontal() {
841 let tabs = Tabs::new()
842 .tab(Tab::new("a", "A"))
843 .tab(Tab::new("b", "B"))
844 .orientation(TabOrientation::Top)
845 .min_tab_width(100.0)
846 .tab_size(48.0);
847
848 let size = tabs.measure(Constraints::loose(Size::new(500.0, 500.0)));
849 assert_eq!(size.width, 200.0);
850 assert_eq!(size.height, 48.0);
851 }
852
853 #[test]
854 fn test_tabs_measure_vertical() {
855 let tabs = Tabs::new()
856 .tab(Tab::new("a", "A"))
857 .tab(Tab::new("b", "B"))
858 .orientation(TabOrientation::Left)
859 .min_tab_width(100.0)
860 .tab_size(48.0);
861
862 let size = tabs.measure(Constraints::loose(Size::new(500.0, 500.0)));
863 assert_eq!(size.width, 100.0);
864 assert_eq!(size.height, 96.0);
865 }
866
867 #[test]
868 fn test_tabs_layout() {
869 let mut tabs = Tabs::new().tab(Tab::new("a", "A"));
870 let bounds = Rect::new(10.0, 20.0, 300.0, 48.0);
871 let result = tabs.layout(bounds);
872 assert_eq!(result.size, Size::new(300.0, 48.0));
873 assert_eq!(tabs.bounds, bounds);
874 }
875
876 #[test]
877 fn test_tabs_children() {
878 let tabs = Tabs::new();
879 assert!(tabs.children().is_empty());
880 }
881
882 #[test]
883 fn test_tabs_is_interactive() {
884 let tabs = Tabs::new();
885 assert!(!tabs.is_interactive()); let tabs = Tabs::new().tab(Tab::new("a", "A"));
888 assert!(tabs.is_interactive());
889 }
890
891 #[test]
892 fn test_tabs_is_focusable() {
893 let tabs = Tabs::new();
894 assert!(!tabs.is_focusable()); let tabs = Tabs::new().tab(Tab::new("a", "A"));
897 assert!(tabs.is_focusable());
898 }
899
900 #[test]
901 fn test_tabs_accessible_role() {
902 let tabs = Tabs::new();
903 assert_eq!(tabs.accessible_role(), AccessibleRole::Tab);
904 }
905
906 #[test]
907 fn test_tabs_accessible_name() {
908 let tabs = Tabs::new().accessible_name("Section tabs");
909 assert_eq!(Widget::accessible_name(&tabs), Some("Section tabs"));
910 }
911
912 #[test]
913 fn test_tabs_test_id() {
914 let tabs = Tabs::new().test_id("nav-tabs");
915 assert_eq!(Widget::test_id(&tabs), Some("nav-tabs"));
916 }
917
918 #[test]
921 fn test_tab_rect_top() {
922 let mut tabs = Tabs::new()
923 .tab(Tab::new("a", "A"))
924 .tab(Tab::new("b", "B"))
925 .orientation(TabOrientation::Top)
926 .tab_size(48.0);
927 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
928
929 let rect0 = tabs.tab_rect(0, 100.0);
930 assert_eq!(rect0.x, 0.0);
931 assert_eq!(rect0.y, 0.0);
932 assert_eq!(rect0.width, 100.0);
933 assert_eq!(rect0.height, 48.0);
934
935 let rect1 = tabs.tab_rect(1, 100.0);
936 assert_eq!(rect1.x, 100.0);
937 }
938
939 #[test]
940 fn test_tab_rect_bottom() {
941 let mut tabs = Tabs::new()
942 .tab(Tab::new("a", "A"))
943 .orientation(TabOrientation::Bottom)
944 .tab_size(48.0);
945 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
946
947 let rect = tabs.tab_rect(0, 100.0);
948 assert_eq!(rect.y, 52.0); }
950
951 #[test]
954 fn test_tabs_click_changes_active() {
955 let mut tabs = Tabs::new()
956 .tab(Tab::new("a", "A"))
957 .tab(Tab::new("b", "B"))
958 .tab_size(48.0)
959 .min_tab_width(100.0);
960 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
961
962 let event = Event::MouseDown {
964 position: Point::new(150.0, 24.0),
965 button: MouseButton::Left,
966 };
967
968 let result = tabs.event(&event);
969 assert!(result.is_some());
970 assert_eq!(tabs.get_active(), 1);
971
972 let msg = result.unwrap().downcast::<TabChanged>().unwrap();
973 assert_eq!(msg.tab_id, "b");
974 assert_eq!(msg.index, 1);
975 }
976
977 #[test]
978 fn test_tabs_click_disabled_no_change() {
979 let mut tabs = Tabs::new()
980 .tab(Tab::new("a", "A"))
981 .tab(Tab::new("b", "B").disabled())
982 .tab_size(48.0)
983 .min_tab_width(100.0);
984 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
985
986 let event = Event::MouseDown {
988 position: Point::new(150.0, 24.0),
989 button: MouseButton::Left,
990 };
991
992 let result = tabs.event(&event);
993 assert!(result.is_none());
994 assert_eq!(tabs.get_active(), 0);
995 }
996
997 #[test]
998 fn test_tabs_click_same_tab_no_event() {
999 let mut tabs = Tabs::new()
1000 .tab(Tab::new("a", "A"))
1001 .tab(Tab::new("b", "B"))
1002 .active(0)
1003 .tab_size(48.0)
1004 .min_tab_width(100.0);
1005 tabs.bounds = Rect::new(0.0, 0.0, 200.0, 48.0);
1006
1007 let event = Event::MouseDown {
1009 position: Point::new(50.0, 24.0),
1010 button: MouseButton::Left,
1011 };
1012
1013 let result = tabs.event(&event);
1014 assert!(result.is_none());
1015 }
1016}