1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
6 MouseButton, Rect, Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct ToggleChanged {
15 pub on: bool,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Toggle {
22 on: bool,
24 disabled: bool,
26 label: String,
28 track_width: f32,
30 track_height: f32,
32 thumb_size: f32,
34 track_off_color: Color,
36 track_on_color: Color,
38 thumb_color: Color,
40 disabled_color: Color,
42 label_color: Color,
44 spacing: f32,
46 accessible_name_value: Option<String>,
48 test_id_value: Option<String>,
50 #[serde(skip)]
52 bounds: Rect,
53}
54
55impl Default for Toggle {
56 fn default() -> Self {
57 Self {
58 on: false,
59 disabled: false,
60 label: String::new(),
61 track_width: 44.0,
62 track_height: 24.0,
63 thumb_size: 20.0,
64 track_off_color: Color::new(0.7, 0.7, 0.7, 1.0),
65 track_on_color: Color::new(0.2, 0.47, 0.96, 1.0),
66 thumb_color: Color::WHITE,
67 disabled_color: Color::new(0.85, 0.85, 0.85, 1.0),
68 label_color: Color::BLACK,
69 spacing: 8.0,
70 accessible_name_value: None,
71 test_id_value: None,
72 bounds: Rect::default(),
73 }
74 }
75}
76
77impl Toggle {
78 #[must_use]
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 #[must_use]
86 pub fn with_state(on: bool) -> Self {
87 Self::default().on(on)
88 }
89
90 #[must_use]
92 pub const fn on(mut self, on: bool) -> Self {
93 self.on = on;
94 self
95 }
96
97 #[must_use]
99 pub const fn disabled(mut self, disabled: bool) -> Self {
100 self.disabled = disabled;
101 self
102 }
103
104 #[must_use]
106 pub fn label(mut self, label: impl Into<String>) -> Self {
107 self.label = label.into();
108 self
109 }
110
111 #[must_use]
113 pub fn track_width(mut self, width: f32) -> Self {
114 self.track_width = width.max(20.0);
115 self
116 }
117
118 #[must_use]
120 pub fn track_height(mut self, height: f32) -> Self {
121 self.track_height = height.max(12.0);
122 self
123 }
124
125 #[must_use]
127 pub fn thumb_size(mut self, size: f32) -> Self {
128 self.thumb_size = size.max(8.0);
129 self
130 }
131
132 #[must_use]
134 pub const fn track_off_color(mut self, color: Color) -> Self {
135 self.track_off_color = color;
136 self
137 }
138
139 #[must_use]
141 pub const fn track_on_color(mut self, color: Color) -> Self {
142 self.track_on_color = color;
143 self
144 }
145
146 #[must_use]
148 pub const fn thumb_color(mut self, color: Color) -> Self {
149 self.thumb_color = color;
150 self
151 }
152
153 #[must_use]
155 pub const fn disabled_color(mut self, color: Color) -> Self {
156 self.disabled_color = color;
157 self
158 }
159
160 #[must_use]
162 pub const fn label_color(mut self, color: Color) -> Self {
163 self.label_color = color;
164 self
165 }
166
167 #[must_use]
169 pub fn spacing(mut self, spacing: f32) -> Self {
170 self.spacing = spacing.max(0.0);
171 self
172 }
173
174 #[must_use]
176 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
177 self.accessible_name_value = Some(name.into());
178 self
179 }
180
181 #[must_use]
183 pub fn test_id(mut self, id: impl Into<String>) -> Self {
184 self.test_id_value = Some(id.into());
185 self
186 }
187
188 #[must_use]
190 pub const fn is_on(&self) -> bool {
191 self.on
192 }
193
194 #[must_use]
196 pub const fn is_disabled(&self) -> bool {
197 self.disabled
198 }
199
200 #[must_use]
202 pub fn get_label(&self) -> &str {
203 &self.label
204 }
205
206 #[must_use]
208 pub const fn get_track_width(&self) -> f32 {
209 self.track_width
210 }
211
212 #[must_use]
214 pub const fn get_track_height(&self) -> f32 {
215 self.track_height
216 }
217
218 #[must_use]
220 pub const fn get_thumb_size(&self) -> f32 {
221 self.thumb_size
222 }
223
224 #[must_use]
226 pub const fn get_spacing(&self) -> f32 {
227 self.spacing
228 }
229
230 pub fn toggle(&mut self) {
232 if !self.disabled {
233 self.on = !self.on;
234 }
235 }
236
237 pub fn set_on(&mut self, on: bool) {
239 self.on = on;
240 }
241
242 fn thumb_x(&self) -> f32 {
244 let padding = (self.track_height - self.thumb_size) / 2.0;
245 if self.on {
246 self.bounds.x + self.track_width - self.thumb_size - padding
247 } else {
248 self.bounds.x + padding
249 }
250 }
251
252 fn thumb_y(&self) -> f32 {
254 self.bounds.y + (self.track_height - self.thumb_size) / 2.0
255 }
256
257 fn hit_test(&self, x: f32, y: f32) -> bool {
259 x >= self.bounds.x
260 && x <= self.bounds.x + self.track_width
261 && y >= self.bounds.y
262 && y <= self.bounds.y + self.track_height
263 }
264}
265
266impl Widget for Toggle {
267 fn type_id(&self) -> TypeId {
268 TypeId::of::<Self>()
269 }
270
271 fn measure(&self, constraints: Constraints) -> Size {
272 let label_width = if self.label.is_empty() {
273 0.0
274 } else {
275 (self.label.len() as f32).mul_add(8.0, self.spacing)
276 };
277 let preferred = Size::new(self.track_width + label_width, self.track_height.max(20.0));
278 constraints.constrain(preferred)
279 }
280
281 fn layout(&mut self, bounds: Rect) -> LayoutResult {
282 self.bounds = bounds;
283 LayoutResult {
284 size: bounds.size(),
285 }
286 }
287
288 fn paint(&self, canvas: &mut dyn Canvas) {
289 let track_color = if self.disabled {
291 self.disabled_color
292 } else if self.on {
293 self.track_on_color
294 } else {
295 self.track_off_color
296 };
297
298 let track_rect = Rect::new(
300 self.bounds.x,
301 self.bounds.y,
302 self.track_width,
303 self.track_height,
304 );
305 canvas.fill_rect(track_rect, track_color);
306
307 let thumb_color = if self.disabled {
309 Color::new(0.9, 0.9, 0.9, 1.0)
310 } else {
311 self.thumb_color
312 };
313 let thumb_rect = Rect::new(
314 self.thumb_x(),
315 self.thumb_y(),
316 self.thumb_size,
317 self.thumb_size,
318 );
319 canvas.fill_rect(thumb_rect, thumb_color);
320 }
321
322 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
323 if self.disabled {
324 return None;
325 }
326
327 if let Event::MouseDown {
328 position,
329 button: MouseButton::Left,
330 } = event
331 {
332 if self.hit_test(position.x, position.y) {
333 self.on = !self.on;
334 return Some(Box::new(ToggleChanged { on: self.on }));
335 }
336 }
337
338 None
339 }
340
341 fn children(&self) -> &[Box<dyn Widget>] {
342 &[]
343 }
344
345 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
346 &mut []
347 }
348
349 fn is_interactive(&self) -> bool {
350 !self.disabled
351 }
352
353 fn is_focusable(&self) -> bool {
354 !self.disabled
355 }
356
357 fn accessible_name(&self) -> Option<&str> {
358 self.accessible_name_value.as_deref().or_else(|| {
359 if self.label.is_empty() {
360 None
361 } else {
362 Some(&self.label)
363 }
364 })
365 }
366
367 fn accessible_role(&self) -> AccessibleRole {
368 AccessibleRole::Checkbox
370 }
371
372 fn test_id(&self) -> Option<&str> {
373 self.test_id_value.as_deref()
374 }
375}
376
377impl Brick for Toggle {
379 fn brick_name(&self) -> &'static str {
380 "Toggle"
381 }
382
383 fn assertions(&self) -> &[BrickAssertion] {
384 &[
385 BrickAssertion::MaxLatencyMs(16),
386 BrickAssertion::ContrastRatio(3.0), ]
388 }
389
390 fn budget(&self) -> BrickBudget {
391 BrickBudget::uniform(16)
392 }
393
394 fn verify(&self) -> BrickVerification {
395 let mut passed = Vec::new();
396 let mut failed = Vec::new();
397
398 let track_color = if self.on {
400 self.track_on_color
401 } else {
402 self.track_off_color
403 };
404 let contrast = track_color.contrast_ratio(&self.thumb_color);
405 if contrast >= 3.0 {
406 passed.push(BrickAssertion::ContrastRatio(3.0));
407 } else {
408 failed.push((
409 BrickAssertion::ContrastRatio(3.0),
410 format!("Contrast ratio {contrast:.2}:1 < 3.0:1"),
411 ));
412 }
413
414 passed.push(BrickAssertion::MaxLatencyMs(16));
416
417 BrickVerification {
418 passed,
419 failed,
420 verification_time: Duration::from_micros(10),
421 }
422 }
423
424 fn to_html(&self) -> String {
425 let test_id = self.test_id_value.as_deref().unwrap_or("toggle");
426 let checked = if self.on { " checked" } else { "" };
427 let disabled = if self.disabled { " disabled" } else { "" };
428 let aria_label = self
429 .accessible_name_value
430 .as_deref()
431 .or(if self.label.is_empty() {
432 None
433 } else {
434 Some(self.label.as_str())
435 })
436 .unwrap_or("");
437 format!(
438 r#"<input type="checkbox" role="switch" class="brick-toggle" data-testid="{test_id}" aria-label="{aria_label}"{checked}{disabled} />"#
439 )
440 }
441
442 fn to_css(&self) -> String {
443 format!(
444 r".brick-toggle {{
445 appearance: none;
446 width: {}px;
447 height: {}px;
448 background: {};
449 border-radius: {}px;
450 position: relative;
451 cursor: pointer;
452}}
453.brick-toggle:checked {{
454 background: {};
455}}
456.brick-toggle::before {{
457 content: '';
458 position: absolute;
459 width: {}px;
460 height: {}px;
461 background: {};
462 border-radius: 50%;
463 top: 50%;
464 transform: translateY(-50%);
465 left: 2px;
466 transition: left 0.2s;
467}}
468.brick-toggle:checked::before {{
469 left: calc(100% - {}px - 2px);
470}}
471.brick-toggle:disabled {{
472 opacity: 0.5;
473 cursor: not-allowed;
474}}",
475 self.track_width,
476 self.track_height,
477 self.track_off_color.to_hex(),
478 self.track_height / 2.0,
479 self.track_on_color.to_hex(),
480 self.thumb_size,
481 self.thumb_size,
482 self.thumb_color.to_hex(),
483 self.thumb_size,
484 )
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use presentar_core::Point;
492
493 #[test]
496 fn test_toggle_changed_message() {
497 let msg = ToggleChanged { on: true };
498 assert!(msg.on);
499
500 let msg = ToggleChanged { on: false };
501 assert!(!msg.on);
502 }
503
504 #[test]
507 fn test_toggle_new() {
508 let toggle = Toggle::new();
509 assert!(!toggle.is_on());
510 assert!(!toggle.is_disabled());
511 }
512
513 #[test]
514 fn test_toggle_with_state_on() {
515 let toggle = Toggle::with_state(true);
516 assert!(toggle.is_on());
517 }
518
519 #[test]
520 fn test_toggle_with_state_off() {
521 let toggle = Toggle::with_state(false);
522 assert!(!toggle.is_on());
523 }
524
525 #[test]
526 fn test_toggle_default() {
527 let toggle = Toggle::default();
528 assert!(!toggle.is_on());
529 assert!(!toggle.is_disabled());
530 assert!(toggle.get_label().is_empty());
531 }
532
533 #[test]
534 fn test_toggle_builder() {
535 let toggle = Toggle::new()
536 .on(true)
537 .disabled(false)
538 .label("Dark Mode")
539 .track_width(50.0)
540 .track_height(28.0)
541 .thumb_size(24.0)
542 .track_off_color(Color::new(0.5, 0.5, 0.5, 1.0))
543 .track_on_color(Color::new(0.0, 0.8, 0.4, 1.0))
544 .thumb_color(Color::WHITE)
545 .disabled_color(Color::new(0.9, 0.9, 0.9, 1.0))
546 .label_color(Color::BLACK)
547 .spacing(12.0)
548 .accessible_name("Toggle dark mode")
549 .test_id("dark-mode-toggle");
550
551 assert!(toggle.is_on());
552 assert!(!toggle.is_disabled());
553 assert_eq!(toggle.get_label(), "Dark Mode");
554 assert_eq!(toggle.get_track_width(), 50.0);
555 assert_eq!(toggle.get_track_height(), 28.0);
556 assert_eq!(toggle.get_thumb_size(), 24.0);
557 assert_eq!(toggle.get_spacing(), 12.0);
558 assert_eq!(Widget::accessible_name(&toggle), Some("Toggle dark mode"));
559 assert_eq!(Widget::test_id(&toggle), Some("dark-mode-toggle"));
560 }
561
562 #[test]
565 fn test_toggle_on() {
566 let toggle = Toggle::new().on(true);
567 assert!(toggle.is_on());
568 }
569
570 #[test]
571 fn test_toggle_off() {
572 let toggle = Toggle::new().on(false);
573 assert!(!toggle.is_on());
574 }
575
576 #[test]
577 fn test_toggle_set_on() {
578 let mut toggle = Toggle::new();
579 toggle.set_on(true);
580 assert!(toggle.is_on());
581 toggle.set_on(false);
582 assert!(!toggle.is_on());
583 }
584
585 #[test]
586 fn test_toggle_toggle_method() {
587 let mut toggle = Toggle::new();
588 assert!(!toggle.is_on());
589 toggle.toggle();
590 assert!(toggle.is_on());
591 toggle.toggle();
592 assert!(!toggle.is_on());
593 }
594
595 #[test]
596 fn test_toggle_disabled_cannot_toggle() {
597 let mut toggle = Toggle::new().disabled(true);
598 toggle.toggle();
599 assert!(!toggle.is_on()); }
601
602 #[test]
605 fn test_toggle_track_width_min() {
606 let toggle = Toggle::new().track_width(10.0);
607 assert_eq!(toggle.get_track_width(), 20.0);
608 }
609
610 #[test]
611 fn test_toggle_track_height_min() {
612 let toggle = Toggle::new().track_height(5.0);
613 assert_eq!(toggle.get_track_height(), 12.0);
614 }
615
616 #[test]
617 fn test_toggle_thumb_size_min() {
618 let toggle = Toggle::new().thumb_size(2.0);
619 assert_eq!(toggle.get_thumb_size(), 8.0);
620 }
621
622 #[test]
623 fn test_toggle_spacing_min() {
624 let toggle = Toggle::new().spacing(-5.0);
625 assert_eq!(toggle.get_spacing(), 0.0);
626 }
627
628 #[test]
631 fn test_toggle_colors() {
632 let track_off = Color::new(0.3, 0.3, 0.3, 1.0);
633 let track_on = Color::new(0.0, 1.0, 0.5, 1.0);
634 let thumb = Color::new(1.0, 1.0, 1.0, 1.0);
635
636 let toggle = Toggle::new()
637 .track_off_color(track_off)
638 .track_on_color(track_on)
639 .thumb_color(thumb);
640
641 assert_eq!(toggle.track_off_color, track_off);
642 assert_eq!(toggle.track_on_color, track_on);
643 assert_eq!(toggle.thumb_color, thumb);
644 }
645
646 #[test]
649 fn test_toggle_thumb_position_off() {
650 let mut toggle = Toggle::new()
651 .track_width(44.0)
652 .track_height(24.0)
653 .thumb_size(20.0);
654 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
655
656 let padding = (24.0 - 20.0) / 2.0;
657 assert_eq!(toggle.thumb_x(), padding); }
659
660 #[test]
661 fn test_toggle_thumb_position_on() {
662 let mut toggle = Toggle::new()
663 .on(true)
664 .track_width(44.0)
665 .track_height(24.0)
666 .thumb_size(20.0);
667 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
668
669 let padding = (24.0 - 20.0) / 2.0;
670 assert_eq!(toggle.thumb_x(), 44.0 - 20.0 - padding); }
672
673 #[test]
674 fn test_toggle_thumb_y_centered() {
675 let mut toggle = Toggle::new().track_height(24.0).thumb_size(20.0);
676 toggle.bounds = Rect::new(10.0, 20.0, 44.0, 24.0);
677
678 assert_eq!(toggle.thumb_y(), 20.0 + (24.0 - 20.0) / 2.0);
679 }
680
681 #[test]
684 fn test_toggle_hit_test_inside() {
685 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
686 toggle.bounds = Rect::new(10.0, 10.0, 44.0, 24.0);
687
688 assert!(toggle.hit_test(20.0, 20.0));
689 assert!(toggle.hit_test(10.0, 10.0)); assert!(toggle.hit_test(54.0, 34.0)); }
692
693 #[test]
694 fn test_toggle_hit_test_outside() {
695 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
696 toggle.bounds = Rect::new(10.0, 10.0, 44.0, 24.0);
697
698 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)); }
703
704 #[test]
707 fn test_toggle_type_id() {
708 let toggle = Toggle::new();
709 assert_eq!(Widget::type_id(&toggle), TypeId::of::<Toggle>());
710 }
711
712 #[test]
713 fn test_toggle_measure_no_label() {
714 let toggle = Toggle::new().track_width(44.0).track_height(24.0);
715 let size = toggle.measure(Constraints::loose(Size::new(200.0, 100.0)));
716 assert_eq!(size.width, 44.0);
717 assert_eq!(size.height, 24.0);
718 }
719
720 #[test]
721 fn test_toggle_measure_with_label() {
722 let toggle = Toggle::new()
723 .track_width(44.0)
724 .track_height(24.0)
725 .label("On")
726 .spacing(8.0);
727 let size = toggle.measure(Constraints::loose(Size::new(200.0, 100.0)));
728 assert_eq!(size.width, 44.0 + 8.0 + 16.0);
730 }
731
732 #[test]
733 fn test_toggle_layout() {
734 let mut toggle = Toggle::new();
735 let bounds = Rect::new(10.0, 20.0, 44.0, 24.0);
736 let result = toggle.layout(bounds);
737 assert_eq!(result.size, Size::new(44.0, 24.0));
738 assert_eq!(toggle.bounds, bounds);
739 }
740
741 #[test]
742 fn test_toggle_children() {
743 let toggle = Toggle::new();
744 assert!(toggle.children().is_empty());
745 }
746
747 #[test]
748 fn test_toggle_is_interactive() {
749 let toggle = Toggle::new();
750 assert!(toggle.is_interactive());
751
752 let toggle = Toggle::new().disabled(true);
753 assert!(!toggle.is_interactive());
754 }
755
756 #[test]
757 fn test_toggle_is_focusable() {
758 let toggle = Toggle::new();
759 assert!(toggle.is_focusable());
760
761 let toggle = Toggle::new().disabled(true);
762 assert!(!toggle.is_focusable());
763 }
764
765 #[test]
766 fn test_toggle_accessible_role() {
767 let toggle = Toggle::new();
768 assert_eq!(toggle.accessible_role(), AccessibleRole::Checkbox);
769 }
770
771 #[test]
772 fn test_toggle_accessible_name_from_label() {
773 let toggle = Toggle::new().label("Enable notifications");
774 assert_eq!(
775 Widget::accessible_name(&toggle),
776 Some("Enable notifications")
777 );
778 }
779
780 #[test]
781 fn test_toggle_accessible_name_override() {
782 let toggle = Toggle::new()
783 .label("Notifications")
784 .accessible_name("Toggle notifications on or off");
785 assert_eq!(
786 Widget::accessible_name(&toggle),
787 Some("Toggle notifications on or off")
788 );
789 }
790
791 #[test]
792 fn test_toggle_accessible_name_none() {
793 let toggle = Toggle::new();
794 assert_eq!(Widget::accessible_name(&toggle), None);
795 }
796
797 #[test]
798 fn test_toggle_test_id() {
799 let toggle = Toggle::new().test_id("settings-toggle");
800 assert_eq!(Widget::test_id(&toggle), Some("settings-toggle"));
801 }
802
803 #[test]
806 fn test_toggle_click_toggles_state() {
807 let mut toggle = Toggle::new();
808 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
809
810 let event = Event::MouseDown {
811 position: Point::new(22.0, 12.0),
812 button: MouseButton::Left,
813 };
814
815 let result = toggle.event(&event);
816 assert!(result.is_some());
817 assert!(toggle.is_on());
818
819 let result = toggle.event(&event);
820 assert!(result.is_some());
821 assert!(!toggle.is_on());
822 }
823
824 #[test]
825 fn test_toggle_click_outside_no_effect() {
826 let mut toggle = Toggle::new();
827 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
828
829 let event = Event::MouseDown {
830 position: Point::new(100.0, 100.0),
831 button: MouseButton::Left,
832 };
833
834 let result = toggle.event(&event);
835 assert!(result.is_none());
836 assert!(!toggle.is_on());
837 }
838
839 #[test]
840 fn test_toggle_right_click_no_effect() {
841 let mut toggle = Toggle::new();
842 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
843
844 let event = Event::MouseDown {
845 position: Point::new(22.0, 12.0),
846 button: MouseButton::Right,
847 };
848
849 let result = toggle.event(&event);
850 assert!(result.is_none());
851 assert!(!toggle.is_on());
852 }
853
854 #[test]
855 fn test_toggle_disabled_click_no_effect() {
856 let mut toggle = Toggle::new().disabled(true);
857 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
858
859 let event = Event::MouseDown {
860 position: Point::new(22.0, 12.0),
861 button: MouseButton::Left,
862 };
863
864 let result = toggle.event(&event);
865 assert!(result.is_none());
866 assert!(!toggle.is_on());
867 }
868
869 #[test]
870 fn test_toggle_changed_contains_new_state() {
871 let mut toggle = Toggle::new();
872 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
873
874 let event = Event::MouseDown {
875 position: Point::new(22.0, 12.0),
876 button: MouseButton::Left,
877 };
878
879 let result = toggle.event(&event);
880 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
881 assert!(msg.on);
882
883 let result = toggle.event(&event);
884 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
885 assert!(!msg.on);
886 }
887
888 use presentar_core::draw::DrawCommand;
891 use presentar_core::RecordingCanvas;
892
893 #[test]
894 fn test_toggle_paint_draws_track_and_thumb() {
895 let mut toggle = Toggle::new()
896 .track_width(44.0)
897 .track_height(24.0)
898 .thumb_size(20.0);
899 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
900
901 let mut canvas = RecordingCanvas::new();
902 toggle.paint(&mut canvas);
903
904 assert_eq!(canvas.command_count(), 2);
906 }
907
908 #[test]
909 fn test_toggle_paint_track_off_color() {
910 let mut toggle = Toggle::new().track_off_color(Color::RED).on(false);
911 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
912
913 let mut canvas = RecordingCanvas::new();
914 toggle.paint(&mut canvas);
915
916 match &canvas.commands()[0] {
918 DrawCommand::Rect { style, .. } => {
919 assert_eq!(style.fill, Some(Color::RED));
920 }
921 _ => panic!("Expected Rect command for track"),
922 }
923 }
924
925 #[test]
926 fn test_toggle_paint_track_on_color() {
927 let mut toggle = Toggle::new().track_on_color(Color::GREEN).on(true);
928 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
929
930 let mut canvas = RecordingCanvas::new();
931 toggle.paint(&mut canvas);
932
933 match &canvas.commands()[0] {
935 DrawCommand::Rect { style, .. } => {
936 assert_eq!(style.fill, Some(Color::GREEN));
937 }
938 _ => panic!("Expected Rect command for track"),
939 }
940 }
941
942 #[test]
943 fn test_toggle_paint_track_disabled_color() {
944 let mut toggle = Toggle::new()
945 .disabled_color(Color::new(0.85, 0.85, 0.85, 1.0))
946 .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()[0] {
954 DrawCommand::Rect { style, .. } => {
955 let fill = style.fill.unwrap();
956 assert!((fill.r - 0.85).abs() < 0.01);
957 }
958 _ => panic!("Expected Rect command for track"),
959 }
960 }
961
962 #[test]
963 fn test_toggle_paint_track_dimensions() {
964 let mut toggle = Toggle::new().track_width(50.0).track_height(28.0);
965 toggle.layout(Rect::new(0.0, 0.0, 50.0, 28.0));
966
967 let mut canvas = RecordingCanvas::new();
968 toggle.paint(&mut canvas);
969
970 match &canvas.commands()[0] {
971 DrawCommand::Rect { bounds, .. } => {
972 assert_eq!(bounds.width, 50.0);
973 assert_eq!(bounds.height, 28.0);
974 }
975 _ => panic!("Expected Rect command for track"),
976 }
977 }
978
979 #[test]
980 fn test_toggle_paint_thumb_size() {
981 let mut toggle = Toggle::new().thumb_size(20.0);
982 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
983
984 let mut canvas = RecordingCanvas::new();
985 toggle.paint(&mut canvas);
986
987 match &canvas.commands()[1] {
988 DrawCommand::Rect { bounds, .. } => {
989 assert_eq!(bounds.width, 20.0);
990 assert_eq!(bounds.height, 20.0);
991 }
992 _ => panic!("Expected Rect command for thumb"),
993 }
994 }
995
996 #[test]
997 fn test_toggle_paint_thumb_position_off() {
998 let mut toggle = Toggle::new()
999 .track_width(44.0)
1000 .track_height(24.0)
1001 .thumb_size(20.0)
1002 .on(false);
1003 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1004
1005 let mut canvas = RecordingCanvas::new();
1006 toggle.paint(&mut canvas);
1007
1008 let padding = (24.0 - 20.0) / 2.0; match &canvas.commands()[1] {
1011 DrawCommand::Rect { bounds, .. } => {
1012 assert_eq!(bounds.x, padding);
1013 }
1014 _ => panic!("Expected Rect command for thumb"),
1015 }
1016 }
1017
1018 #[test]
1019 fn test_toggle_paint_thumb_position_on() {
1020 let mut toggle = Toggle::new()
1021 .track_width(44.0)
1022 .track_height(24.0)
1023 .thumb_size(20.0)
1024 .on(true);
1025 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1026
1027 let mut canvas = RecordingCanvas::new();
1028 toggle.paint(&mut canvas);
1029
1030 let padding = (24.0 - 20.0) / 2.0; let expected_x = 44.0 - 20.0 - padding; match &canvas.commands()[1] {
1034 DrawCommand::Rect { bounds, .. } => {
1035 assert_eq!(bounds.x, expected_x);
1036 }
1037 _ => panic!("Expected Rect command for thumb"),
1038 }
1039 }
1040
1041 #[test]
1042 fn test_toggle_paint_thumb_color() {
1043 let mut toggle = Toggle::new().thumb_color(Color::BLUE);
1044 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1045
1046 let mut canvas = RecordingCanvas::new();
1047 toggle.paint(&mut canvas);
1048
1049 match &canvas.commands()[1] {
1050 DrawCommand::Rect { style, .. } => {
1051 assert_eq!(style.fill, Some(Color::BLUE));
1052 }
1053 _ => panic!("Expected Rect command for thumb"),
1054 }
1055 }
1056
1057 #[test]
1058 fn test_toggle_paint_thumb_disabled_color() {
1059 let mut toggle = Toggle::new().disabled(true);
1060 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1061
1062 let mut canvas = RecordingCanvas::new();
1063 toggle.paint(&mut canvas);
1064
1065 match &canvas.commands()[1] {
1067 DrawCommand::Rect { style, .. } => {
1068 let fill = style.fill.unwrap();
1069 assert!((fill.r - 0.9).abs() < 0.01);
1070 assert!((fill.g - 0.9).abs() < 0.01);
1071 assert!((fill.b - 0.9).abs() < 0.01);
1072 }
1073 _ => panic!("Expected Rect command for thumb"),
1074 }
1075 }
1076
1077 #[test]
1078 fn test_toggle_paint_position_from_layout() {
1079 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
1080 toggle.layout(Rect::new(100.0, 50.0, 44.0, 24.0));
1081
1082 let mut canvas = RecordingCanvas::new();
1083 toggle.paint(&mut canvas);
1084
1085 match &canvas.commands()[0] {
1087 DrawCommand::Rect { bounds, .. } => {
1088 assert_eq!(bounds.x, 100.0);
1089 assert_eq!(bounds.y, 50.0);
1090 }
1091 _ => panic!("Expected Rect command for track"),
1092 }
1093 }
1094
1095 #[test]
1096 fn test_toggle_paint_thumb_centered_vertically() {
1097 let mut toggle = Toggle::new().track_height(30.0).thumb_size(20.0);
1098 toggle.layout(Rect::new(0.0, 0.0, 44.0, 30.0));
1099
1100 let mut canvas = RecordingCanvas::new();
1101 toggle.paint(&mut canvas);
1102
1103 let expected_y = (30.0 - 20.0) / 2.0; match &canvas.commands()[1] {
1106 DrawCommand::Rect { bounds, .. } => {
1107 assert_eq!(bounds.y, expected_y);
1108 }
1109 _ => panic!("Expected Rect command for thumb"),
1110 }
1111 }
1112
1113 #[test]
1114 fn test_toggle_paint_custom_track_and_thumb() {
1115 let mut toggle = Toggle::new()
1116 .track_width(60.0)
1117 .track_height(32.0)
1118 .thumb_size(28.0)
1119 .track_on_color(Color::GREEN)
1120 .thumb_color(Color::WHITE)
1121 .on(true);
1122 toggle.layout(Rect::new(10.0, 20.0, 60.0, 32.0));
1123
1124 let mut canvas = RecordingCanvas::new();
1125 toggle.paint(&mut canvas);
1126
1127 match &canvas.commands()[0] {
1129 DrawCommand::Rect { bounds, style, .. } => {
1130 assert_eq!(bounds.width, 60.0);
1131 assert_eq!(bounds.height, 32.0);
1132 assert_eq!(style.fill, Some(Color::GREEN));
1133 }
1134 _ => panic!("Expected Rect command for track"),
1135 }
1136
1137 let padding = (32.0 - 28.0) / 2.0;
1139 let expected_thumb_x = 10.0 + 60.0 - 28.0 - padding;
1140 match &canvas.commands()[1] {
1141 DrawCommand::Rect { bounds, style, .. } => {
1142 assert_eq!(bounds.width, 28.0);
1143 assert_eq!(bounds.height, 28.0);
1144 assert_eq!(bounds.x, expected_thumb_x);
1145 assert_eq!(style.fill, Some(Color::WHITE));
1146 }
1147 _ => panic!("Expected Rect command for thumb"),
1148 }
1149 }
1150
1151 #[test]
1156 fn test_toggle_event_click_turns_on() {
1157 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1158 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1159
1160 assert!(!toggle.is_on());
1161 let result = toggle.event(&Event::MouseDown {
1162 position: Point::new(22.0, 12.0),
1163 button: MouseButton::Left,
1164 });
1165 assert!(toggle.is_on());
1166 assert!(result.is_some());
1167 }
1168
1169 #[test]
1170 fn test_toggle_event_click_turns_off() {
1171 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(true);
1172 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1173
1174 assert!(toggle.is_on());
1175 let result = toggle.event(&Event::MouseDown {
1176 position: Point::new(22.0, 12.0),
1177 button: MouseButton::Left,
1178 });
1179 assert!(!toggle.is_on());
1180 assert!(result.is_some());
1181 }
1182
1183 #[test]
1184 fn test_toggle_event_emits_toggle_changed() {
1185 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1186 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1187
1188 let result = toggle.event(&Event::MouseDown {
1189 position: Point::new(22.0, 12.0),
1190 button: MouseButton::Left,
1191 });
1192
1193 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1194 assert!(msg.on);
1195 }
1196
1197 #[test]
1198 fn test_toggle_event_message_reflects_new_state() {
1199 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(true);
1200 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1201
1202 let result = toggle.event(&Event::MouseDown {
1203 position: Point::new(22.0, 12.0),
1204 button: MouseButton::Left,
1205 });
1206
1207 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1208 assert!(!msg.on);
1209 }
1210
1211 #[test]
1212 fn test_toggle_event_click_outside_track_no_toggle() {
1213 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1214 toggle.layout(Rect::new(0.0, 0.0, 100.0, 24.0));
1215
1216 let result = toggle.event(&Event::MouseDown {
1218 position: Point::new(80.0, 12.0),
1219 button: MouseButton::Left,
1220 });
1221 assert!(!toggle.is_on());
1222 assert!(result.is_none());
1223 }
1224
1225 #[test]
1226 fn test_toggle_event_click_below_track_no_toggle() {
1227 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1228 toggle.layout(Rect::new(0.0, 0.0, 44.0, 50.0));
1229
1230 let result = toggle.event(&Event::MouseDown {
1232 position: Point::new(22.0, 40.0),
1233 button: MouseButton::Left,
1234 });
1235 assert!(!toggle.is_on());
1236 assert!(result.is_none());
1237 }
1238
1239 #[test]
1240 fn test_toggle_event_right_click_no_toggle() {
1241 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1242 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1243
1244 let result = toggle.event(&Event::MouseDown {
1245 position: Point::new(22.0, 12.0),
1246 button: MouseButton::Right,
1247 });
1248 assert!(!toggle.is_on());
1249 assert!(result.is_none());
1250 }
1251
1252 #[test]
1253 fn test_toggle_event_disabled_blocks_click() {
1254 let mut toggle = Toggle::new()
1255 .track_width(44.0)
1256 .track_height(24.0)
1257 .on(false)
1258 .disabled(true);
1259 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1260
1261 let result = toggle.event(&Event::MouseDown {
1262 position: Point::new(22.0, 12.0),
1263 button: MouseButton::Left,
1264 });
1265 assert!(!toggle.is_on());
1266 assert!(result.is_none());
1267 }
1268
1269 #[test]
1270 fn test_toggle_event_hit_test_track_edges() {
1271 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1272 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1273
1274 let result = toggle.event(&Event::MouseDown {
1276 position: Point::new(0.0, 0.0),
1277 button: MouseButton::Left,
1278 });
1279 assert!(toggle.is_on());
1280 assert!(result.is_some());
1281
1282 toggle.on = false;
1283
1284 let result = toggle.event(&Event::MouseDown {
1286 position: Point::new(44.0, 24.0),
1287 button: MouseButton::Left,
1288 });
1289 assert!(toggle.is_on());
1290 assert!(result.is_some());
1291 }
1292
1293 #[test]
1294 fn test_toggle_event_with_offset_bounds() {
1295 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1296 toggle.layout(Rect::new(100.0, 50.0, 44.0, 24.0));
1297
1298 let result = toggle.event(&Event::MouseDown {
1300 position: Point::new(122.0, 62.0),
1301 button: MouseButton::Left,
1302 });
1303 assert!(toggle.is_on());
1304 assert!(result.is_some());
1305 }
1306
1307 #[test]
1308 fn test_toggle_event_full_interaction_flow() {
1309 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1310 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1311
1312 assert!(!toggle.is_on());
1314
1315 let result = toggle.event(&Event::MouseDown {
1317 position: Point::new(22.0, 12.0),
1318 button: MouseButton::Left,
1319 });
1320 assert!(toggle.is_on());
1321 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1322 assert!(msg.on);
1323
1324 let result = toggle.event(&Event::MouseDown {
1326 position: Point::new(22.0, 12.0),
1327 button: MouseButton::Left,
1328 });
1329 assert!(!toggle.is_on());
1330 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1331 assert!(!msg.on);
1332
1333 let result = toggle.event(&Event::MouseDown {
1335 position: Point::new(22.0, 12.0),
1336 button: MouseButton::Left,
1337 });
1338 assert!(toggle.is_on());
1339 assert!(result.is_some());
1340 }
1341
1342 #[test]
1343 fn test_toggle_event_mouse_move_no_effect() {
1344 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1345 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1346
1347 let result = toggle.event(&Event::MouseMove {
1349 position: Point::new(22.0, 12.0),
1350 });
1351 assert!(!toggle.is_on());
1352 assert!(result.is_none());
1353 }
1354}