1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult},
5 Canvas, Color, Constraints, Event, MouseButton, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ToggleChanged {
13 pub on: bool,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Toggle {
20 on: bool,
22 disabled: bool,
24 label: String,
26 track_width: f32,
28 track_height: f32,
30 thumb_size: f32,
32 track_off_color: Color,
34 track_on_color: Color,
36 thumb_color: Color,
38 disabled_color: Color,
40 label_color: Color,
42 spacing: f32,
44 accessible_name_value: Option<String>,
46 test_id_value: Option<String>,
48 #[serde(skip)]
50 bounds: Rect,
51}
52
53impl Default for Toggle {
54 fn default() -> Self {
55 Self {
56 on: false,
57 disabled: false,
58 label: String::new(),
59 track_width: 44.0,
60 track_height: 24.0,
61 thumb_size: 20.0,
62 track_off_color: Color::new(0.7, 0.7, 0.7, 1.0),
63 track_on_color: Color::new(0.2, 0.47, 0.96, 1.0),
64 thumb_color: Color::WHITE,
65 disabled_color: Color::new(0.85, 0.85, 0.85, 1.0),
66 label_color: Color::BLACK,
67 spacing: 8.0,
68 accessible_name_value: None,
69 test_id_value: None,
70 bounds: Rect::default(),
71 }
72 }
73}
74
75impl Toggle {
76 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 #[must_use]
84 pub fn with_state(on: bool) -> Self {
85 Self::default().on(on)
86 }
87
88 #[must_use]
90 pub const fn on(mut self, on: bool) -> Self {
91 self.on = on;
92 self
93 }
94
95 #[must_use]
97 pub const fn disabled(mut self, disabled: bool) -> Self {
98 self.disabled = disabled;
99 self
100 }
101
102 #[must_use]
104 pub fn label(mut self, label: impl Into<String>) -> Self {
105 self.label = label.into();
106 self
107 }
108
109 #[must_use]
111 pub fn track_width(mut self, width: f32) -> Self {
112 self.track_width = width.max(20.0);
113 self
114 }
115
116 #[must_use]
118 pub fn track_height(mut self, height: f32) -> Self {
119 self.track_height = height.max(12.0);
120 self
121 }
122
123 #[must_use]
125 pub fn thumb_size(mut self, size: f32) -> Self {
126 self.thumb_size = size.max(8.0);
127 self
128 }
129
130 #[must_use]
132 pub const fn track_off_color(mut self, color: Color) -> Self {
133 self.track_off_color = color;
134 self
135 }
136
137 #[must_use]
139 pub const fn track_on_color(mut self, color: Color) -> Self {
140 self.track_on_color = color;
141 self
142 }
143
144 #[must_use]
146 pub const fn thumb_color(mut self, color: Color) -> Self {
147 self.thumb_color = color;
148 self
149 }
150
151 #[must_use]
153 pub const fn disabled_color(mut self, color: Color) -> Self {
154 self.disabled_color = color;
155 self
156 }
157
158 #[must_use]
160 pub const fn label_color(mut self, color: Color) -> Self {
161 self.label_color = color;
162 self
163 }
164
165 #[must_use]
167 pub fn spacing(mut self, spacing: f32) -> Self {
168 self.spacing = spacing.max(0.0);
169 self
170 }
171
172 #[must_use]
174 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
175 self.accessible_name_value = Some(name.into());
176 self
177 }
178
179 #[must_use]
181 pub fn test_id(mut self, id: impl Into<String>) -> Self {
182 self.test_id_value = Some(id.into());
183 self
184 }
185
186 #[must_use]
188 pub const fn is_on(&self) -> bool {
189 self.on
190 }
191
192 #[must_use]
194 pub const fn is_disabled(&self) -> bool {
195 self.disabled
196 }
197
198 #[must_use]
200 pub fn get_label(&self) -> &str {
201 &self.label
202 }
203
204 #[must_use]
206 pub const fn get_track_width(&self) -> f32 {
207 self.track_width
208 }
209
210 #[must_use]
212 pub const fn get_track_height(&self) -> f32 {
213 self.track_height
214 }
215
216 #[must_use]
218 pub const fn get_thumb_size(&self) -> f32 {
219 self.thumb_size
220 }
221
222 #[must_use]
224 pub const fn get_spacing(&self) -> f32 {
225 self.spacing
226 }
227
228 pub fn toggle(&mut self) {
230 if !self.disabled {
231 self.on = !self.on;
232 }
233 }
234
235 pub fn set_on(&mut self, on: bool) {
237 self.on = on;
238 }
239
240 fn thumb_x(&self) -> f32 {
242 let padding = (self.track_height - self.thumb_size) / 2.0;
243 if self.on {
244 self.bounds.x + self.track_width - self.thumb_size - padding
245 } else {
246 self.bounds.x + padding
247 }
248 }
249
250 fn thumb_y(&self) -> f32 {
252 self.bounds.y + (self.track_height - self.thumb_size) / 2.0
253 }
254
255 fn hit_test(&self, x: f32, y: f32) -> bool {
257 x >= self.bounds.x
258 && x <= self.bounds.x + self.track_width
259 && y >= self.bounds.y
260 && y <= self.bounds.y + self.track_height
261 }
262}
263
264impl Widget for Toggle {
265 fn type_id(&self) -> TypeId {
266 TypeId::of::<Self>()
267 }
268
269 fn measure(&self, constraints: Constraints) -> Size {
270 let label_width = if self.label.is_empty() {
271 0.0
272 } else {
273 (self.label.len() as f32).mul_add(8.0, self.spacing)
274 };
275 let preferred = Size::new(self.track_width + label_width, self.track_height.max(20.0));
276 constraints.constrain(preferred)
277 }
278
279 fn layout(&mut self, bounds: Rect) -> LayoutResult {
280 self.bounds = bounds;
281 LayoutResult {
282 size: bounds.size(),
283 }
284 }
285
286 fn paint(&self, canvas: &mut dyn Canvas) {
287 let track_color = if self.disabled {
289 self.disabled_color
290 } else if self.on {
291 self.track_on_color
292 } else {
293 self.track_off_color
294 };
295
296 let track_rect = Rect::new(
298 self.bounds.x,
299 self.bounds.y,
300 self.track_width,
301 self.track_height,
302 );
303 canvas.fill_rect(track_rect, track_color);
304
305 let thumb_color = if self.disabled {
307 Color::new(0.9, 0.9, 0.9, 1.0)
308 } else {
309 self.thumb_color
310 };
311 let thumb_rect = Rect::new(
312 self.thumb_x(),
313 self.thumb_y(),
314 self.thumb_size,
315 self.thumb_size,
316 );
317 canvas.fill_rect(thumb_rect, thumb_color);
318 }
319
320 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
321 if self.disabled {
322 return None;
323 }
324
325 if let Event::MouseDown {
326 position,
327 button: MouseButton::Left,
328 } = event
329 {
330 if self.hit_test(position.x, position.y) {
331 self.on = !self.on;
332 return Some(Box::new(ToggleChanged { on: self.on }));
333 }
334 }
335
336 None
337 }
338
339 fn children(&self) -> &[Box<dyn Widget>] {
340 &[]
341 }
342
343 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
344 &mut []
345 }
346
347 fn is_interactive(&self) -> bool {
348 !self.disabled
349 }
350
351 fn is_focusable(&self) -> bool {
352 !self.disabled
353 }
354
355 fn accessible_name(&self) -> Option<&str> {
356 self.accessible_name_value.as_deref().or_else(|| {
357 if self.label.is_empty() {
358 None
359 } else {
360 Some(&self.label)
361 }
362 })
363 }
364
365 fn accessible_role(&self) -> AccessibleRole {
366 AccessibleRole::Checkbox
368 }
369
370 fn test_id(&self) -> Option<&str> {
371 self.test_id_value.as_deref()
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use presentar_core::Point;
379
380 #[test]
383 fn test_toggle_changed_message() {
384 let msg = ToggleChanged { on: true };
385 assert!(msg.on);
386
387 let msg = ToggleChanged { on: false };
388 assert!(!msg.on);
389 }
390
391 #[test]
394 fn test_toggle_new() {
395 let toggle = Toggle::new();
396 assert!(!toggle.is_on());
397 assert!(!toggle.is_disabled());
398 }
399
400 #[test]
401 fn test_toggle_with_state_on() {
402 let toggle = Toggle::with_state(true);
403 assert!(toggle.is_on());
404 }
405
406 #[test]
407 fn test_toggle_with_state_off() {
408 let toggle = Toggle::with_state(false);
409 assert!(!toggle.is_on());
410 }
411
412 #[test]
413 fn test_toggle_default() {
414 let toggle = Toggle::default();
415 assert!(!toggle.is_on());
416 assert!(!toggle.is_disabled());
417 assert!(toggle.get_label().is_empty());
418 }
419
420 #[test]
421 fn test_toggle_builder() {
422 let toggle = Toggle::new()
423 .on(true)
424 .disabled(false)
425 .label("Dark Mode")
426 .track_width(50.0)
427 .track_height(28.0)
428 .thumb_size(24.0)
429 .track_off_color(Color::new(0.5, 0.5, 0.5, 1.0))
430 .track_on_color(Color::new(0.0, 0.8, 0.4, 1.0))
431 .thumb_color(Color::WHITE)
432 .disabled_color(Color::new(0.9, 0.9, 0.9, 1.0))
433 .label_color(Color::BLACK)
434 .spacing(12.0)
435 .accessible_name("Toggle dark mode")
436 .test_id("dark-mode-toggle");
437
438 assert!(toggle.is_on());
439 assert!(!toggle.is_disabled());
440 assert_eq!(toggle.get_label(), "Dark Mode");
441 assert_eq!(toggle.get_track_width(), 50.0);
442 assert_eq!(toggle.get_track_height(), 28.0);
443 assert_eq!(toggle.get_thumb_size(), 24.0);
444 assert_eq!(toggle.get_spacing(), 12.0);
445 assert_eq!(Widget::accessible_name(&toggle), Some("Toggle dark mode"));
446 assert_eq!(Widget::test_id(&toggle), Some("dark-mode-toggle"));
447 }
448
449 #[test]
452 fn test_toggle_on() {
453 let toggle = Toggle::new().on(true);
454 assert!(toggle.is_on());
455 }
456
457 #[test]
458 fn test_toggle_off() {
459 let toggle = Toggle::new().on(false);
460 assert!(!toggle.is_on());
461 }
462
463 #[test]
464 fn test_toggle_set_on() {
465 let mut toggle = Toggle::new();
466 toggle.set_on(true);
467 assert!(toggle.is_on());
468 toggle.set_on(false);
469 assert!(!toggle.is_on());
470 }
471
472 #[test]
473 fn test_toggle_toggle_method() {
474 let mut toggle = Toggle::new();
475 assert!(!toggle.is_on());
476 toggle.toggle();
477 assert!(toggle.is_on());
478 toggle.toggle();
479 assert!(!toggle.is_on());
480 }
481
482 #[test]
483 fn test_toggle_disabled_cannot_toggle() {
484 let mut toggle = Toggle::new().disabled(true);
485 toggle.toggle();
486 assert!(!toggle.is_on()); }
488
489 #[test]
492 fn test_toggle_track_width_min() {
493 let toggle = Toggle::new().track_width(10.0);
494 assert_eq!(toggle.get_track_width(), 20.0);
495 }
496
497 #[test]
498 fn test_toggle_track_height_min() {
499 let toggle = Toggle::new().track_height(5.0);
500 assert_eq!(toggle.get_track_height(), 12.0);
501 }
502
503 #[test]
504 fn test_toggle_thumb_size_min() {
505 let toggle = Toggle::new().thumb_size(2.0);
506 assert_eq!(toggle.get_thumb_size(), 8.0);
507 }
508
509 #[test]
510 fn test_toggle_spacing_min() {
511 let toggle = Toggle::new().spacing(-5.0);
512 assert_eq!(toggle.get_spacing(), 0.0);
513 }
514
515 #[test]
518 fn test_toggle_colors() {
519 let track_off = Color::new(0.3, 0.3, 0.3, 1.0);
520 let track_on = Color::new(0.0, 1.0, 0.5, 1.0);
521 let thumb = Color::new(1.0, 1.0, 1.0, 1.0);
522
523 let toggle = Toggle::new()
524 .track_off_color(track_off)
525 .track_on_color(track_on)
526 .thumb_color(thumb);
527
528 assert_eq!(toggle.track_off_color, track_off);
529 assert_eq!(toggle.track_on_color, track_on);
530 assert_eq!(toggle.thumb_color, thumb);
531 }
532
533 #[test]
536 fn test_toggle_thumb_position_off() {
537 let mut toggle = Toggle::new()
538 .track_width(44.0)
539 .track_height(24.0)
540 .thumb_size(20.0);
541 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
542
543 let padding = (24.0 - 20.0) / 2.0;
544 assert_eq!(toggle.thumb_x(), padding); }
546
547 #[test]
548 fn test_toggle_thumb_position_on() {
549 let mut toggle = Toggle::new()
550 .on(true)
551 .track_width(44.0)
552 .track_height(24.0)
553 .thumb_size(20.0);
554 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
555
556 let padding = (24.0 - 20.0) / 2.0;
557 assert_eq!(toggle.thumb_x(), 44.0 - 20.0 - padding); }
559
560 #[test]
561 fn test_toggle_thumb_y_centered() {
562 let mut toggle = Toggle::new().track_height(24.0).thumb_size(20.0);
563 toggle.bounds = Rect::new(10.0, 20.0, 44.0, 24.0);
564
565 assert_eq!(toggle.thumb_y(), 20.0 + (24.0 - 20.0) / 2.0);
566 }
567
568 #[test]
571 fn test_toggle_hit_test_inside() {
572 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
573 toggle.bounds = Rect::new(10.0, 10.0, 44.0, 24.0);
574
575 assert!(toggle.hit_test(20.0, 20.0));
576 assert!(toggle.hit_test(10.0, 10.0)); assert!(toggle.hit_test(54.0, 34.0)); }
579
580 #[test]
581 fn test_toggle_hit_test_outside() {
582 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
583 toggle.bounds = Rect::new(10.0, 10.0, 44.0, 24.0);
584
585 assert!(!toggle.hit_test(5.0, 10.0)); assert!(!toggle.hit_test(60.0, 10.0)); assert!(!toggle.hit_test(20.0, 5.0)); assert!(!toggle.hit_test(20.0, 40.0)); }
590
591 #[test]
594 fn test_toggle_type_id() {
595 let toggle = Toggle::new();
596 assert_eq!(Widget::type_id(&toggle), TypeId::of::<Toggle>());
597 }
598
599 #[test]
600 fn test_toggle_measure_no_label() {
601 let toggle = Toggle::new().track_width(44.0).track_height(24.0);
602 let size = toggle.measure(Constraints::loose(Size::new(200.0, 100.0)));
603 assert_eq!(size.width, 44.0);
604 assert_eq!(size.height, 24.0);
605 }
606
607 #[test]
608 fn test_toggle_measure_with_label() {
609 let toggle = Toggle::new()
610 .track_width(44.0)
611 .track_height(24.0)
612 .label("On")
613 .spacing(8.0);
614 let size = toggle.measure(Constraints::loose(Size::new(200.0, 100.0)));
615 assert_eq!(size.width, 44.0 + 8.0 + 16.0);
617 }
618
619 #[test]
620 fn test_toggle_layout() {
621 let mut toggle = Toggle::new();
622 let bounds = Rect::new(10.0, 20.0, 44.0, 24.0);
623 let result = toggle.layout(bounds);
624 assert_eq!(result.size, Size::new(44.0, 24.0));
625 assert_eq!(toggle.bounds, bounds);
626 }
627
628 #[test]
629 fn test_toggle_children() {
630 let toggle = Toggle::new();
631 assert!(toggle.children().is_empty());
632 }
633
634 #[test]
635 fn test_toggle_is_interactive() {
636 let toggle = Toggle::new();
637 assert!(toggle.is_interactive());
638
639 let toggle = Toggle::new().disabled(true);
640 assert!(!toggle.is_interactive());
641 }
642
643 #[test]
644 fn test_toggle_is_focusable() {
645 let toggle = Toggle::new();
646 assert!(toggle.is_focusable());
647
648 let toggle = Toggle::new().disabled(true);
649 assert!(!toggle.is_focusable());
650 }
651
652 #[test]
653 fn test_toggle_accessible_role() {
654 let toggle = Toggle::new();
655 assert_eq!(toggle.accessible_role(), AccessibleRole::Checkbox);
656 }
657
658 #[test]
659 fn test_toggle_accessible_name_from_label() {
660 let toggle = Toggle::new().label("Enable notifications");
661 assert_eq!(
662 Widget::accessible_name(&toggle),
663 Some("Enable notifications")
664 );
665 }
666
667 #[test]
668 fn test_toggle_accessible_name_override() {
669 let toggle = Toggle::new()
670 .label("Notifications")
671 .accessible_name("Toggle notifications on or off");
672 assert_eq!(
673 Widget::accessible_name(&toggle),
674 Some("Toggle notifications on or off")
675 );
676 }
677
678 #[test]
679 fn test_toggle_accessible_name_none() {
680 let toggle = Toggle::new();
681 assert_eq!(Widget::accessible_name(&toggle), None);
682 }
683
684 #[test]
685 fn test_toggle_test_id() {
686 let toggle = Toggle::new().test_id("settings-toggle");
687 assert_eq!(Widget::test_id(&toggle), Some("settings-toggle"));
688 }
689
690 #[test]
693 fn test_toggle_click_toggles_state() {
694 let mut toggle = Toggle::new();
695 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
696
697 let event = Event::MouseDown {
698 position: Point::new(22.0, 12.0),
699 button: MouseButton::Left,
700 };
701
702 let result = toggle.event(&event);
703 assert!(result.is_some());
704 assert!(toggle.is_on());
705
706 let result = toggle.event(&event);
707 assert!(result.is_some());
708 assert!(!toggle.is_on());
709 }
710
711 #[test]
712 fn test_toggle_click_outside_no_effect() {
713 let mut toggle = Toggle::new();
714 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
715
716 let event = Event::MouseDown {
717 position: Point::new(100.0, 100.0),
718 button: MouseButton::Left,
719 };
720
721 let result = toggle.event(&event);
722 assert!(result.is_none());
723 assert!(!toggle.is_on());
724 }
725
726 #[test]
727 fn test_toggle_right_click_no_effect() {
728 let mut toggle = Toggle::new();
729 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
730
731 let event = Event::MouseDown {
732 position: Point::new(22.0, 12.0),
733 button: MouseButton::Right,
734 };
735
736 let result = toggle.event(&event);
737 assert!(result.is_none());
738 assert!(!toggle.is_on());
739 }
740
741 #[test]
742 fn test_toggle_disabled_click_no_effect() {
743 let mut toggle = Toggle::new().disabled(true);
744 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
745
746 let event = Event::MouseDown {
747 position: Point::new(22.0, 12.0),
748 button: MouseButton::Left,
749 };
750
751 let result = toggle.event(&event);
752 assert!(result.is_none());
753 assert!(!toggle.is_on());
754 }
755
756 #[test]
757 fn test_toggle_changed_contains_new_state() {
758 let mut toggle = Toggle::new();
759 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
760
761 let event = Event::MouseDown {
762 position: Point::new(22.0, 12.0),
763 button: MouseButton::Left,
764 };
765
766 let result = toggle.event(&event);
767 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
768 assert!(msg.on);
769
770 let result = toggle.event(&event);
771 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
772 assert!(!msg.on);
773 }
774
775 use presentar_core::draw::DrawCommand;
778 use presentar_core::RecordingCanvas;
779
780 #[test]
781 fn test_toggle_paint_draws_track_and_thumb() {
782 let mut toggle = Toggle::new()
783 .track_width(44.0)
784 .track_height(24.0)
785 .thumb_size(20.0);
786 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
787
788 let mut canvas = RecordingCanvas::new();
789 toggle.paint(&mut canvas);
790
791 assert_eq!(canvas.command_count(), 2);
793 }
794
795 #[test]
796 fn test_toggle_paint_track_off_color() {
797 let mut toggle = Toggle::new().track_off_color(Color::RED).on(false);
798 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
799
800 let mut canvas = RecordingCanvas::new();
801 toggle.paint(&mut canvas);
802
803 match &canvas.commands()[0] {
805 DrawCommand::Rect { style, .. } => {
806 assert_eq!(style.fill, Some(Color::RED));
807 }
808 _ => panic!("Expected Rect command for track"),
809 }
810 }
811
812 #[test]
813 fn test_toggle_paint_track_on_color() {
814 let mut toggle = Toggle::new().track_on_color(Color::GREEN).on(true);
815 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
816
817 let mut canvas = RecordingCanvas::new();
818 toggle.paint(&mut canvas);
819
820 match &canvas.commands()[0] {
822 DrawCommand::Rect { style, .. } => {
823 assert_eq!(style.fill, Some(Color::GREEN));
824 }
825 _ => panic!("Expected Rect command for track"),
826 }
827 }
828
829 #[test]
830 fn test_toggle_paint_track_disabled_color() {
831 let mut toggle = Toggle::new()
832 .disabled_color(Color::new(0.85, 0.85, 0.85, 1.0))
833 .disabled(true);
834 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
835
836 let mut canvas = RecordingCanvas::new();
837 toggle.paint(&mut canvas);
838
839 match &canvas.commands()[0] {
841 DrawCommand::Rect { style, .. } => {
842 let fill = style.fill.unwrap();
843 assert!((fill.r - 0.85).abs() < 0.01);
844 }
845 _ => panic!("Expected Rect command for track"),
846 }
847 }
848
849 #[test]
850 fn test_toggle_paint_track_dimensions() {
851 let mut toggle = Toggle::new().track_width(50.0).track_height(28.0);
852 toggle.layout(Rect::new(0.0, 0.0, 50.0, 28.0));
853
854 let mut canvas = RecordingCanvas::new();
855 toggle.paint(&mut canvas);
856
857 match &canvas.commands()[0] {
858 DrawCommand::Rect { bounds, .. } => {
859 assert_eq!(bounds.width, 50.0);
860 assert_eq!(bounds.height, 28.0);
861 }
862 _ => panic!("Expected Rect command for track"),
863 }
864 }
865
866 #[test]
867 fn test_toggle_paint_thumb_size() {
868 let mut toggle = Toggle::new().thumb_size(20.0);
869 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
870
871 let mut canvas = RecordingCanvas::new();
872 toggle.paint(&mut canvas);
873
874 match &canvas.commands()[1] {
875 DrawCommand::Rect { bounds, .. } => {
876 assert_eq!(bounds.width, 20.0);
877 assert_eq!(bounds.height, 20.0);
878 }
879 _ => panic!("Expected Rect command for thumb"),
880 }
881 }
882
883 #[test]
884 fn test_toggle_paint_thumb_position_off() {
885 let mut toggle = Toggle::new()
886 .track_width(44.0)
887 .track_height(24.0)
888 .thumb_size(20.0)
889 .on(false);
890 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
891
892 let mut canvas = RecordingCanvas::new();
893 toggle.paint(&mut canvas);
894
895 let padding = (24.0 - 20.0) / 2.0; match &canvas.commands()[1] {
898 DrawCommand::Rect { bounds, .. } => {
899 assert_eq!(bounds.x, padding);
900 }
901 _ => panic!("Expected Rect command for thumb"),
902 }
903 }
904
905 #[test]
906 fn test_toggle_paint_thumb_position_on() {
907 let mut toggle = Toggle::new()
908 .track_width(44.0)
909 .track_height(24.0)
910 .thumb_size(20.0)
911 .on(true);
912 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
913
914 let mut canvas = RecordingCanvas::new();
915 toggle.paint(&mut canvas);
916
917 let padding = (24.0 - 20.0) / 2.0; let expected_x = 44.0 - 20.0 - padding; match &canvas.commands()[1] {
921 DrawCommand::Rect { bounds, .. } => {
922 assert_eq!(bounds.x, expected_x);
923 }
924 _ => panic!("Expected Rect command for thumb"),
925 }
926 }
927
928 #[test]
929 fn test_toggle_paint_thumb_color() {
930 let mut toggle = Toggle::new().thumb_color(Color::BLUE);
931 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
932
933 let mut canvas = RecordingCanvas::new();
934 toggle.paint(&mut canvas);
935
936 match &canvas.commands()[1] {
937 DrawCommand::Rect { style, .. } => {
938 assert_eq!(style.fill, Some(Color::BLUE));
939 }
940 _ => panic!("Expected Rect command for thumb"),
941 }
942 }
943
944 #[test]
945 fn test_toggle_paint_thumb_disabled_color() {
946 let mut toggle = Toggle::new().disabled(true);
947 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
948
949 let mut canvas = RecordingCanvas::new();
950 toggle.paint(&mut canvas);
951
952 match &canvas.commands()[1] {
954 DrawCommand::Rect { style, .. } => {
955 let fill = style.fill.unwrap();
956 assert!((fill.r - 0.9).abs() < 0.01);
957 assert!((fill.g - 0.9).abs() < 0.01);
958 assert!((fill.b - 0.9).abs() < 0.01);
959 }
960 _ => panic!("Expected Rect command for thumb"),
961 }
962 }
963
964 #[test]
965 fn test_toggle_paint_position_from_layout() {
966 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
967 toggle.layout(Rect::new(100.0, 50.0, 44.0, 24.0));
968
969 let mut canvas = RecordingCanvas::new();
970 toggle.paint(&mut canvas);
971
972 match &canvas.commands()[0] {
974 DrawCommand::Rect { bounds, .. } => {
975 assert_eq!(bounds.x, 100.0);
976 assert_eq!(bounds.y, 50.0);
977 }
978 _ => panic!("Expected Rect command for track"),
979 }
980 }
981
982 #[test]
983 fn test_toggle_paint_thumb_centered_vertically() {
984 let mut toggle = Toggle::new().track_height(30.0).thumb_size(20.0);
985 toggle.layout(Rect::new(0.0, 0.0, 44.0, 30.0));
986
987 let mut canvas = RecordingCanvas::new();
988 toggle.paint(&mut canvas);
989
990 let expected_y = (30.0 - 20.0) / 2.0; match &canvas.commands()[1] {
993 DrawCommand::Rect { bounds, .. } => {
994 assert_eq!(bounds.y, expected_y);
995 }
996 _ => panic!("Expected Rect command for thumb"),
997 }
998 }
999
1000 #[test]
1001 fn test_toggle_paint_custom_track_and_thumb() {
1002 let mut toggle = Toggle::new()
1003 .track_width(60.0)
1004 .track_height(32.0)
1005 .thumb_size(28.0)
1006 .track_on_color(Color::GREEN)
1007 .thumb_color(Color::WHITE)
1008 .on(true);
1009 toggle.layout(Rect::new(10.0, 20.0, 60.0, 32.0));
1010
1011 let mut canvas = RecordingCanvas::new();
1012 toggle.paint(&mut canvas);
1013
1014 match &canvas.commands()[0] {
1016 DrawCommand::Rect { bounds, style, .. } => {
1017 assert_eq!(bounds.width, 60.0);
1018 assert_eq!(bounds.height, 32.0);
1019 assert_eq!(style.fill, Some(Color::GREEN));
1020 }
1021 _ => panic!("Expected Rect command for track"),
1022 }
1023
1024 let padding = (32.0 - 28.0) / 2.0;
1026 let expected_thumb_x = 10.0 + 60.0 - 28.0 - padding;
1027 match &canvas.commands()[1] {
1028 DrawCommand::Rect { bounds, style, .. } => {
1029 assert_eq!(bounds.width, 28.0);
1030 assert_eq!(bounds.height, 28.0);
1031 assert_eq!(bounds.x, expected_thumb_x);
1032 assert_eq!(style.fill, Some(Color::WHITE));
1033 }
1034 _ => panic!("Expected Rect command for thumb"),
1035 }
1036 }
1037
1038 #[test]
1043 fn test_toggle_event_click_turns_on() {
1044 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1045 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1046
1047 assert!(!toggle.is_on());
1048 let result = toggle.event(&Event::MouseDown {
1049 position: Point::new(22.0, 12.0),
1050 button: MouseButton::Left,
1051 });
1052 assert!(toggle.is_on());
1053 assert!(result.is_some());
1054 }
1055
1056 #[test]
1057 fn test_toggle_event_click_turns_off() {
1058 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(true);
1059 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1060
1061 assert!(toggle.is_on());
1062 let result = toggle.event(&Event::MouseDown {
1063 position: Point::new(22.0, 12.0),
1064 button: MouseButton::Left,
1065 });
1066 assert!(!toggle.is_on());
1067 assert!(result.is_some());
1068 }
1069
1070 #[test]
1071 fn test_toggle_event_emits_toggle_changed() {
1072 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1073 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1074
1075 let result = toggle.event(&Event::MouseDown {
1076 position: Point::new(22.0, 12.0),
1077 button: MouseButton::Left,
1078 });
1079
1080 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1081 assert!(msg.on);
1082 }
1083
1084 #[test]
1085 fn test_toggle_event_message_reflects_new_state() {
1086 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(true);
1087 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1088
1089 let result = toggle.event(&Event::MouseDown {
1090 position: Point::new(22.0, 12.0),
1091 button: MouseButton::Left,
1092 });
1093
1094 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1095 assert!(!msg.on);
1096 }
1097
1098 #[test]
1099 fn test_toggle_event_click_outside_track_no_toggle() {
1100 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1101 toggle.layout(Rect::new(0.0, 0.0, 100.0, 24.0));
1102
1103 let result = toggle.event(&Event::MouseDown {
1105 position: Point::new(80.0, 12.0),
1106 button: MouseButton::Left,
1107 });
1108 assert!(!toggle.is_on());
1109 assert!(result.is_none());
1110 }
1111
1112 #[test]
1113 fn test_toggle_event_click_below_track_no_toggle() {
1114 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1115 toggle.layout(Rect::new(0.0, 0.0, 44.0, 50.0));
1116
1117 let result = toggle.event(&Event::MouseDown {
1119 position: Point::new(22.0, 40.0),
1120 button: MouseButton::Left,
1121 });
1122 assert!(!toggle.is_on());
1123 assert!(result.is_none());
1124 }
1125
1126 #[test]
1127 fn test_toggle_event_right_click_no_toggle() {
1128 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1129 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1130
1131 let result = toggle.event(&Event::MouseDown {
1132 position: Point::new(22.0, 12.0),
1133 button: MouseButton::Right,
1134 });
1135 assert!(!toggle.is_on());
1136 assert!(result.is_none());
1137 }
1138
1139 #[test]
1140 fn test_toggle_event_disabled_blocks_click() {
1141 let mut toggle = Toggle::new()
1142 .track_width(44.0)
1143 .track_height(24.0)
1144 .on(false)
1145 .disabled(true);
1146 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1147
1148 let result = toggle.event(&Event::MouseDown {
1149 position: Point::new(22.0, 12.0),
1150 button: MouseButton::Left,
1151 });
1152 assert!(!toggle.is_on());
1153 assert!(result.is_none());
1154 }
1155
1156 #[test]
1157 fn test_toggle_event_hit_test_track_edges() {
1158 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1159 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1160
1161 let result = toggle.event(&Event::MouseDown {
1163 position: Point::new(0.0, 0.0),
1164 button: MouseButton::Left,
1165 });
1166 assert!(toggle.is_on());
1167 assert!(result.is_some());
1168
1169 toggle.on = false;
1170
1171 let result = toggle.event(&Event::MouseDown {
1173 position: Point::new(44.0, 24.0),
1174 button: MouseButton::Left,
1175 });
1176 assert!(toggle.is_on());
1177 assert!(result.is_some());
1178 }
1179
1180 #[test]
1181 fn test_toggle_event_with_offset_bounds() {
1182 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1183 toggle.layout(Rect::new(100.0, 50.0, 44.0, 24.0));
1184
1185 let result = toggle.event(&Event::MouseDown {
1187 position: Point::new(122.0, 62.0),
1188 button: MouseButton::Left,
1189 });
1190 assert!(toggle.is_on());
1191 assert!(result.is_some());
1192 }
1193
1194 #[test]
1195 fn test_toggle_event_full_interaction_flow() {
1196 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1197 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1198
1199 assert!(!toggle.is_on());
1201
1202 let result = toggle.event(&Event::MouseDown {
1204 position: Point::new(22.0, 12.0),
1205 button: MouseButton::Left,
1206 });
1207 assert!(toggle.is_on());
1208 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1209 assert!(msg.on);
1210
1211 let result = toggle.event(&Event::MouseDown {
1213 position: Point::new(22.0, 12.0),
1214 button: MouseButton::Left,
1215 });
1216 assert!(!toggle.is_on());
1217 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1218 assert!(!msg.on);
1219
1220 let result = toggle.event(&Event::MouseDown {
1222 position: Point::new(22.0, 12.0),
1223 button: MouseButton::Left,
1224 });
1225 assert!(toggle.is_on());
1226 assert!(result.is_some());
1227 }
1228
1229 #[test]
1230 fn test_toggle_event_mouse_move_no_effect() {
1231 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1232 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1233
1234 let result = toggle.event(&Event::MouseMove {
1236 position: Point::new(22.0, 12.0),
1237 });
1238 assert!(!toggle.is_on());
1239 assert!(result.is_none());
1240 }
1241}