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)]
489#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
490mod tests {
491 use super::*;
492 use presentar_core::Point;
493
494 #[test]
497 fn test_toggle_changed_message() {
498 let msg = ToggleChanged { on: true };
499 assert!(msg.on);
500
501 let msg = ToggleChanged { on: false };
502 assert!(!msg.on);
503 }
504
505 #[test]
508 fn test_toggle_new() {
509 let toggle = Toggle::new();
510 assert!(!toggle.is_on());
511 assert!(!toggle.is_disabled());
512 }
513
514 #[test]
515 fn test_toggle_with_state_on() {
516 let toggle = Toggle::with_state(true);
517 assert!(toggle.is_on());
518 }
519
520 #[test]
521 fn test_toggle_with_state_off() {
522 let toggle = Toggle::with_state(false);
523 assert!(!toggle.is_on());
524 }
525
526 #[test]
527 fn test_toggle_default() {
528 let toggle = Toggle::default();
529 assert!(!toggle.is_on());
530 assert!(!toggle.is_disabled());
531 assert!(toggle.get_label().is_empty());
532 }
533
534 #[test]
535 fn test_toggle_builder() {
536 let toggle = Toggle::new()
537 .on(true)
538 .disabled(false)
539 .label("Dark Mode")
540 .track_width(50.0)
541 .track_height(28.0)
542 .thumb_size(24.0)
543 .track_off_color(Color::new(0.5, 0.5, 0.5, 1.0))
544 .track_on_color(Color::new(0.0, 0.8, 0.4, 1.0))
545 .thumb_color(Color::WHITE)
546 .disabled_color(Color::new(0.9, 0.9, 0.9, 1.0))
547 .label_color(Color::BLACK)
548 .spacing(12.0)
549 .accessible_name("Toggle dark mode")
550 .test_id("dark-mode-toggle");
551
552 assert!(toggle.is_on());
553 assert!(!toggle.is_disabled());
554 assert_eq!(toggle.get_label(), "Dark Mode");
555 assert_eq!(toggle.get_track_width(), 50.0);
556 assert_eq!(toggle.get_track_height(), 28.0);
557 assert_eq!(toggle.get_thumb_size(), 24.0);
558 assert_eq!(toggle.get_spacing(), 12.0);
559 assert_eq!(Widget::accessible_name(&toggle), Some("Toggle dark mode"));
560 assert_eq!(Widget::test_id(&toggle), Some("dark-mode-toggle"));
561 }
562
563 #[test]
566 fn test_toggle_on() {
567 let toggle = Toggle::new().on(true);
568 assert!(toggle.is_on());
569 }
570
571 #[test]
572 fn test_toggle_off() {
573 let toggle = Toggle::new().on(false);
574 assert!(!toggle.is_on());
575 }
576
577 #[test]
578 fn test_toggle_set_on() {
579 let mut toggle = Toggle::new();
580 toggle.set_on(true);
581 assert!(toggle.is_on());
582 toggle.set_on(false);
583 assert!(!toggle.is_on());
584 }
585
586 #[test]
587 fn test_toggle_toggle_method() {
588 let mut toggle = Toggle::new();
589 assert!(!toggle.is_on());
590 toggle.toggle();
591 assert!(toggle.is_on());
592 toggle.toggle();
593 assert!(!toggle.is_on());
594 }
595
596 #[test]
597 fn test_toggle_disabled_cannot_toggle() {
598 let mut toggle = Toggle::new().disabled(true);
599 toggle.toggle();
600 assert!(!toggle.is_on()); }
602
603 #[test]
606 fn test_toggle_track_width_min() {
607 let toggle = Toggle::new().track_width(10.0);
608 assert_eq!(toggle.get_track_width(), 20.0);
609 }
610
611 #[test]
612 fn test_toggle_track_height_min() {
613 let toggle = Toggle::new().track_height(5.0);
614 assert_eq!(toggle.get_track_height(), 12.0);
615 }
616
617 #[test]
618 fn test_toggle_thumb_size_min() {
619 let toggle = Toggle::new().thumb_size(2.0);
620 assert_eq!(toggle.get_thumb_size(), 8.0);
621 }
622
623 #[test]
624 fn test_toggle_spacing_min() {
625 let toggle = Toggle::new().spacing(-5.0);
626 assert_eq!(toggle.get_spacing(), 0.0);
627 }
628
629 #[test]
632 fn test_toggle_colors() {
633 let track_off = Color::new(0.3, 0.3, 0.3, 1.0);
634 let track_on = Color::new(0.0, 1.0, 0.5, 1.0);
635 let thumb = Color::new(1.0, 1.0, 1.0, 1.0);
636
637 let toggle = Toggle::new()
638 .track_off_color(track_off)
639 .track_on_color(track_on)
640 .thumb_color(thumb);
641
642 assert_eq!(toggle.track_off_color, track_off);
643 assert_eq!(toggle.track_on_color, track_on);
644 assert_eq!(toggle.thumb_color, thumb);
645 }
646
647 #[test]
650 fn test_toggle_thumb_position_off() {
651 let mut toggle = Toggle::new()
652 .track_width(44.0)
653 .track_height(24.0)
654 .thumb_size(20.0);
655 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
656
657 let padding = (24.0 - 20.0) / 2.0;
658 assert_eq!(toggle.thumb_x(), padding); }
660
661 #[test]
662 fn test_toggle_thumb_position_on() {
663 let mut toggle = Toggle::new()
664 .on(true)
665 .track_width(44.0)
666 .track_height(24.0)
667 .thumb_size(20.0);
668 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
669
670 let padding = (24.0 - 20.0) / 2.0;
671 assert_eq!(toggle.thumb_x(), 44.0 - 20.0 - padding); }
673
674 #[test]
675 fn test_toggle_thumb_y_centered() {
676 let mut toggle = Toggle::new().track_height(24.0).thumb_size(20.0);
677 toggle.bounds = Rect::new(10.0, 20.0, 44.0, 24.0);
678
679 assert_eq!(toggle.thumb_y(), 20.0 + (24.0 - 20.0) / 2.0);
680 }
681
682 #[test]
685 fn test_toggle_hit_test_inside() {
686 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
687 toggle.bounds = Rect::new(10.0, 10.0, 44.0, 24.0);
688
689 assert!(toggle.hit_test(20.0, 20.0));
690 assert!(toggle.hit_test(10.0, 10.0)); assert!(toggle.hit_test(54.0, 34.0)); }
693
694 #[test]
695 fn test_toggle_hit_test_outside() {
696 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
697 toggle.bounds = Rect::new(10.0, 10.0, 44.0, 24.0);
698
699 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)); }
704
705 #[test]
708 fn test_toggle_type_id() {
709 let toggle = Toggle::new();
710 assert_eq!(Widget::type_id(&toggle), TypeId::of::<Toggle>());
711 }
712
713 #[test]
714 fn test_toggle_measure_no_label() {
715 let toggle = Toggle::new().track_width(44.0).track_height(24.0);
716 let size = toggle.measure(Constraints::loose(Size::new(200.0, 100.0)));
717 assert_eq!(size.width, 44.0);
718 assert_eq!(size.height, 24.0);
719 }
720
721 #[test]
722 fn test_toggle_measure_with_label() {
723 let toggle = Toggle::new()
724 .track_width(44.0)
725 .track_height(24.0)
726 .label("On")
727 .spacing(8.0);
728 let size = toggle.measure(Constraints::loose(Size::new(200.0, 100.0)));
729 assert_eq!(size.width, 44.0 + 8.0 + 16.0);
731 }
732
733 #[test]
734 fn test_toggle_layout() {
735 let mut toggle = Toggle::new();
736 let bounds = Rect::new(10.0, 20.0, 44.0, 24.0);
737 let result = toggle.layout(bounds);
738 assert_eq!(result.size, Size::new(44.0, 24.0));
739 assert_eq!(toggle.bounds, bounds);
740 }
741
742 #[test]
743 fn test_toggle_children() {
744 let toggle = Toggle::new();
745 assert!(toggle.children().is_empty());
746 }
747
748 #[test]
749 fn test_toggle_is_interactive() {
750 let toggle = Toggle::new();
751 assert!(toggle.is_interactive());
752
753 let toggle = Toggle::new().disabled(true);
754 assert!(!toggle.is_interactive());
755 }
756
757 #[test]
758 fn test_toggle_is_focusable() {
759 let toggle = Toggle::new();
760 assert!(toggle.is_focusable());
761
762 let toggle = Toggle::new().disabled(true);
763 assert!(!toggle.is_focusable());
764 }
765
766 #[test]
767 fn test_toggle_accessible_role() {
768 let toggle = Toggle::new();
769 assert_eq!(toggle.accessible_role(), AccessibleRole::Checkbox);
770 }
771
772 #[test]
773 fn test_toggle_accessible_name_from_label() {
774 let toggle = Toggle::new().label("Enable notifications");
775 assert_eq!(
776 Widget::accessible_name(&toggle),
777 Some("Enable notifications")
778 );
779 }
780
781 #[test]
782 fn test_toggle_accessible_name_override() {
783 let toggle = Toggle::new()
784 .label("Notifications")
785 .accessible_name("Toggle notifications on or off");
786 assert_eq!(
787 Widget::accessible_name(&toggle),
788 Some("Toggle notifications on or off")
789 );
790 }
791
792 #[test]
793 fn test_toggle_accessible_name_none() {
794 let toggle = Toggle::new();
795 assert_eq!(Widget::accessible_name(&toggle), None);
796 }
797
798 #[test]
799 fn test_toggle_test_id() {
800 let toggle = Toggle::new().test_id("settings-toggle");
801 assert_eq!(Widget::test_id(&toggle), Some("settings-toggle"));
802 }
803
804 #[test]
807 fn test_toggle_click_toggles_state() {
808 let mut toggle = Toggle::new();
809 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
810
811 let event = Event::MouseDown {
812 position: Point::new(22.0, 12.0),
813 button: MouseButton::Left,
814 };
815
816 let result = toggle.event(&event);
817 assert!(result.is_some());
818 assert!(toggle.is_on());
819
820 let result = toggle.event(&event);
821 assert!(result.is_some());
822 assert!(!toggle.is_on());
823 }
824
825 #[test]
826 fn test_toggle_click_outside_no_effect() {
827 let mut toggle = Toggle::new();
828 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
829
830 let event = Event::MouseDown {
831 position: Point::new(100.0, 100.0),
832 button: MouseButton::Left,
833 };
834
835 let result = toggle.event(&event);
836 assert!(result.is_none());
837 assert!(!toggle.is_on());
838 }
839
840 #[test]
841 fn test_toggle_right_click_no_effect() {
842 let mut toggle = Toggle::new();
843 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
844
845 let event = Event::MouseDown {
846 position: Point::new(22.0, 12.0),
847 button: MouseButton::Right,
848 };
849
850 let result = toggle.event(&event);
851 assert!(result.is_none());
852 assert!(!toggle.is_on());
853 }
854
855 #[test]
856 fn test_toggle_disabled_click_no_effect() {
857 let mut toggle = Toggle::new().disabled(true);
858 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
859
860 let event = Event::MouseDown {
861 position: Point::new(22.0, 12.0),
862 button: MouseButton::Left,
863 };
864
865 let result = toggle.event(&event);
866 assert!(result.is_none());
867 assert!(!toggle.is_on());
868 }
869
870 #[test]
871 fn test_toggle_changed_contains_new_state() {
872 let mut toggle = Toggle::new();
873 toggle.bounds = Rect::new(0.0, 0.0, 44.0, 24.0);
874
875 let event = Event::MouseDown {
876 position: Point::new(22.0, 12.0),
877 button: MouseButton::Left,
878 };
879
880 let result = toggle.event(&event);
881 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
882 assert!(msg.on);
883
884 let result = toggle.event(&event);
885 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
886 assert!(!msg.on);
887 }
888
889 use presentar_core::draw::DrawCommand;
892 use presentar_core::RecordingCanvas;
893
894 #[test]
895 fn test_toggle_paint_draws_track_and_thumb() {
896 let mut toggle = Toggle::new()
897 .track_width(44.0)
898 .track_height(24.0)
899 .thumb_size(20.0);
900 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
901
902 let mut canvas = RecordingCanvas::new();
903 toggle.paint(&mut canvas);
904
905 assert_eq!(canvas.command_count(), 2);
907 }
908
909 #[test]
910 fn test_toggle_paint_track_off_color() {
911 let mut toggle = Toggle::new().track_off_color(Color::RED).on(false);
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 match &canvas.commands()[0] {
919 DrawCommand::Rect { style, .. } => {
920 assert_eq!(style.fill, Some(Color::RED));
921 }
922 _ => panic!("Expected Rect command for track"),
923 }
924 }
925
926 #[test]
927 fn test_toggle_paint_track_on_color() {
928 let mut toggle = Toggle::new().track_on_color(Color::GREEN).on(true);
929 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
930
931 let mut canvas = RecordingCanvas::new();
932 toggle.paint(&mut canvas);
933
934 match &canvas.commands()[0] {
936 DrawCommand::Rect { style, .. } => {
937 assert_eq!(style.fill, Some(Color::GREEN));
938 }
939 _ => panic!("Expected Rect command for track"),
940 }
941 }
942
943 #[test]
944 fn test_toggle_paint_track_disabled_color() {
945 let mut toggle = Toggle::new()
946 .disabled_color(Color::new(0.85, 0.85, 0.85, 1.0))
947 .disabled(true);
948 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
949
950 let mut canvas = RecordingCanvas::new();
951 toggle.paint(&mut canvas);
952
953 match &canvas.commands()[0] {
955 DrawCommand::Rect { style, .. } => {
956 let fill = style.fill.unwrap();
957 assert!((fill.r - 0.85).abs() < 0.01);
958 }
959 _ => panic!("Expected Rect command for track"),
960 }
961 }
962
963 #[test]
964 fn test_toggle_paint_track_dimensions() {
965 let mut toggle = Toggle::new().track_width(50.0).track_height(28.0);
966 toggle.layout(Rect::new(0.0, 0.0, 50.0, 28.0));
967
968 let mut canvas = RecordingCanvas::new();
969 toggle.paint(&mut canvas);
970
971 match &canvas.commands()[0] {
972 DrawCommand::Rect { bounds, .. } => {
973 assert_eq!(bounds.width, 50.0);
974 assert_eq!(bounds.height, 28.0);
975 }
976 _ => panic!("Expected Rect command for track"),
977 }
978 }
979
980 #[test]
981 fn test_toggle_paint_thumb_size() {
982 let mut toggle = Toggle::new().thumb_size(20.0);
983 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
984
985 let mut canvas = RecordingCanvas::new();
986 toggle.paint(&mut canvas);
987
988 match &canvas.commands()[1] {
989 DrawCommand::Rect { bounds, .. } => {
990 assert_eq!(bounds.width, 20.0);
991 assert_eq!(bounds.height, 20.0);
992 }
993 _ => panic!("Expected Rect command for thumb"),
994 }
995 }
996
997 #[test]
998 fn test_toggle_paint_thumb_position_off() {
999 let mut toggle = Toggle::new()
1000 .track_width(44.0)
1001 .track_height(24.0)
1002 .thumb_size(20.0)
1003 .on(false);
1004 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1005
1006 let mut canvas = RecordingCanvas::new();
1007 toggle.paint(&mut canvas);
1008
1009 let padding = (24.0 - 20.0) / 2.0; match &canvas.commands()[1] {
1012 DrawCommand::Rect { bounds, .. } => {
1013 assert_eq!(bounds.x, padding);
1014 }
1015 _ => panic!("Expected Rect command for thumb"),
1016 }
1017 }
1018
1019 #[test]
1020 fn test_toggle_paint_thumb_position_on() {
1021 let mut toggle = Toggle::new()
1022 .track_width(44.0)
1023 .track_height(24.0)
1024 .thumb_size(20.0)
1025 .on(true);
1026 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1027
1028 let mut canvas = RecordingCanvas::new();
1029 toggle.paint(&mut canvas);
1030
1031 let padding = (24.0 - 20.0) / 2.0; let expected_x = 44.0 - 20.0 - padding; match &canvas.commands()[1] {
1035 DrawCommand::Rect { bounds, .. } => {
1036 assert_eq!(bounds.x, expected_x);
1037 }
1038 _ => panic!("Expected Rect command for thumb"),
1039 }
1040 }
1041
1042 #[test]
1043 fn test_toggle_paint_thumb_color() {
1044 let mut toggle = Toggle::new().thumb_color(Color::BLUE);
1045 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1046
1047 let mut canvas = RecordingCanvas::new();
1048 toggle.paint(&mut canvas);
1049
1050 match &canvas.commands()[1] {
1051 DrawCommand::Rect { style, .. } => {
1052 assert_eq!(style.fill, Some(Color::BLUE));
1053 }
1054 _ => panic!("Expected Rect command for thumb"),
1055 }
1056 }
1057
1058 #[test]
1059 fn test_toggle_paint_thumb_disabled_color() {
1060 let mut toggle = Toggle::new().disabled(true);
1061 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1062
1063 let mut canvas = RecordingCanvas::new();
1064 toggle.paint(&mut canvas);
1065
1066 match &canvas.commands()[1] {
1068 DrawCommand::Rect { style, .. } => {
1069 let fill = style.fill.unwrap();
1070 assert!((fill.r - 0.9).abs() < 0.01);
1071 assert!((fill.g - 0.9).abs() < 0.01);
1072 assert!((fill.b - 0.9).abs() < 0.01);
1073 }
1074 _ => panic!("Expected Rect command for thumb"),
1075 }
1076 }
1077
1078 #[test]
1079 fn test_toggle_paint_position_from_layout() {
1080 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0);
1081 toggle.layout(Rect::new(100.0, 50.0, 44.0, 24.0));
1082
1083 let mut canvas = RecordingCanvas::new();
1084 toggle.paint(&mut canvas);
1085
1086 match &canvas.commands()[0] {
1088 DrawCommand::Rect { bounds, .. } => {
1089 assert_eq!(bounds.x, 100.0);
1090 assert_eq!(bounds.y, 50.0);
1091 }
1092 _ => panic!("Expected Rect command for track"),
1093 }
1094 }
1095
1096 #[test]
1097 fn test_toggle_paint_thumb_centered_vertically() {
1098 let mut toggle = Toggle::new().track_height(30.0).thumb_size(20.0);
1099 toggle.layout(Rect::new(0.0, 0.0, 44.0, 30.0));
1100
1101 let mut canvas = RecordingCanvas::new();
1102 toggle.paint(&mut canvas);
1103
1104 let expected_y = (30.0 - 20.0) / 2.0; match &canvas.commands()[1] {
1107 DrawCommand::Rect { bounds, .. } => {
1108 assert_eq!(bounds.y, expected_y);
1109 }
1110 _ => panic!("Expected Rect command for thumb"),
1111 }
1112 }
1113
1114 #[test]
1115 fn test_toggle_paint_custom_track_and_thumb() {
1116 let mut toggle = Toggle::new()
1117 .track_width(60.0)
1118 .track_height(32.0)
1119 .thumb_size(28.0)
1120 .track_on_color(Color::GREEN)
1121 .thumb_color(Color::WHITE)
1122 .on(true);
1123 toggle.layout(Rect::new(10.0, 20.0, 60.0, 32.0));
1124
1125 let mut canvas = RecordingCanvas::new();
1126 toggle.paint(&mut canvas);
1127
1128 match &canvas.commands()[0] {
1130 DrawCommand::Rect { bounds, style, .. } => {
1131 assert_eq!(bounds.width, 60.0);
1132 assert_eq!(bounds.height, 32.0);
1133 assert_eq!(style.fill, Some(Color::GREEN));
1134 }
1135 _ => panic!("Expected Rect command for track"),
1136 }
1137
1138 let padding = (32.0 - 28.0) / 2.0;
1140 let expected_thumb_x = 10.0 + 60.0 - 28.0 - padding;
1141 match &canvas.commands()[1] {
1142 DrawCommand::Rect { bounds, style, .. } => {
1143 assert_eq!(bounds.width, 28.0);
1144 assert_eq!(bounds.height, 28.0);
1145 assert_eq!(bounds.x, expected_thumb_x);
1146 assert_eq!(style.fill, Some(Color::WHITE));
1147 }
1148 _ => panic!("Expected Rect command for thumb"),
1149 }
1150 }
1151
1152 #[test]
1157 fn test_toggle_event_click_turns_on() {
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 assert!(!toggle.is_on());
1162 let result = toggle.event(&Event::MouseDown {
1163 position: Point::new(22.0, 12.0),
1164 button: MouseButton::Left,
1165 });
1166 assert!(toggle.is_on());
1167 assert!(result.is_some());
1168 }
1169
1170 #[test]
1171 fn test_toggle_event_click_turns_off() {
1172 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(true);
1173 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1174
1175 assert!(toggle.is_on());
1176 let result = toggle.event(&Event::MouseDown {
1177 position: Point::new(22.0, 12.0),
1178 button: MouseButton::Left,
1179 });
1180 assert!(!toggle.is_on());
1181 assert!(result.is_some());
1182 }
1183
1184 #[test]
1185 fn test_toggle_event_emits_toggle_changed() {
1186 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1187 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1188
1189 let result = toggle.event(&Event::MouseDown {
1190 position: Point::new(22.0, 12.0),
1191 button: MouseButton::Left,
1192 });
1193
1194 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1195 assert!(msg.on);
1196 }
1197
1198 #[test]
1199 fn test_toggle_event_message_reflects_new_state() {
1200 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(true);
1201 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1202
1203 let result = toggle.event(&Event::MouseDown {
1204 position: Point::new(22.0, 12.0),
1205 button: MouseButton::Left,
1206 });
1207
1208 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1209 assert!(!msg.on);
1210 }
1211
1212 #[test]
1213 fn test_toggle_event_click_outside_track_no_toggle() {
1214 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1215 toggle.layout(Rect::new(0.0, 0.0, 100.0, 24.0));
1216
1217 let result = toggle.event(&Event::MouseDown {
1219 position: Point::new(80.0, 12.0),
1220 button: MouseButton::Left,
1221 });
1222 assert!(!toggle.is_on());
1223 assert!(result.is_none());
1224 }
1225
1226 #[test]
1227 fn test_toggle_event_click_below_track_no_toggle() {
1228 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1229 toggle.layout(Rect::new(0.0, 0.0, 44.0, 50.0));
1230
1231 let result = toggle.event(&Event::MouseDown {
1233 position: Point::new(22.0, 40.0),
1234 button: MouseButton::Left,
1235 });
1236 assert!(!toggle.is_on());
1237 assert!(result.is_none());
1238 }
1239
1240 #[test]
1241 fn test_toggle_event_right_click_no_toggle() {
1242 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1243 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1244
1245 let result = toggle.event(&Event::MouseDown {
1246 position: Point::new(22.0, 12.0),
1247 button: MouseButton::Right,
1248 });
1249 assert!(!toggle.is_on());
1250 assert!(result.is_none());
1251 }
1252
1253 #[test]
1254 fn test_toggle_event_disabled_blocks_click() {
1255 let mut toggle = Toggle::new()
1256 .track_width(44.0)
1257 .track_height(24.0)
1258 .on(false)
1259 .disabled(true);
1260 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1261
1262 let result = toggle.event(&Event::MouseDown {
1263 position: Point::new(22.0, 12.0),
1264 button: MouseButton::Left,
1265 });
1266 assert!(!toggle.is_on());
1267 assert!(result.is_none());
1268 }
1269
1270 #[test]
1271 fn test_toggle_event_hit_test_track_edges() {
1272 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1273 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1274
1275 let result = toggle.event(&Event::MouseDown {
1277 position: Point::new(0.0, 0.0),
1278 button: MouseButton::Left,
1279 });
1280 assert!(toggle.is_on());
1281 assert!(result.is_some());
1282
1283 toggle.on = false;
1284
1285 let result = toggle.event(&Event::MouseDown {
1287 position: Point::new(44.0, 24.0),
1288 button: MouseButton::Left,
1289 });
1290 assert!(toggle.is_on());
1291 assert!(result.is_some());
1292 }
1293
1294 #[test]
1295 fn test_toggle_event_with_offset_bounds() {
1296 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1297 toggle.layout(Rect::new(100.0, 50.0, 44.0, 24.0));
1298
1299 let result = toggle.event(&Event::MouseDown {
1301 position: Point::new(122.0, 62.0),
1302 button: MouseButton::Left,
1303 });
1304 assert!(toggle.is_on());
1305 assert!(result.is_some());
1306 }
1307
1308 #[test]
1309 fn test_toggle_event_full_interaction_flow() {
1310 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1311 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1312
1313 assert!(!toggle.is_on());
1315
1316 let result = toggle.event(&Event::MouseDown {
1318 position: Point::new(22.0, 12.0),
1319 button: MouseButton::Left,
1320 });
1321 assert!(toggle.is_on());
1322 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1323 assert!(msg.on);
1324
1325 let result = toggle.event(&Event::MouseDown {
1327 position: Point::new(22.0, 12.0),
1328 button: MouseButton::Left,
1329 });
1330 assert!(!toggle.is_on());
1331 let msg = result.unwrap().downcast::<ToggleChanged>().unwrap();
1332 assert!(!msg.on);
1333
1334 let result = toggle.event(&Event::MouseDown {
1336 position: Point::new(22.0, 12.0),
1337 button: MouseButton::Left,
1338 });
1339 assert!(toggle.is_on());
1340 assert!(result.is_some());
1341 }
1342
1343 #[test]
1344 fn test_toggle_event_mouse_move_no_effect() {
1345 let mut toggle = Toggle::new().track_width(44.0).track_height(24.0).on(false);
1346 toggle.layout(Rect::new(0.0, 0.0, 44.0, 24.0));
1347
1348 let result = toggle.event(&Event::MouseMove {
1350 position: Point::new(22.0, 12.0),
1351 });
1352 assert!(!toggle.is_on());
1353 assert!(result.is_none());
1354 }
1355}