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 } if self.bounds.contains_point(position) => {
330 self.state = self.state.toggle();
331 return Some(Box::new(CheckboxChanged { state: self.state }));
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
374impl Brick for Checkbox {
376 fn brick_name(&self) -> &'static str {
377 "Checkbox"
378 }
379
380 fn assertions(&self) -> &[BrickAssertion] {
381 &[BrickAssertion::MaxLatencyMs(16)]
382 }
383
384 fn budget(&self) -> BrickBudget {
385 BrickBudget::uniform(16)
386 }
387
388 fn verify(&self) -> BrickVerification {
389 BrickVerification {
390 passed: self.assertions().to_vec(),
391 failed: vec![],
392 verification_time: Duration::from_micros(10),
393 }
394 }
395
396 fn to_html(&self) -> String {
397 let test_id = self.test_id_value.as_deref().unwrap_or("checkbox");
398 let checked = if self.state.is_checked() {
399 " checked"
400 } else {
401 ""
402 };
403 let disabled = if self.disabled { " disabled" } else { "" };
404 format!(
405 r#"<input type="checkbox" class="brick-checkbox" data-testid="{}" aria-label="{}"{}{}/>"#,
406 test_id,
407 self.accessible_name_value.as_deref().unwrap_or(&self.label),
408 checked,
409 disabled
410 )
411 }
412
413 fn to_css(&self) -> String {
414 ".brick-checkbox { display: inline-block; }".into()
415 }
416
417 fn test_id(&self) -> Option<&str> {
418 self.test_id_value.as_deref()
419 }
420}
421
422#[cfg(test)]
423#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
424mod tests {
425 use super::*;
426 use presentar_core::Widget;
427
428 #[test]
433 fn test_check_state_default() {
434 assert_eq!(CheckState::default(), CheckState::Unchecked);
435 }
436
437 #[test]
438 fn test_check_state_toggle() {
439 assert_eq!(CheckState::Unchecked.toggle(), CheckState::Checked);
440 assert_eq!(CheckState::Checked.toggle(), CheckState::Unchecked);
441 assert_eq!(CheckState::Indeterminate.toggle(), CheckState::Unchecked);
442 }
443
444 #[test]
445 fn test_check_state_is_checked() {
446 assert!(!CheckState::Unchecked.is_checked());
447 assert!(CheckState::Checked.is_checked());
448 assert!(!CheckState::Indeterminate.is_checked());
449 }
450
451 #[test]
452 fn test_check_state_is_indeterminate() {
453 assert!(!CheckState::Unchecked.is_indeterminate());
454 assert!(!CheckState::Checked.is_indeterminate());
455 assert!(CheckState::Indeterminate.is_indeterminate());
456 }
457
458 #[test]
463 fn test_checkbox_changed_message() {
464 let msg = CheckboxChanged {
465 state: CheckState::Checked,
466 };
467 assert_eq!(msg.state, CheckState::Checked);
468 }
469
470 #[test]
475 fn test_checkbox_new() {
476 let cb = Checkbox::new();
477 assert_eq!(cb.get_state(), CheckState::Unchecked);
478 assert!(!cb.is_checked());
479 assert!(!cb.disabled);
480 assert!(cb.get_label().is_empty());
481 }
482
483 #[test]
484 fn test_checkbox_default() {
485 let cb = Checkbox::default();
486 assert_eq!(cb.get_state(), CheckState::Unchecked);
487 }
488
489 #[test]
490 fn test_checkbox_builder() {
491 let cb = Checkbox::new()
492 .checked(true)
493 .label("Accept terms")
494 .disabled(false)
495 .box_size(20.0)
496 .spacing(10.0)
497 .with_test_id("terms-checkbox")
498 .with_accessible_name("Terms and Conditions");
499
500 assert!(cb.is_checked());
501 assert_eq!(cb.get_label(), "Accept terms");
502 assert!(!cb.disabled);
503 assert_eq!(Widget::test_id(&cb), Some("terms-checkbox"));
504 assert_eq!(cb.accessible_name(), Some("Terms and Conditions"));
505 }
506
507 #[test]
508 fn test_checkbox_state_builder() {
509 let cb = Checkbox::new().state(CheckState::Indeterminate);
510 assert!(cb.is_indeterminate());
511 assert!(!cb.is_checked());
512 }
513
514 #[test]
519 fn test_checkbox_checked_true() {
520 let cb = Checkbox::new().checked(true);
521 assert!(cb.is_checked());
522 assert_eq!(cb.get_state(), CheckState::Checked);
523 }
524
525 #[test]
526 fn test_checkbox_checked_false() {
527 let cb = Checkbox::new().checked(false);
528 assert!(!cb.is_checked());
529 assert_eq!(cb.get_state(), CheckState::Unchecked);
530 }
531
532 #[test]
533 fn test_checkbox_indeterminate() {
534 let cb = Checkbox::new().state(CheckState::Indeterminate);
535 assert!(cb.is_indeterminate());
536 assert!(!cb.is_checked());
537 }
538
539 #[test]
544 fn test_checkbox_type_id() {
545 let cb = Checkbox::new();
546 assert_eq!(Widget::type_id(&cb), TypeId::of::<Checkbox>());
547 }
548
549 #[test]
550 fn test_checkbox_measure_no_label() {
551 let cb = Checkbox::new().box_size(18.0);
552 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
553 assert_eq!(size.width, 18.0 + 8.0); assert_eq!(size.height, 18.0);
555 }
556
557 #[test]
558 fn test_checkbox_measure_with_label() {
559 let cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Test");
560 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
561 assert!(size.width > 18.0);
563 }
564
565 #[test]
566 fn test_checkbox_is_interactive() {
567 let cb = Checkbox::new();
568 assert!(cb.is_interactive());
569
570 let cb = Checkbox::new().disabled(true);
571 assert!(!cb.is_interactive());
572 }
573
574 #[test]
575 fn test_checkbox_is_focusable() {
576 let cb = Checkbox::new();
577 assert!(cb.is_focusable());
578
579 let cb = Checkbox::new().disabled(true);
580 assert!(!cb.is_focusable());
581 }
582
583 #[test]
584 fn test_checkbox_accessible_role() {
585 let cb = Checkbox::new();
586 assert_eq!(cb.accessible_role(), AccessibleRole::Checkbox);
587 }
588
589 #[test]
590 fn test_checkbox_accessible_name_from_label() {
591 let cb = Checkbox::new().label("My checkbox");
592 assert_eq!(cb.accessible_name(), Some("My checkbox"));
593 }
594
595 #[test]
596 fn test_checkbox_accessible_name_override() {
597 let cb = Checkbox::new()
598 .label("Short")
599 .with_accessible_name("Full accessible name");
600 assert_eq!(cb.accessible_name(), Some("Full accessible name"));
601 }
602
603 #[test]
604 fn test_checkbox_children() {
605 let cb = Checkbox::new();
606 assert!(cb.children().is_empty());
607 }
608
609 #[test]
614 fn test_checkbox_colors() {
615 let cb = Checkbox::new()
616 .checked_color(Color::RED)
617 .check_color(Color::GREEN)
618 .label_color(Color::BLUE);
619
620 assert_eq!(cb.checked_color, Color::RED);
621 assert_eq!(cb.check_color, Color::GREEN);
622 assert_eq!(cb.label_color, Color::BLUE);
623 }
624
625 #[test]
630 fn test_checkbox_layout() {
631 let mut cb = Checkbox::new();
632 let bounds = Rect::new(10.0, 20.0, 100.0, 30.0);
633 let result = cb.layout(bounds);
634 assert_eq!(result.size, bounds.size());
635 assert_eq!(cb.bounds, bounds);
636 }
637
638 #[test]
643 fn test_checkbox_box_size_min() {
644 let cb = Checkbox::new().box_size(2.0);
645 assert_eq!(cb.box_size, 8.0); }
647
648 #[test]
649 fn test_checkbox_spacing_min() {
650 let cb = Checkbox::new().spacing(-5.0);
651 assert_eq!(cb.spacing, 0.0); }
653
654 use presentar_core::draw::DrawCommand;
659 use presentar_core::RecordingCanvas;
660
661 #[test]
662 fn test_checkbox_paint_unchecked_draws_box() {
663 let mut cb = Checkbox::new().box_size(18.0);
664 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
665
666 let mut canvas = RecordingCanvas::new();
667 cb.paint(&mut canvas);
668
669 assert!(canvas.command_count() >= 1);
671 match &canvas.commands()[0] {
672 DrawCommand::Rect { bounds, style, .. } => {
673 assert_eq!(bounds.width, 18.0);
674 assert_eq!(bounds.height, 18.0);
675 assert!(style.fill.is_some());
676 }
677 _ => panic!("Expected Rect command for checkbox box"),
678 }
679 }
680
681 #[test]
682 fn test_checkbox_paint_unchecked_no_checkmark() {
683 let mut cb = Checkbox::new().box_size(18.0);
684 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
685
686 let mut canvas = RecordingCanvas::new();
687 cb.paint(&mut canvas);
688
689 assert_eq!(canvas.command_count(), 1);
691 }
692
693 #[test]
694 fn test_checkbox_paint_checked_draws_checkmark() {
695 let mut cb = Checkbox::new().box_size(18.0).checked(true);
696 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
697
698 let mut canvas = RecordingCanvas::new();
699 cb.paint(&mut canvas);
700
701 assert_eq!(canvas.command_count(), 2);
703
704 match &canvas.commands()[1] {
706 DrawCommand::Rect { bounds, .. } => {
707 assert!((bounds.width - 9.0).abs() < 0.1);
709 assert!((bounds.height - 9.0).abs() < 0.1);
710 }
711 _ => panic!("Expected Rect command for checkmark"),
712 }
713 }
714
715 #[test]
716 fn test_checkbox_paint_indeterminate_draws_line() {
717 let mut cb = Checkbox::new()
718 .box_size(18.0)
719 .state(CheckState::Indeterminate);
720 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
721
722 let mut canvas = RecordingCanvas::new();
723 cb.paint(&mut canvas);
724
725 assert_eq!(canvas.command_count(), 2);
727
728 match &canvas.commands()[1] {
730 DrawCommand::Rect { bounds, .. } => {
731 assert!((bounds.width - 10.8).abs() < 0.1);
733 assert!((bounds.height - 3.6).abs() < 0.1);
734 }
735 _ => panic!("Expected Rect command for indeterminate line"),
736 }
737 }
738
739 #[test]
740 fn test_checkbox_paint_with_label() {
741 let mut cb = Checkbox::new().box_size(18.0).label("Test label");
742 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
743
744 let mut canvas = RecordingCanvas::new();
745 cb.paint(&mut canvas);
746
747 assert_eq!(canvas.command_count(), 2);
749
750 match &canvas.commands()[1] {
752 DrawCommand::Text { content, .. } => {
753 assert_eq!(content, "Test label");
754 }
755 _ => panic!("Expected Text command for label"),
756 }
757 }
758
759 #[test]
760 fn test_checkbox_paint_checked_with_label() {
761 let mut cb = Checkbox::new().box_size(18.0).checked(true).label("Accept");
762 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
763
764 let mut canvas = RecordingCanvas::new();
765 cb.paint(&mut canvas);
766
767 assert_eq!(canvas.command_count(), 3);
769
770 match &canvas.commands()[2] {
772 DrawCommand::Text { content, .. } => {
773 assert_eq!(content, "Accept");
774 }
775 _ => panic!("Expected Text command for label"),
776 }
777 }
778
779 #[test]
780 fn test_checkbox_paint_uses_checked_color() {
781 let mut cb = Checkbox::new()
782 .box_size(18.0)
783 .checked(true)
784 .checked_color(Color::RED);
785 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
786
787 let mut canvas = RecordingCanvas::new();
788 cb.paint(&mut canvas);
789
790 match &canvas.commands()[0] {
792 DrawCommand::Rect { style, .. } => {
793 assert_eq!(style.fill, Some(Color::RED));
794 }
795 _ => panic!("Expected Rect command"),
796 }
797 }
798
799 #[test]
800 fn test_checkbox_paint_uses_check_color() {
801 let mut cb = Checkbox::new()
802 .box_size(18.0)
803 .checked(true)
804 .check_color(Color::GREEN);
805 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
806
807 let mut canvas = RecordingCanvas::new();
808 cb.paint(&mut canvas);
809
810 match &canvas.commands()[1] {
812 DrawCommand::Rect { style, .. } => {
813 assert_eq!(style.fill, Some(Color::GREEN));
814 }
815 _ => panic!("Expected Rect command for checkmark"),
816 }
817 }
818
819 #[test]
820 fn test_checkbox_paint_disabled_no_checkmark() {
821 let mut cb = Checkbox::new().box_size(18.0).checked(true).disabled(true);
822 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
823
824 let mut canvas = RecordingCanvas::new();
825 cb.paint(&mut canvas);
826
827 assert_eq!(canvas.command_count(), 1);
829 }
830
831 #[test]
832 fn test_checkbox_paint_disabled_uses_disabled_color() {
833 let mut cb = Checkbox::new()
834 .box_size(18.0)
835 .disabled(true)
836 .label("Disabled");
837 let disabled_color = cb.disabled_color;
838 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
839
840 let mut canvas = RecordingCanvas::new();
841 cb.paint(&mut canvas);
842
843 match &canvas.commands()[0] {
845 DrawCommand::Rect { style, .. } => {
846 assert_eq!(style.fill, Some(disabled_color));
847 }
848 _ => panic!("Expected Rect command"),
849 }
850 }
851
852 #[test]
853 fn test_checkbox_paint_label_position() {
854 let mut cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Label");
855 cb.layout(Rect::new(10.0, 20.0, 200.0, 18.0));
856
857 let mut canvas = RecordingCanvas::new();
858 cb.paint(&mut canvas);
859
860 match &canvas.commands()[1] {
862 DrawCommand::Text { position, .. } => {
863 assert_eq!(position.x, 36.0);
865 }
866 _ => panic!("Expected Text command"),
867 }
868 }
869
870 #[test]
871 fn test_checkbox_paint_box_position_from_layout() {
872 let mut cb = Checkbox::new().box_size(18.0);
873 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
874
875 let mut canvas = RecordingCanvas::new();
876 cb.paint(&mut canvas);
877
878 match &canvas.commands()[0] {
879 DrawCommand::Rect { bounds, .. } => {
880 assert_eq!(bounds.x, 50.0);
881 }
882 _ => panic!("Expected Rect command"),
883 }
884 }
885
886 #[test]
887 fn test_checkbox_paint_custom_box_size() {
888 let mut cb = Checkbox::new().box_size(24.0).checked(true);
889 cb.layout(Rect::new(0.0, 0.0, 100.0, 24.0));
890
891 let mut canvas = RecordingCanvas::new();
892 cb.paint(&mut canvas);
893
894 match &canvas.commands()[0] {
896 DrawCommand::Rect { bounds, .. } => {
897 assert_eq!(bounds.width, 24.0);
898 assert_eq!(bounds.height, 24.0);
899 }
900 _ => panic!("Expected Rect command"),
901 }
902
903 match &canvas.commands()[1] {
905 DrawCommand::Rect { bounds, .. } => {
906 assert_eq!(bounds.width, 12.0);
907 assert_eq!(bounds.height, 12.0);
908 }
909 _ => panic!("Expected Rect command for checkmark"),
910 }
911 }
912
913 use presentar_core::{MouseButton, Point};
918
919 #[test]
920 fn test_checkbox_event_click_toggles_unchecked_to_checked() {
921 let mut cb = Checkbox::new().box_size(18.0);
922 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
923
924 assert!(!cb.is_checked());
925 let result = cb.event(&Event::MouseDown {
926 position: Point::new(9.0, 9.0),
927 button: MouseButton::Left,
928 });
929 assert!(cb.is_checked());
930 assert!(result.is_some());
931 }
932
933 #[test]
934 fn test_checkbox_event_click_toggles_checked_to_unchecked() {
935 let mut cb = Checkbox::new().box_size(18.0).checked(true);
936 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
937
938 assert!(cb.is_checked());
939 let result = cb.event(&Event::MouseDown {
940 position: Point::new(9.0, 9.0),
941 button: MouseButton::Left,
942 });
943 assert!(!cb.is_checked());
944 assert!(result.is_some());
945 }
946
947 #[test]
948 fn test_checkbox_event_click_indeterminate_to_unchecked() {
949 let mut cb = Checkbox::new()
950 .box_size(18.0)
951 .state(CheckState::Indeterminate);
952 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
953
954 assert!(cb.is_indeterminate());
955 let result = cb.event(&Event::MouseDown {
956 position: Point::new(9.0, 9.0),
957 button: MouseButton::Left,
958 });
959 assert!(!cb.is_checked());
961 assert!(!cb.is_indeterminate());
962 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
963 assert_eq!(msg.state, CheckState::Unchecked);
964 }
965
966 #[test]
967 fn test_checkbox_event_emits_checkbox_changed() {
968 let mut cb = Checkbox::new().box_size(18.0);
969 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
970
971 let result = cb.event(&Event::MouseDown {
972 position: Point::new(9.0, 9.0),
973 button: MouseButton::Left,
974 });
975
976 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
977 assert_eq!(msg.state, CheckState::Checked);
978 }
979
980 #[test]
981 fn test_checkbox_event_message_reflects_new_state() {
982 let mut cb = Checkbox::new().box_size(18.0).checked(true);
983 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
984
985 let result = cb.event(&Event::MouseDown {
986 position: Point::new(9.0, 9.0),
987 button: MouseButton::Left,
988 });
989
990 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
991 assert_eq!(msg.state, CheckState::Unchecked);
992 }
993
994 #[test]
995 fn test_checkbox_event_click_outside_bounds_no_toggle() {
996 let mut cb = Checkbox::new().box_size(18.0);
997 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
998
999 let result = cb.event(&Event::MouseDown {
1000 position: Point::new(200.0, 9.0),
1001 button: MouseButton::Left,
1002 });
1003 assert!(!cb.is_checked());
1004 assert!(result.is_none());
1005 }
1006
1007 #[test]
1008 fn test_checkbox_event_right_click_no_toggle() {
1009 let mut cb = Checkbox::new().box_size(18.0);
1010 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1011
1012 let result = cb.event(&Event::MouseDown {
1013 position: Point::new(9.0, 9.0),
1014 button: MouseButton::Right,
1015 });
1016 assert!(!cb.is_checked());
1017 assert!(result.is_none());
1018 }
1019
1020 #[test]
1021 fn test_checkbox_event_mouse_move_sets_hover() {
1022 let mut cb = Checkbox::new().box_size(18.0);
1023 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1024
1025 assert!(!cb.hovered);
1026 cb.event(&Event::MouseMove {
1027 position: Point::new(50.0, 9.0),
1028 });
1029 assert!(cb.hovered);
1030 }
1031
1032 #[test]
1033 fn test_checkbox_event_mouse_move_clears_hover() {
1034 let mut cb = Checkbox::new().box_size(18.0);
1035 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1036 cb.hovered = true;
1037
1038 cb.event(&Event::MouseMove {
1039 position: Point::new(200.0, 200.0),
1040 });
1041 assert!(!cb.hovered);
1042 }
1043
1044 #[test]
1045 fn test_checkbox_event_disabled_blocks_click() {
1046 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
1047 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1048
1049 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 assert!(result.is_none());
1055 }
1056
1057 #[test]
1058 fn test_checkbox_event_disabled_blocks_hover() {
1059 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
1060 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1061
1062 cb.event(&Event::MouseMove {
1063 position: Point::new(50.0, 9.0),
1064 });
1065 assert!(!cb.hovered);
1066 }
1067
1068 #[test]
1069 fn test_checkbox_event_click_on_label_area_toggles() {
1070 let mut cb = Checkbox::new().box_size(18.0).label("Accept terms");
1071 cb.layout(Rect::new(0.0, 0.0, 150.0, 18.0));
1072
1073 let result = cb.event(&Event::MouseDown {
1075 position: Point::new(100.0, 9.0),
1076 button: MouseButton::Left,
1077 });
1078 assert!(cb.is_checked());
1079 assert!(result.is_some());
1080 }
1081
1082 #[test]
1083 fn test_checkbox_event_full_interaction_flow() {
1084 let mut cb = Checkbox::new().box_size(18.0);
1085 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1086
1087 assert!(!cb.is_checked());
1089 assert!(!cb.hovered);
1090
1091 cb.event(&Event::MouseMove {
1093 position: Point::new(50.0, 9.0),
1094 });
1095 assert!(cb.hovered);
1096
1097 let result = cb.event(&Event::MouseDown {
1099 position: Point::new(9.0, 9.0),
1100 button: MouseButton::Left,
1101 });
1102 assert!(cb.is_checked());
1103 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1104 assert_eq!(msg.state, CheckState::Checked);
1105
1106 let result = cb.event(&Event::MouseDown {
1108 position: Point::new(9.0, 9.0),
1109 button: MouseButton::Left,
1110 });
1111 assert!(!cb.is_checked());
1112 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1113 assert_eq!(msg.state, CheckState::Unchecked);
1114
1115 cb.event(&Event::MouseMove {
1117 position: Point::new(200.0, 200.0),
1118 });
1119 assert!(!cb.hovered);
1120 }
1121
1122 #[test]
1123 fn test_checkbox_event_with_offset_bounds() {
1124 let mut cb = Checkbox::new().box_size(18.0);
1125 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
1126
1127 let result = cb.event(&Event::MouseDown {
1129 position: Point::new(100.0, 109.0),
1130 button: MouseButton::Left,
1131 });
1132 assert!(cb.is_checked());
1133 assert!(result.is_some());
1134 }
1135}