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, Default, Serialize, Deserialize)]
14pub enum CheckState {
15 #[default]
17 Unchecked,
18 Checked,
20 Indeterminate,
22}
23
24impl CheckState {
25 #[must_use]
27 pub const fn toggle(&self) -> Self {
28 match self {
29 Self::Unchecked => Self::Checked,
30 Self::Checked | Self::Indeterminate => Self::Unchecked,
31 }
32 }
33
34 #[must_use]
36 pub const fn is_checked(&self) -> bool {
37 matches!(self, Self::Checked)
38 }
39
40 #[must_use]
42 pub const fn is_indeterminate(&self) -> bool {
43 matches!(self, Self::Indeterminate)
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct CheckboxChanged {
50 pub state: CheckState,
52}
53
54#[derive(Serialize, Deserialize)]
56pub struct Checkbox {
57 state: CheckState,
59 disabled: bool,
61 label: String,
63 box_size: f32,
65 spacing: f32,
67 box_color: Color,
69 checked_color: Color,
71 check_color: Color,
73 label_color: Color,
75 disabled_color: Color,
77 test_id_value: Option<String>,
79 accessible_name_value: Option<String>,
81 #[serde(skip)]
83 bounds: Rect,
84 #[serde(skip)]
86 hovered: bool,
87}
88
89impl Default for Checkbox {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl Checkbox {
96 #[must_use]
98 pub fn new() -> Self {
99 Self {
100 state: CheckState::Unchecked,
101 disabled: false,
102 label: String::new(),
103 box_size: 18.0,
104 spacing: 8.0,
105 box_color: Color::new(0.8, 0.8, 0.8, 1.0),
106 checked_color: Color::new(0.2, 0.47, 0.96, 1.0),
107 check_color: Color::WHITE,
108 label_color: Color::BLACK,
109 disabled_color: Color::new(0.6, 0.6, 0.6, 1.0),
110 test_id_value: None,
111 accessible_name_value: None,
112 bounds: Rect::default(),
113 hovered: false,
114 }
115 }
116
117 #[must_use]
119 pub const fn checked(mut self, checked: bool) -> Self {
120 self.state = if checked {
121 CheckState::Checked
122 } else {
123 CheckState::Unchecked
124 };
125 self
126 }
127
128 #[must_use]
130 pub const fn state(mut self, state: CheckState) -> Self {
131 self.state = state;
132 self
133 }
134
135 #[must_use]
137 pub fn label(mut self, label: impl Into<String>) -> Self {
138 self.label = label.into();
139 self
140 }
141
142 #[must_use]
144 pub const fn disabled(mut self, disabled: bool) -> Self {
145 self.disabled = disabled;
146 self
147 }
148
149 #[must_use]
151 pub fn box_size(mut self, size: f32) -> Self {
152 self.box_size = size.max(8.0);
153 self
154 }
155
156 #[must_use]
158 pub fn spacing(mut self, spacing: f32) -> Self {
159 self.spacing = spacing.max(0.0);
160 self
161 }
162
163 #[must_use]
165 pub const fn checked_color(mut self, color: Color) -> Self {
166 self.checked_color = color;
167 self
168 }
169
170 #[must_use]
172 pub const fn check_color(mut self, color: Color) -> Self {
173 self.check_color = color;
174 self
175 }
176
177 #[must_use]
179 pub const fn label_color(mut self, color: Color) -> Self {
180 self.label_color = color;
181 self
182 }
183
184 #[must_use]
186 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
187 self.test_id_value = Some(id.into());
188 self
189 }
190
191 #[must_use]
193 pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
194 self.accessible_name_value = Some(name.into());
195 self
196 }
197
198 #[must_use]
200 pub const fn get_state(&self) -> CheckState {
201 self.state
202 }
203
204 #[must_use]
206 pub const fn is_checked(&self) -> bool {
207 self.state.is_checked()
208 }
209
210 #[must_use]
212 pub const fn is_indeterminate(&self) -> bool {
213 self.state.is_indeterminate()
214 }
215
216 #[must_use]
218 pub fn get_label(&self) -> &str {
219 &self.label
220 }
221}
222
223impl Widget for Checkbox {
224 fn type_id(&self) -> TypeId {
225 TypeId::of::<Self>()
226 }
227
228 fn measure(&self, constraints: Constraints) -> Size {
229 let label_width = if self.label.is_empty() {
231 0.0
232 } else {
233 self.label.len() as f32 * 8.0 };
235
236 let total_width = self.box_size + self.spacing + label_width;
237 let height = self.box_size;
238
239 constraints.constrain(Size::new(total_width, height))
240 }
241
242 fn layout(&mut self, bounds: Rect) -> LayoutResult {
243 self.bounds = bounds;
244 LayoutResult {
245 size: bounds.size(),
246 }
247 }
248
249 fn paint(&self, canvas: &mut dyn Canvas) {
250 let box_rect = Rect::new(
251 self.bounds.x,
252 self.bounds.y + (self.bounds.height - self.box_size) / 2.0,
253 self.box_size,
254 self.box_size,
255 );
256
257 let box_color = if self.disabled {
259 self.disabled_color
260 } else if self.state.is_checked() || self.state.is_indeterminate() {
261 self.checked_color
262 } else {
263 self.box_color
264 };
265
266 canvas.fill_rect(box_rect, box_color);
267
268 if !self.disabled {
270 match self.state {
271 CheckState::Checked => {
272 let inner = Rect::new(
274 self.box_size.mul_add(0.25, box_rect.x),
275 self.box_size.mul_add(0.25, box_rect.y),
276 self.box_size * 0.5,
277 self.box_size * 0.5,
278 );
279 canvas.fill_rect(inner, self.check_color);
280 }
281 CheckState::Indeterminate => {
282 let line = Rect::new(
284 self.box_size.mul_add(0.2, box_rect.x),
285 self.box_size.mul_add(0.4, box_rect.y),
286 self.box_size * 0.6,
287 self.box_size * 0.2,
288 );
289 canvas.fill_rect(line, self.check_color);
290 }
291 CheckState::Unchecked => {}
292 }
293 }
294
295 if !self.label.is_empty() {
297 let label_x = self.bounds.x + self.box_size + self.spacing;
298 let label_y = self.bounds.y + (self.bounds.height - 16.0) / 2.0;
299 let label_color = if self.disabled {
300 self.disabled_color
301 } else {
302 self.label_color
303 };
304
305 let style = presentar_core::widget::TextStyle {
306 color: label_color,
307 ..Default::default()
308 };
309 canvas.draw_text(
310 &self.label,
311 presentar_core::Point::new(label_x, label_y),
312 &style,
313 );
314 }
315 }
316
317 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
318 if self.disabled {
319 return None;
320 }
321
322 match event {
323 Event::MouseMove { position } => {
324 self.hovered = self.bounds.contains_point(position);
325 }
326 Event::MouseDown {
327 position,
328 button: MouseButton::Left,
329 } => {
330 if self.bounds.contains_point(position) {
331 self.state = self.state.toggle();
332 return Some(Box::new(CheckboxChanged { state: self.state }));
333 }
334 }
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
359 .as_deref()
360 .or(if self.label.is_empty() {
361 None
362 } else {
363 Some(self.label.as_str())
364 })
365 }
366
367 fn accessible_role(&self) -> AccessibleRole {
368 AccessibleRole::Checkbox
369 }
370
371 fn test_id(&self) -> Option<&str> {
372 self.test_id_value.as_deref()
373 }
374}
375
376impl Brick for Checkbox {
378 fn brick_name(&self) -> &'static str {
379 "Checkbox"
380 }
381
382 fn assertions(&self) -> &[BrickAssertion] {
383 &[BrickAssertion::MaxLatencyMs(16)]
384 }
385
386 fn budget(&self) -> BrickBudget {
387 BrickBudget::uniform(16)
388 }
389
390 fn verify(&self) -> BrickVerification {
391 BrickVerification {
392 passed: self.assertions().to_vec(),
393 failed: vec![],
394 verification_time: Duration::from_micros(10),
395 }
396 }
397
398 fn to_html(&self) -> String {
399 let test_id = self.test_id_value.as_deref().unwrap_or("checkbox");
400 let checked = if self.state.is_checked() {
401 " checked"
402 } else {
403 ""
404 };
405 let disabled = if self.disabled { " disabled" } else { "" };
406 format!(
407 r#"<input type="checkbox" class="brick-checkbox" data-testid="{}" aria-label="{}"{}{}/>"#,
408 test_id,
409 self.accessible_name_value.as_deref().unwrap_or(&self.label),
410 checked,
411 disabled
412 )
413 }
414
415 fn to_css(&self) -> String {
416 ".brick-checkbox { display: inline-block; }".into()
417 }
418
419 fn test_id(&self) -> Option<&str> {
420 self.test_id_value.as_deref()
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use presentar_core::Widget;
428
429 #[test]
434 fn test_check_state_default() {
435 assert_eq!(CheckState::default(), CheckState::Unchecked);
436 }
437
438 #[test]
439 fn test_check_state_toggle() {
440 assert_eq!(CheckState::Unchecked.toggle(), CheckState::Checked);
441 assert_eq!(CheckState::Checked.toggle(), CheckState::Unchecked);
442 assert_eq!(CheckState::Indeterminate.toggle(), CheckState::Unchecked);
443 }
444
445 #[test]
446 fn test_check_state_is_checked() {
447 assert!(!CheckState::Unchecked.is_checked());
448 assert!(CheckState::Checked.is_checked());
449 assert!(!CheckState::Indeterminate.is_checked());
450 }
451
452 #[test]
453 fn test_check_state_is_indeterminate() {
454 assert!(!CheckState::Unchecked.is_indeterminate());
455 assert!(!CheckState::Checked.is_indeterminate());
456 assert!(CheckState::Indeterminate.is_indeterminate());
457 }
458
459 #[test]
464 fn test_checkbox_changed_message() {
465 let msg = CheckboxChanged {
466 state: CheckState::Checked,
467 };
468 assert_eq!(msg.state, CheckState::Checked);
469 }
470
471 #[test]
476 fn test_checkbox_new() {
477 let cb = Checkbox::new();
478 assert_eq!(cb.get_state(), CheckState::Unchecked);
479 assert!(!cb.is_checked());
480 assert!(!cb.disabled);
481 assert!(cb.get_label().is_empty());
482 }
483
484 #[test]
485 fn test_checkbox_default() {
486 let cb = Checkbox::default();
487 assert_eq!(cb.get_state(), CheckState::Unchecked);
488 }
489
490 #[test]
491 fn test_checkbox_builder() {
492 let cb = Checkbox::new()
493 .checked(true)
494 .label("Accept terms")
495 .disabled(false)
496 .box_size(20.0)
497 .spacing(10.0)
498 .with_test_id("terms-checkbox")
499 .with_accessible_name("Terms and Conditions");
500
501 assert!(cb.is_checked());
502 assert_eq!(cb.get_label(), "Accept terms");
503 assert!(!cb.disabled);
504 assert_eq!(Widget::test_id(&cb), Some("terms-checkbox"));
505 assert_eq!(cb.accessible_name(), Some("Terms and Conditions"));
506 }
507
508 #[test]
509 fn test_checkbox_state_builder() {
510 let cb = Checkbox::new().state(CheckState::Indeterminate);
511 assert!(cb.is_indeterminate());
512 assert!(!cb.is_checked());
513 }
514
515 #[test]
520 fn test_checkbox_checked_true() {
521 let cb = Checkbox::new().checked(true);
522 assert!(cb.is_checked());
523 assert_eq!(cb.get_state(), CheckState::Checked);
524 }
525
526 #[test]
527 fn test_checkbox_checked_false() {
528 let cb = Checkbox::new().checked(false);
529 assert!(!cb.is_checked());
530 assert_eq!(cb.get_state(), CheckState::Unchecked);
531 }
532
533 #[test]
534 fn test_checkbox_indeterminate() {
535 let cb = Checkbox::new().state(CheckState::Indeterminate);
536 assert!(cb.is_indeterminate());
537 assert!(!cb.is_checked());
538 }
539
540 #[test]
545 fn test_checkbox_type_id() {
546 let cb = Checkbox::new();
547 assert_eq!(Widget::type_id(&cb), TypeId::of::<Checkbox>());
548 }
549
550 #[test]
551 fn test_checkbox_measure_no_label() {
552 let cb = Checkbox::new().box_size(18.0);
553 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
554 assert_eq!(size.width, 18.0 + 8.0); assert_eq!(size.height, 18.0);
556 }
557
558 #[test]
559 fn test_checkbox_measure_with_label() {
560 let cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Test");
561 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
562 assert!(size.width > 18.0);
564 }
565
566 #[test]
567 fn test_checkbox_is_interactive() {
568 let cb = Checkbox::new();
569 assert!(cb.is_interactive());
570
571 let cb = Checkbox::new().disabled(true);
572 assert!(!cb.is_interactive());
573 }
574
575 #[test]
576 fn test_checkbox_is_focusable() {
577 let cb = Checkbox::new();
578 assert!(cb.is_focusable());
579
580 let cb = Checkbox::new().disabled(true);
581 assert!(!cb.is_focusable());
582 }
583
584 #[test]
585 fn test_checkbox_accessible_role() {
586 let cb = Checkbox::new();
587 assert_eq!(cb.accessible_role(), AccessibleRole::Checkbox);
588 }
589
590 #[test]
591 fn test_checkbox_accessible_name_from_label() {
592 let cb = Checkbox::new().label("My checkbox");
593 assert_eq!(cb.accessible_name(), Some("My checkbox"));
594 }
595
596 #[test]
597 fn test_checkbox_accessible_name_override() {
598 let cb = Checkbox::new()
599 .label("Short")
600 .with_accessible_name("Full accessible name");
601 assert_eq!(cb.accessible_name(), Some("Full accessible name"));
602 }
603
604 #[test]
605 fn test_checkbox_children() {
606 let cb = Checkbox::new();
607 assert!(cb.children().is_empty());
608 }
609
610 #[test]
615 fn test_checkbox_colors() {
616 let cb = Checkbox::new()
617 .checked_color(Color::RED)
618 .check_color(Color::GREEN)
619 .label_color(Color::BLUE);
620
621 assert_eq!(cb.checked_color, Color::RED);
622 assert_eq!(cb.check_color, Color::GREEN);
623 assert_eq!(cb.label_color, Color::BLUE);
624 }
625
626 #[test]
631 fn test_checkbox_layout() {
632 let mut cb = Checkbox::new();
633 let bounds = Rect::new(10.0, 20.0, 100.0, 30.0);
634 let result = cb.layout(bounds);
635 assert_eq!(result.size, bounds.size());
636 assert_eq!(cb.bounds, bounds);
637 }
638
639 #[test]
644 fn test_checkbox_box_size_min() {
645 let cb = Checkbox::new().box_size(2.0);
646 assert_eq!(cb.box_size, 8.0); }
648
649 #[test]
650 fn test_checkbox_spacing_min() {
651 let cb = Checkbox::new().spacing(-5.0);
652 assert_eq!(cb.spacing, 0.0); }
654
655 use presentar_core::draw::DrawCommand;
660 use presentar_core::RecordingCanvas;
661
662 #[test]
663 fn test_checkbox_paint_unchecked_draws_box() {
664 let mut cb = Checkbox::new().box_size(18.0);
665 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
666
667 let mut canvas = RecordingCanvas::new();
668 cb.paint(&mut canvas);
669
670 assert!(canvas.command_count() >= 1);
672 match &canvas.commands()[0] {
673 DrawCommand::Rect { bounds, style, .. } => {
674 assert_eq!(bounds.width, 18.0);
675 assert_eq!(bounds.height, 18.0);
676 assert!(style.fill.is_some());
677 }
678 _ => panic!("Expected Rect command for checkbox box"),
679 }
680 }
681
682 #[test]
683 fn test_checkbox_paint_unchecked_no_checkmark() {
684 let mut cb = Checkbox::new().box_size(18.0);
685 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
686
687 let mut canvas = RecordingCanvas::new();
688 cb.paint(&mut canvas);
689
690 assert_eq!(canvas.command_count(), 1);
692 }
693
694 #[test]
695 fn test_checkbox_paint_checked_draws_checkmark() {
696 let mut cb = Checkbox::new().box_size(18.0).checked(true);
697 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
698
699 let mut canvas = RecordingCanvas::new();
700 cb.paint(&mut canvas);
701
702 assert_eq!(canvas.command_count(), 2);
704
705 match &canvas.commands()[1] {
707 DrawCommand::Rect { bounds, .. } => {
708 assert!((bounds.width - 9.0).abs() < 0.1);
710 assert!((bounds.height - 9.0).abs() < 0.1);
711 }
712 _ => panic!("Expected Rect command for checkmark"),
713 }
714 }
715
716 #[test]
717 fn test_checkbox_paint_indeterminate_draws_line() {
718 let mut cb = Checkbox::new()
719 .box_size(18.0)
720 .state(CheckState::Indeterminate);
721 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
722
723 let mut canvas = RecordingCanvas::new();
724 cb.paint(&mut canvas);
725
726 assert_eq!(canvas.command_count(), 2);
728
729 match &canvas.commands()[1] {
731 DrawCommand::Rect { bounds, .. } => {
732 assert!((bounds.width - 10.8).abs() < 0.1);
734 assert!((bounds.height - 3.6).abs() < 0.1);
735 }
736 _ => panic!("Expected Rect command for indeterminate line"),
737 }
738 }
739
740 #[test]
741 fn test_checkbox_paint_with_label() {
742 let mut cb = Checkbox::new().box_size(18.0).label("Test label");
743 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
744
745 let mut canvas = RecordingCanvas::new();
746 cb.paint(&mut canvas);
747
748 assert_eq!(canvas.command_count(), 2);
750
751 match &canvas.commands()[1] {
753 DrawCommand::Text { content, .. } => {
754 assert_eq!(content, "Test label");
755 }
756 _ => panic!("Expected Text command for label"),
757 }
758 }
759
760 #[test]
761 fn test_checkbox_paint_checked_with_label() {
762 let mut cb = Checkbox::new().box_size(18.0).checked(true).label("Accept");
763 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
764
765 let mut canvas = RecordingCanvas::new();
766 cb.paint(&mut canvas);
767
768 assert_eq!(canvas.command_count(), 3);
770
771 match &canvas.commands()[2] {
773 DrawCommand::Text { content, .. } => {
774 assert_eq!(content, "Accept");
775 }
776 _ => panic!("Expected Text command for label"),
777 }
778 }
779
780 #[test]
781 fn test_checkbox_paint_uses_checked_color() {
782 let mut cb = Checkbox::new()
783 .box_size(18.0)
784 .checked(true)
785 .checked_color(Color::RED);
786 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
787
788 let mut canvas = RecordingCanvas::new();
789 cb.paint(&mut canvas);
790
791 match &canvas.commands()[0] {
793 DrawCommand::Rect { style, .. } => {
794 assert_eq!(style.fill, Some(Color::RED));
795 }
796 _ => panic!("Expected Rect command"),
797 }
798 }
799
800 #[test]
801 fn test_checkbox_paint_uses_check_color() {
802 let mut cb = Checkbox::new()
803 .box_size(18.0)
804 .checked(true)
805 .check_color(Color::GREEN);
806 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
807
808 let mut canvas = RecordingCanvas::new();
809 cb.paint(&mut canvas);
810
811 match &canvas.commands()[1] {
813 DrawCommand::Rect { style, .. } => {
814 assert_eq!(style.fill, Some(Color::GREEN));
815 }
816 _ => panic!("Expected Rect command for checkmark"),
817 }
818 }
819
820 #[test]
821 fn test_checkbox_paint_disabled_no_checkmark() {
822 let mut cb = Checkbox::new().box_size(18.0).checked(true).disabled(true);
823 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
824
825 let mut canvas = RecordingCanvas::new();
826 cb.paint(&mut canvas);
827
828 assert_eq!(canvas.command_count(), 1);
830 }
831
832 #[test]
833 fn test_checkbox_paint_disabled_uses_disabled_color() {
834 let mut cb = Checkbox::new()
835 .box_size(18.0)
836 .disabled(true)
837 .label("Disabled");
838 let disabled_color = cb.disabled_color;
839 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
840
841 let mut canvas = RecordingCanvas::new();
842 cb.paint(&mut canvas);
843
844 match &canvas.commands()[0] {
846 DrawCommand::Rect { style, .. } => {
847 assert_eq!(style.fill, Some(disabled_color));
848 }
849 _ => panic!("Expected Rect command"),
850 }
851 }
852
853 #[test]
854 fn test_checkbox_paint_label_position() {
855 let mut cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Label");
856 cb.layout(Rect::new(10.0, 20.0, 200.0, 18.0));
857
858 let mut canvas = RecordingCanvas::new();
859 cb.paint(&mut canvas);
860
861 match &canvas.commands()[1] {
863 DrawCommand::Text { position, .. } => {
864 assert_eq!(position.x, 36.0);
866 }
867 _ => panic!("Expected Text command"),
868 }
869 }
870
871 #[test]
872 fn test_checkbox_paint_box_position_from_layout() {
873 let mut cb = Checkbox::new().box_size(18.0);
874 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
875
876 let mut canvas = RecordingCanvas::new();
877 cb.paint(&mut canvas);
878
879 match &canvas.commands()[0] {
880 DrawCommand::Rect { bounds, .. } => {
881 assert_eq!(bounds.x, 50.0);
882 }
883 _ => panic!("Expected Rect command"),
884 }
885 }
886
887 #[test]
888 fn test_checkbox_paint_custom_box_size() {
889 let mut cb = Checkbox::new().box_size(24.0).checked(true);
890 cb.layout(Rect::new(0.0, 0.0, 100.0, 24.0));
891
892 let mut canvas = RecordingCanvas::new();
893 cb.paint(&mut canvas);
894
895 match &canvas.commands()[0] {
897 DrawCommand::Rect { bounds, .. } => {
898 assert_eq!(bounds.width, 24.0);
899 assert_eq!(bounds.height, 24.0);
900 }
901 _ => panic!("Expected Rect command"),
902 }
903
904 match &canvas.commands()[1] {
906 DrawCommand::Rect { bounds, .. } => {
907 assert_eq!(bounds.width, 12.0);
908 assert_eq!(bounds.height, 12.0);
909 }
910 _ => panic!("Expected Rect command for checkmark"),
911 }
912 }
913
914 use presentar_core::{MouseButton, Point};
919
920 #[test]
921 fn test_checkbox_event_click_toggles_unchecked_to_checked() {
922 let mut cb = Checkbox::new().box_size(18.0);
923 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
924
925 assert!(!cb.is_checked());
926 let result = cb.event(&Event::MouseDown {
927 position: Point::new(9.0, 9.0),
928 button: MouseButton::Left,
929 });
930 assert!(cb.is_checked());
931 assert!(result.is_some());
932 }
933
934 #[test]
935 fn test_checkbox_event_click_toggles_checked_to_unchecked() {
936 let mut cb = Checkbox::new().box_size(18.0).checked(true);
937 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
938
939 assert!(cb.is_checked());
940 let result = cb.event(&Event::MouseDown {
941 position: Point::new(9.0, 9.0),
942 button: MouseButton::Left,
943 });
944 assert!(!cb.is_checked());
945 assert!(result.is_some());
946 }
947
948 #[test]
949 fn test_checkbox_event_click_indeterminate_to_unchecked() {
950 let mut cb = Checkbox::new()
951 .box_size(18.0)
952 .state(CheckState::Indeterminate);
953 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
954
955 assert!(cb.is_indeterminate());
956 let result = cb.event(&Event::MouseDown {
957 position: Point::new(9.0, 9.0),
958 button: MouseButton::Left,
959 });
960 assert!(!cb.is_checked());
962 assert!(!cb.is_indeterminate());
963 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
964 assert_eq!(msg.state, CheckState::Unchecked);
965 }
966
967 #[test]
968 fn test_checkbox_event_emits_checkbox_changed() {
969 let mut cb = Checkbox::new().box_size(18.0);
970 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
971
972 let result = cb.event(&Event::MouseDown {
973 position: Point::new(9.0, 9.0),
974 button: MouseButton::Left,
975 });
976
977 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
978 assert_eq!(msg.state, CheckState::Checked);
979 }
980
981 #[test]
982 fn test_checkbox_event_message_reflects_new_state() {
983 let mut cb = Checkbox::new().box_size(18.0).checked(true);
984 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
985
986 let result = cb.event(&Event::MouseDown {
987 position: Point::new(9.0, 9.0),
988 button: MouseButton::Left,
989 });
990
991 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
992 assert_eq!(msg.state, CheckState::Unchecked);
993 }
994
995 #[test]
996 fn test_checkbox_event_click_outside_bounds_no_toggle() {
997 let mut cb = Checkbox::new().box_size(18.0);
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(200.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_right_click_no_toggle() {
1010 let mut cb = Checkbox::new().box_size(18.0);
1011 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1012
1013 let result = cb.event(&Event::MouseDown {
1014 position: Point::new(9.0, 9.0),
1015 button: MouseButton::Right,
1016 });
1017 assert!(!cb.is_checked());
1018 assert!(result.is_none());
1019 }
1020
1021 #[test]
1022 fn test_checkbox_event_mouse_move_sets_hover() {
1023 let mut cb = Checkbox::new().box_size(18.0);
1024 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1025
1026 assert!(!cb.hovered);
1027 cb.event(&Event::MouseMove {
1028 position: Point::new(50.0, 9.0),
1029 });
1030 assert!(cb.hovered);
1031 }
1032
1033 #[test]
1034 fn test_checkbox_event_mouse_move_clears_hover() {
1035 let mut cb = Checkbox::new().box_size(18.0);
1036 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1037 cb.hovered = true;
1038
1039 cb.event(&Event::MouseMove {
1040 position: Point::new(200.0, 200.0),
1041 });
1042 assert!(!cb.hovered);
1043 }
1044
1045 #[test]
1046 fn test_checkbox_event_disabled_blocks_click() {
1047 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
1048 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1049
1050 let result = cb.event(&Event::MouseDown {
1051 position: Point::new(9.0, 9.0),
1052 button: MouseButton::Left,
1053 });
1054 assert!(!cb.is_checked());
1055 assert!(result.is_none());
1056 }
1057
1058 #[test]
1059 fn test_checkbox_event_disabled_blocks_hover() {
1060 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
1061 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1062
1063 cb.event(&Event::MouseMove {
1064 position: Point::new(50.0, 9.0),
1065 });
1066 assert!(!cb.hovered);
1067 }
1068
1069 #[test]
1070 fn test_checkbox_event_click_on_label_area_toggles() {
1071 let mut cb = Checkbox::new().box_size(18.0).label("Accept terms");
1072 cb.layout(Rect::new(0.0, 0.0, 150.0, 18.0));
1073
1074 let result = cb.event(&Event::MouseDown {
1076 position: Point::new(100.0, 9.0),
1077 button: MouseButton::Left,
1078 });
1079 assert!(cb.is_checked());
1080 assert!(result.is_some());
1081 }
1082
1083 #[test]
1084 fn test_checkbox_event_full_interaction_flow() {
1085 let mut cb = Checkbox::new().box_size(18.0);
1086 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1087
1088 assert!(!cb.is_checked());
1090 assert!(!cb.hovered);
1091
1092 cb.event(&Event::MouseMove {
1094 position: Point::new(50.0, 9.0),
1095 });
1096 assert!(cb.hovered);
1097
1098 let result = cb.event(&Event::MouseDown {
1100 position: Point::new(9.0, 9.0),
1101 button: MouseButton::Left,
1102 });
1103 assert!(cb.is_checked());
1104 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1105 assert_eq!(msg.state, CheckState::Checked);
1106
1107 let result = cb.event(&Event::MouseDown {
1109 position: Point::new(9.0, 9.0),
1110 button: MouseButton::Left,
1111 });
1112 assert!(!cb.is_checked());
1113 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1114 assert_eq!(msg.state, CheckState::Unchecked);
1115
1116 cb.event(&Event::MouseMove {
1118 position: Point::new(200.0, 200.0),
1119 });
1120 assert!(!cb.hovered);
1121 }
1122
1123 #[test]
1124 fn test_checkbox_event_with_offset_bounds() {
1125 let mut cb = Checkbox::new().box_size(18.0);
1126 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
1127
1128 let result = cb.event(&Event::MouseDown {
1130 position: Point::new(100.0, 109.0),
1131 button: MouseButton::Left,
1132 });
1133 assert!(cb.is_checked());
1134 assert!(result.is_some());
1135 }
1136}