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, Default, Serialize, Deserialize)]
12pub enum CheckState {
13 #[default]
15 Unchecked,
16 Checked,
18 Indeterminate,
20}
21
22impl CheckState {
23 #[must_use]
25 pub const fn toggle(&self) -> Self {
26 match self {
27 Self::Unchecked => Self::Checked,
28 Self::Checked | Self::Indeterminate => Self::Unchecked,
29 }
30 }
31
32 #[must_use]
34 pub const fn is_checked(&self) -> bool {
35 matches!(self, Self::Checked)
36 }
37
38 #[must_use]
40 pub const fn is_indeterminate(&self) -> bool {
41 matches!(self, Self::Indeterminate)
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct CheckboxChanged {
48 pub state: CheckState,
50}
51
52#[derive(Serialize, Deserialize)]
54pub struct Checkbox {
55 state: CheckState,
57 disabled: bool,
59 label: String,
61 box_size: f32,
63 spacing: f32,
65 box_color: Color,
67 checked_color: Color,
69 check_color: Color,
71 label_color: Color,
73 disabled_color: Color,
75 test_id_value: Option<String>,
77 accessible_name_value: Option<String>,
79 #[serde(skip)]
81 bounds: Rect,
82 #[serde(skip)]
84 hovered: bool,
85}
86
87impl Default for Checkbox {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl Checkbox {
94 #[must_use]
96 pub fn new() -> Self {
97 Self {
98 state: CheckState::Unchecked,
99 disabled: false,
100 label: String::new(),
101 box_size: 18.0,
102 spacing: 8.0,
103 box_color: Color::new(0.8, 0.8, 0.8, 1.0),
104 checked_color: Color::new(0.2, 0.47, 0.96, 1.0),
105 check_color: Color::WHITE,
106 label_color: Color::BLACK,
107 disabled_color: Color::new(0.6, 0.6, 0.6, 1.0),
108 test_id_value: None,
109 accessible_name_value: None,
110 bounds: Rect::default(),
111 hovered: false,
112 }
113 }
114
115 #[must_use]
117 pub const fn checked(mut self, checked: bool) -> Self {
118 self.state = if checked {
119 CheckState::Checked
120 } else {
121 CheckState::Unchecked
122 };
123 self
124 }
125
126 #[must_use]
128 pub const fn state(mut self, state: CheckState) -> Self {
129 self.state = state;
130 self
131 }
132
133 #[must_use]
135 pub fn label(mut self, label: impl Into<String>) -> Self {
136 self.label = label.into();
137 self
138 }
139
140 #[must_use]
142 pub const fn disabled(mut self, disabled: bool) -> Self {
143 self.disabled = disabled;
144 self
145 }
146
147 #[must_use]
149 pub fn box_size(mut self, size: f32) -> Self {
150 self.box_size = size.max(8.0);
151 self
152 }
153
154 #[must_use]
156 pub fn spacing(mut self, spacing: f32) -> Self {
157 self.spacing = spacing.max(0.0);
158 self
159 }
160
161 #[must_use]
163 pub const fn checked_color(mut self, color: Color) -> Self {
164 self.checked_color = color;
165 self
166 }
167
168 #[must_use]
170 pub const fn check_color(mut self, color: Color) -> Self {
171 self.check_color = color;
172 self
173 }
174
175 #[must_use]
177 pub const fn label_color(mut self, color: Color) -> Self {
178 self.label_color = color;
179 self
180 }
181
182 #[must_use]
184 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
185 self.test_id_value = Some(id.into());
186 self
187 }
188
189 #[must_use]
191 pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
192 self.accessible_name_value = Some(name.into());
193 self
194 }
195
196 #[must_use]
198 pub const fn get_state(&self) -> CheckState {
199 self.state
200 }
201
202 #[must_use]
204 pub const fn is_checked(&self) -> bool {
205 self.state.is_checked()
206 }
207
208 #[must_use]
210 pub const fn is_indeterminate(&self) -> bool {
211 self.state.is_indeterminate()
212 }
213
214 #[must_use]
216 pub fn get_label(&self) -> &str {
217 &self.label
218 }
219}
220
221impl Widget for Checkbox {
222 fn type_id(&self) -> TypeId {
223 TypeId::of::<Self>()
224 }
225
226 fn measure(&self, constraints: Constraints) -> Size {
227 let label_width = if self.label.is_empty() {
229 0.0
230 } else {
231 self.label.len() as f32 * 8.0 };
233
234 let total_width = self.box_size + self.spacing + label_width;
235 let height = self.box_size;
236
237 constraints.constrain(Size::new(total_width, height))
238 }
239
240 fn layout(&mut self, bounds: Rect) -> LayoutResult {
241 self.bounds = bounds;
242 LayoutResult {
243 size: bounds.size(),
244 }
245 }
246
247 fn paint(&self, canvas: &mut dyn Canvas) {
248 let box_rect = Rect::new(
249 self.bounds.x,
250 self.bounds.y + (self.bounds.height - self.box_size) / 2.0,
251 self.box_size,
252 self.box_size,
253 );
254
255 let box_color = if self.disabled {
257 self.disabled_color
258 } else if self.state.is_checked() || self.state.is_indeterminate() {
259 self.checked_color
260 } else {
261 self.box_color
262 };
263
264 canvas.fill_rect(box_rect, box_color);
265
266 if !self.disabled {
268 match self.state {
269 CheckState::Checked => {
270 let inner = Rect::new(
272 self.box_size.mul_add(0.25, box_rect.x),
273 self.box_size.mul_add(0.25, box_rect.y),
274 self.box_size * 0.5,
275 self.box_size * 0.5,
276 );
277 canvas.fill_rect(inner, self.check_color);
278 }
279 CheckState::Indeterminate => {
280 let line = Rect::new(
282 self.box_size.mul_add(0.2, box_rect.x),
283 self.box_size.mul_add(0.4, box_rect.y),
284 self.box_size * 0.6,
285 self.box_size * 0.2,
286 );
287 canvas.fill_rect(line, self.check_color);
288 }
289 CheckState::Unchecked => {}
290 }
291 }
292
293 if !self.label.is_empty() {
295 let label_x = self.bounds.x + self.box_size + self.spacing;
296 let label_y = self.bounds.y + (self.bounds.height - 16.0) / 2.0;
297 let label_color = if self.disabled {
298 self.disabled_color
299 } else {
300 self.label_color
301 };
302
303 let style = presentar_core::widget::TextStyle {
304 color: label_color,
305 ..Default::default()
306 };
307 canvas.draw_text(
308 &self.label,
309 presentar_core::Point::new(label_x, label_y),
310 &style,
311 );
312 }
313 }
314
315 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
316 if self.disabled {
317 return None;
318 }
319
320 match event {
321 Event::MouseMove { position } => {
322 self.hovered = self.bounds.contains_point(position);
323 }
324 Event::MouseDown {
325 position,
326 button: MouseButton::Left,
327 } => {
328 if self.bounds.contains_point(position) {
329 self.state = self.state.toggle();
330 return Some(Box::new(CheckboxChanged { state: self.state }));
331 }
332 }
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
357 .as_deref()
358 .or(if self.label.is_empty() {
359 None
360 } else {
361 Some(self.label.as_str())
362 })
363 }
364
365 fn accessible_role(&self) -> AccessibleRole {
366 AccessibleRole::Checkbox
367 }
368
369 fn test_id(&self) -> Option<&str> {
370 self.test_id_value.as_deref()
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use presentar_core::Widget;
378
379 #[test]
384 fn test_check_state_default() {
385 assert_eq!(CheckState::default(), CheckState::Unchecked);
386 }
387
388 #[test]
389 fn test_check_state_toggle() {
390 assert_eq!(CheckState::Unchecked.toggle(), CheckState::Checked);
391 assert_eq!(CheckState::Checked.toggle(), CheckState::Unchecked);
392 assert_eq!(CheckState::Indeterminate.toggle(), CheckState::Unchecked);
393 }
394
395 #[test]
396 fn test_check_state_is_checked() {
397 assert!(!CheckState::Unchecked.is_checked());
398 assert!(CheckState::Checked.is_checked());
399 assert!(!CheckState::Indeterminate.is_checked());
400 }
401
402 #[test]
403 fn test_check_state_is_indeterminate() {
404 assert!(!CheckState::Unchecked.is_indeterminate());
405 assert!(!CheckState::Checked.is_indeterminate());
406 assert!(CheckState::Indeterminate.is_indeterminate());
407 }
408
409 #[test]
414 fn test_checkbox_changed_message() {
415 let msg = CheckboxChanged {
416 state: CheckState::Checked,
417 };
418 assert_eq!(msg.state, CheckState::Checked);
419 }
420
421 #[test]
426 fn test_checkbox_new() {
427 let cb = Checkbox::new();
428 assert_eq!(cb.get_state(), CheckState::Unchecked);
429 assert!(!cb.is_checked());
430 assert!(!cb.disabled);
431 assert!(cb.get_label().is_empty());
432 }
433
434 #[test]
435 fn test_checkbox_default() {
436 let cb = Checkbox::default();
437 assert_eq!(cb.get_state(), CheckState::Unchecked);
438 }
439
440 #[test]
441 fn test_checkbox_builder() {
442 let cb = Checkbox::new()
443 .checked(true)
444 .label("Accept terms")
445 .disabled(false)
446 .box_size(20.0)
447 .spacing(10.0)
448 .with_test_id("terms-checkbox")
449 .with_accessible_name("Terms and Conditions");
450
451 assert!(cb.is_checked());
452 assert_eq!(cb.get_label(), "Accept terms");
453 assert!(!cb.disabled);
454 assert_eq!(Widget::test_id(&cb), Some("terms-checkbox"));
455 assert_eq!(cb.accessible_name(), Some("Terms and Conditions"));
456 }
457
458 #[test]
459 fn test_checkbox_state_builder() {
460 let cb = Checkbox::new().state(CheckState::Indeterminate);
461 assert!(cb.is_indeterminate());
462 assert!(!cb.is_checked());
463 }
464
465 #[test]
470 fn test_checkbox_checked_true() {
471 let cb = Checkbox::new().checked(true);
472 assert!(cb.is_checked());
473 assert_eq!(cb.get_state(), CheckState::Checked);
474 }
475
476 #[test]
477 fn test_checkbox_checked_false() {
478 let cb = Checkbox::new().checked(false);
479 assert!(!cb.is_checked());
480 assert_eq!(cb.get_state(), CheckState::Unchecked);
481 }
482
483 #[test]
484 fn test_checkbox_indeterminate() {
485 let cb = Checkbox::new().state(CheckState::Indeterminate);
486 assert!(cb.is_indeterminate());
487 assert!(!cb.is_checked());
488 }
489
490 #[test]
495 fn test_checkbox_type_id() {
496 let cb = Checkbox::new();
497 assert_eq!(Widget::type_id(&cb), TypeId::of::<Checkbox>());
498 }
499
500 #[test]
501 fn test_checkbox_measure_no_label() {
502 let cb = Checkbox::new().box_size(18.0);
503 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
504 assert_eq!(size.width, 18.0 + 8.0); assert_eq!(size.height, 18.0);
506 }
507
508 #[test]
509 fn test_checkbox_measure_with_label() {
510 let cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Test");
511 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
512 assert!(size.width > 18.0);
514 }
515
516 #[test]
517 fn test_checkbox_is_interactive() {
518 let cb = Checkbox::new();
519 assert!(cb.is_interactive());
520
521 let cb = Checkbox::new().disabled(true);
522 assert!(!cb.is_interactive());
523 }
524
525 #[test]
526 fn test_checkbox_is_focusable() {
527 let cb = Checkbox::new();
528 assert!(cb.is_focusable());
529
530 let cb = Checkbox::new().disabled(true);
531 assert!(!cb.is_focusable());
532 }
533
534 #[test]
535 fn test_checkbox_accessible_role() {
536 let cb = Checkbox::new();
537 assert_eq!(cb.accessible_role(), AccessibleRole::Checkbox);
538 }
539
540 #[test]
541 fn test_checkbox_accessible_name_from_label() {
542 let cb = Checkbox::new().label("My checkbox");
543 assert_eq!(cb.accessible_name(), Some("My checkbox"));
544 }
545
546 #[test]
547 fn test_checkbox_accessible_name_override() {
548 let cb = Checkbox::new()
549 .label("Short")
550 .with_accessible_name("Full accessible name");
551 assert_eq!(cb.accessible_name(), Some("Full accessible name"));
552 }
553
554 #[test]
555 fn test_checkbox_children() {
556 let cb = Checkbox::new();
557 assert!(cb.children().is_empty());
558 }
559
560 #[test]
565 fn test_checkbox_colors() {
566 let cb = Checkbox::new()
567 .checked_color(Color::RED)
568 .check_color(Color::GREEN)
569 .label_color(Color::BLUE);
570
571 assert_eq!(cb.checked_color, Color::RED);
572 assert_eq!(cb.check_color, Color::GREEN);
573 assert_eq!(cb.label_color, Color::BLUE);
574 }
575
576 #[test]
581 fn test_checkbox_layout() {
582 let mut cb = Checkbox::new();
583 let bounds = Rect::new(10.0, 20.0, 100.0, 30.0);
584 let result = cb.layout(bounds);
585 assert_eq!(result.size, bounds.size());
586 assert_eq!(cb.bounds, bounds);
587 }
588
589 #[test]
594 fn test_checkbox_box_size_min() {
595 let cb = Checkbox::new().box_size(2.0);
596 assert_eq!(cb.box_size, 8.0); }
598
599 #[test]
600 fn test_checkbox_spacing_min() {
601 let cb = Checkbox::new().spacing(-5.0);
602 assert_eq!(cb.spacing, 0.0); }
604
605 use presentar_core::draw::DrawCommand;
610 use presentar_core::RecordingCanvas;
611
612 #[test]
613 fn test_checkbox_paint_unchecked_draws_box() {
614 let mut cb = Checkbox::new().box_size(18.0);
615 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
616
617 let mut canvas = RecordingCanvas::new();
618 cb.paint(&mut canvas);
619
620 assert!(canvas.command_count() >= 1);
622 match &canvas.commands()[0] {
623 DrawCommand::Rect { bounds, style, .. } => {
624 assert_eq!(bounds.width, 18.0);
625 assert_eq!(bounds.height, 18.0);
626 assert!(style.fill.is_some());
627 }
628 _ => panic!("Expected Rect command for checkbox box"),
629 }
630 }
631
632 #[test]
633 fn test_checkbox_paint_unchecked_no_checkmark() {
634 let mut cb = Checkbox::new().box_size(18.0);
635 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
636
637 let mut canvas = RecordingCanvas::new();
638 cb.paint(&mut canvas);
639
640 assert_eq!(canvas.command_count(), 1);
642 }
643
644 #[test]
645 fn test_checkbox_paint_checked_draws_checkmark() {
646 let mut cb = Checkbox::new().box_size(18.0).checked(true);
647 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
648
649 let mut canvas = RecordingCanvas::new();
650 cb.paint(&mut canvas);
651
652 assert_eq!(canvas.command_count(), 2);
654
655 match &canvas.commands()[1] {
657 DrawCommand::Rect { bounds, .. } => {
658 assert!((bounds.width - 9.0).abs() < 0.1);
660 assert!((bounds.height - 9.0).abs() < 0.1);
661 }
662 _ => panic!("Expected Rect command for checkmark"),
663 }
664 }
665
666 #[test]
667 fn test_checkbox_paint_indeterminate_draws_line() {
668 let mut cb = Checkbox::new()
669 .box_size(18.0)
670 .state(CheckState::Indeterminate);
671 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
672
673 let mut canvas = RecordingCanvas::new();
674 cb.paint(&mut canvas);
675
676 assert_eq!(canvas.command_count(), 2);
678
679 match &canvas.commands()[1] {
681 DrawCommand::Rect { bounds, .. } => {
682 assert!((bounds.width - 10.8).abs() < 0.1);
684 assert!((bounds.height - 3.6).abs() < 0.1);
685 }
686 _ => panic!("Expected Rect command for indeterminate line"),
687 }
688 }
689
690 #[test]
691 fn test_checkbox_paint_with_label() {
692 let mut cb = Checkbox::new().box_size(18.0).label("Test label");
693 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
694
695 let mut canvas = RecordingCanvas::new();
696 cb.paint(&mut canvas);
697
698 assert_eq!(canvas.command_count(), 2);
700
701 match &canvas.commands()[1] {
703 DrawCommand::Text { content, .. } => {
704 assert_eq!(content, "Test label");
705 }
706 _ => panic!("Expected Text command for label"),
707 }
708 }
709
710 #[test]
711 fn test_checkbox_paint_checked_with_label() {
712 let mut cb = Checkbox::new().box_size(18.0).checked(true).label("Accept");
713 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
714
715 let mut canvas = RecordingCanvas::new();
716 cb.paint(&mut canvas);
717
718 assert_eq!(canvas.command_count(), 3);
720
721 match &canvas.commands()[2] {
723 DrawCommand::Text { content, .. } => {
724 assert_eq!(content, "Accept");
725 }
726 _ => panic!("Expected Text command for label"),
727 }
728 }
729
730 #[test]
731 fn test_checkbox_paint_uses_checked_color() {
732 let mut cb = Checkbox::new()
733 .box_size(18.0)
734 .checked(true)
735 .checked_color(Color::RED);
736 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
737
738 let mut canvas = RecordingCanvas::new();
739 cb.paint(&mut canvas);
740
741 match &canvas.commands()[0] {
743 DrawCommand::Rect { style, .. } => {
744 assert_eq!(style.fill, Some(Color::RED));
745 }
746 _ => panic!("Expected Rect command"),
747 }
748 }
749
750 #[test]
751 fn test_checkbox_paint_uses_check_color() {
752 let mut cb = Checkbox::new()
753 .box_size(18.0)
754 .checked(true)
755 .check_color(Color::GREEN);
756 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
757
758 let mut canvas = RecordingCanvas::new();
759 cb.paint(&mut canvas);
760
761 match &canvas.commands()[1] {
763 DrawCommand::Rect { style, .. } => {
764 assert_eq!(style.fill, Some(Color::GREEN));
765 }
766 _ => panic!("Expected Rect command for checkmark"),
767 }
768 }
769
770 #[test]
771 fn test_checkbox_paint_disabled_no_checkmark() {
772 let mut cb = Checkbox::new().box_size(18.0).checked(true).disabled(true);
773 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
774
775 let mut canvas = RecordingCanvas::new();
776 cb.paint(&mut canvas);
777
778 assert_eq!(canvas.command_count(), 1);
780 }
781
782 #[test]
783 fn test_checkbox_paint_disabled_uses_disabled_color() {
784 let mut cb = Checkbox::new()
785 .box_size(18.0)
786 .disabled(true)
787 .label("Disabled");
788 let disabled_color = cb.disabled_color;
789 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
790
791 let mut canvas = RecordingCanvas::new();
792 cb.paint(&mut canvas);
793
794 match &canvas.commands()[0] {
796 DrawCommand::Rect { style, .. } => {
797 assert_eq!(style.fill, Some(disabled_color));
798 }
799 _ => panic!("Expected Rect command"),
800 }
801 }
802
803 #[test]
804 fn test_checkbox_paint_label_position() {
805 let mut cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Label");
806 cb.layout(Rect::new(10.0, 20.0, 200.0, 18.0));
807
808 let mut canvas = RecordingCanvas::new();
809 cb.paint(&mut canvas);
810
811 match &canvas.commands()[1] {
813 DrawCommand::Text { position, .. } => {
814 assert_eq!(position.x, 36.0);
816 }
817 _ => panic!("Expected Text command"),
818 }
819 }
820
821 #[test]
822 fn test_checkbox_paint_box_position_from_layout() {
823 let mut cb = Checkbox::new().box_size(18.0);
824 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
825
826 let mut canvas = RecordingCanvas::new();
827 cb.paint(&mut canvas);
828
829 match &canvas.commands()[0] {
830 DrawCommand::Rect { bounds, .. } => {
831 assert_eq!(bounds.x, 50.0);
832 }
833 _ => panic!("Expected Rect command"),
834 }
835 }
836
837 #[test]
838 fn test_checkbox_paint_custom_box_size() {
839 let mut cb = Checkbox::new().box_size(24.0).checked(true);
840 cb.layout(Rect::new(0.0, 0.0, 100.0, 24.0));
841
842 let mut canvas = RecordingCanvas::new();
843 cb.paint(&mut canvas);
844
845 match &canvas.commands()[0] {
847 DrawCommand::Rect { bounds, .. } => {
848 assert_eq!(bounds.width, 24.0);
849 assert_eq!(bounds.height, 24.0);
850 }
851 _ => panic!("Expected Rect command"),
852 }
853
854 match &canvas.commands()[1] {
856 DrawCommand::Rect { bounds, .. } => {
857 assert_eq!(bounds.width, 12.0);
858 assert_eq!(bounds.height, 12.0);
859 }
860 _ => panic!("Expected Rect command for checkmark"),
861 }
862 }
863
864 use presentar_core::{MouseButton, Point};
869
870 #[test]
871 fn test_checkbox_event_click_toggles_unchecked_to_checked() {
872 let mut cb = Checkbox::new().box_size(18.0);
873 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
874
875 assert!(!cb.is_checked());
876 let result = cb.event(&Event::MouseDown {
877 position: Point::new(9.0, 9.0),
878 button: MouseButton::Left,
879 });
880 assert!(cb.is_checked());
881 assert!(result.is_some());
882 }
883
884 #[test]
885 fn test_checkbox_event_click_toggles_checked_to_unchecked() {
886 let mut cb = Checkbox::new().box_size(18.0).checked(true);
887 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
888
889 assert!(cb.is_checked());
890 let result = cb.event(&Event::MouseDown {
891 position: Point::new(9.0, 9.0),
892 button: MouseButton::Left,
893 });
894 assert!(!cb.is_checked());
895 assert!(result.is_some());
896 }
897
898 #[test]
899 fn test_checkbox_event_click_indeterminate_to_unchecked() {
900 let mut cb = Checkbox::new()
901 .box_size(18.0)
902 .state(CheckState::Indeterminate);
903 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
904
905 assert!(cb.is_indeterminate());
906 let result = cb.event(&Event::MouseDown {
907 position: Point::new(9.0, 9.0),
908 button: MouseButton::Left,
909 });
910 assert!(!cb.is_checked());
912 assert!(!cb.is_indeterminate());
913 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
914 assert_eq!(msg.state, CheckState::Unchecked);
915 }
916
917 #[test]
918 fn test_checkbox_event_emits_checkbox_changed() {
919 let mut cb = Checkbox::new().box_size(18.0);
920 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
921
922 let result = cb.event(&Event::MouseDown {
923 position: Point::new(9.0, 9.0),
924 button: MouseButton::Left,
925 });
926
927 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
928 assert_eq!(msg.state, CheckState::Checked);
929 }
930
931 #[test]
932 fn test_checkbox_event_message_reflects_new_state() {
933 let mut cb = Checkbox::new().box_size(18.0).checked(true);
934 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
935
936 let result = cb.event(&Event::MouseDown {
937 position: Point::new(9.0, 9.0),
938 button: MouseButton::Left,
939 });
940
941 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
942 assert_eq!(msg.state, CheckState::Unchecked);
943 }
944
945 #[test]
946 fn test_checkbox_event_click_outside_bounds_no_toggle() {
947 let mut cb = Checkbox::new().box_size(18.0);
948 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
949
950 let result = cb.event(&Event::MouseDown {
951 position: Point::new(200.0, 9.0),
952 button: MouseButton::Left,
953 });
954 assert!(!cb.is_checked());
955 assert!(result.is_none());
956 }
957
958 #[test]
959 fn test_checkbox_event_right_click_no_toggle() {
960 let mut cb = Checkbox::new().box_size(18.0);
961 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
962
963 let result = cb.event(&Event::MouseDown {
964 position: Point::new(9.0, 9.0),
965 button: MouseButton::Right,
966 });
967 assert!(!cb.is_checked());
968 assert!(result.is_none());
969 }
970
971 #[test]
972 fn test_checkbox_event_mouse_move_sets_hover() {
973 let mut cb = Checkbox::new().box_size(18.0);
974 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
975
976 assert!(!cb.hovered);
977 cb.event(&Event::MouseMove {
978 position: Point::new(50.0, 9.0),
979 });
980 assert!(cb.hovered);
981 }
982
983 #[test]
984 fn test_checkbox_event_mouse_move_clears_hover() {
985 let mut cb = Checkbox::new().box_size(18.0);
986 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
987 cb.hovered = true;
988
989 cb.event(&Event::MouseMove {
990 position: Point::new(200.0, 200.0),
991 });
992 assert!(!cb.hovered);
993 }
994
995 #[test]
996 fn test_checkbox_event_disabled_blocks_click() {
997 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
998 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
999
1000 let result = cb.event(&Event::MouseDown {
1001 position: Point::new(9.0, 9.0),
1002 button: MouseButton::Left,
1003 });
1004 assert!(!cb.is_checked());
1005 assert!(result.is_none());
1006 }
1007
1008 #[test]
1009 fn test_checkbox_event_disabled_blocks_hover() {
1010 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
1011 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1012
1013 cb.event(&Event::MouseMove {
1014 position: Point::new(50.0, 9.0),
1015 });
1016 assert!(!cb.hovered);
1017 }
1018
1019 #[test]
1020 fn test_checkbox_event_click_on_label_area_toggles() {
1021 let mut cb = Checkbox::new().box_size(18.0).label("Accept terms");
1022 cb.layout(Rect::new(0.0, 0.0, 150.0, 18.0));
1023
1024 let result = cb.event(&Event::MouseDown {
1026 position: Point::new(100.0, 9.0),
1027 button: MouseButton::Left,
1028 });
1029 assert!(cb.is_checked());
1030 assert!(result.is_some());
1031 }
1032
1033 #[test]
1034 fn test_checkbox_event_full_interaction_flow() {
1035 let mut cb = Checkbox::new().box_size(18.0);
1036 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1037
1038 assert!(!cb.is_checked());
1040 assert!(!cb.hovered);
1041
1042 cb.event(&Event::MouseMove {
1044 position: Point::new(50.0, 9.0),
1045 });
1046 assert!(cb.hovered);
1047
1048 let result = cb.event(&Event::MouseDown {
1050 position: Point::new(9.0, 9.0),
1051 button: MouseButton::Left,
1052 });
1053 assert!(cb.is_checked());
1054 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1055 assert_eq!(msg.state, CheckState::Checked);
1056
1057 let result = cb.event(&Event::MouseDown {
1059 position: Point::new(9.0, 9.0),
1060 button: MouseButton::Left,
1061 });
1062 assert!(!cb.is_checked());
1063 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1064 assert_eq!(msg.state, CheckState::Unchecked);
1065
1066 cb.event(&Event::MouseMove {
1068 position: Point::new(200.0, 200.0),
1069 });
1070 assert!(!cb.hovered);
1071 }
1072
1073 #[test]
1074 fn test_checkbox_event_with_offset_bounds() {
1075 let mut cb = Checkbox::new().box_size(18.0);
1076 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
1077
1078 let result = cb.event(&Event::MouseDown {
1080 position: Point::new(100.0, 109.0),
1081 button: MouseButton::Left,
1082 });
1083 assert!(cb.is_checked());
1084 assert!(result.is_some());
1085 }
1086}