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)]
425#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
426mod tests {
427 use super::*;
428 use presentar_core::Widget;
429
430 #[test]
435 fn test_check_state_default() {
436 assert_eq!(CheckState::default(), CheckState::Unchecked);
437 }
438
439 #[test]
440 fn test_check_state_toggle() {
441 assert_eq!(CheckState::Unchecked.toggle(), CheckState::Checked);
442 assert_eq!(CheckState::Checked.toggle(), CheckState::Unchecked);
443 assert_eq!(CheckState::Indeterminate.toggle(), CheckState::Unchecked);
444 }
445
446 #[test]
447 fn test_check_state_is_checked() {
448 assert!(!CheckState::Unchecked.is_checked());
449 assert!(CheckState::Checked.is_checked());
450 assert!(!CheckState::Indeterminate.is_checked());
451 }
452
453 #[test]
454 fn test_check_state_is_indeterminate() {
455 assert!(!CheckState::Unchecked.is_indeterminate());
456 assert!(!CheckState::Checked.is_indeterminate());
457 assert!(CheckState::Indeterminate.is_indeterminate());
458 }
459
460 #[test]
465 fn test_checkbox_changed_message() {
466 let msg = CheckboxChanged {
467 state: CheckState::Checked,
468 };
469 assert_eq!(msg.state, CheckState::Checked);
470 }
471
472 #[test]
477 fn test_checkbox_new() {
478 let cb = Checkbox::new();
479 assert_eq!(cb.get_state(), CheckState::Unchecked);
480 assert!(!cb.is_checked());
481 assert!(!cb.disabled);
482 assert!(cb.get_label().is_empty());
483 }
484
485 #[test]
486 fn test_checkbox_default() {
487 let cb = Checkbox::default();
488 assert_eq!(cb.get_state(), CheckState::Unchecked);
489 }
490
491 #[test]
492 fn test_checkbox_builder() {
493 let cb = Checkbox::new()
494 .checked(true)
495 .label("Accept terms")
496 .disabled(false)
497 .box_size(20.0)
498 .spacing(10.0)
499 .with_test_id("terms-checkbox")
500 .with_accessible_name("Terms and Conditions");
501
502 assert!(cb.is_checked());
503 assert_eq!(cb.get_label(), "Accept terms");
504 assert!(!cb.disabled);
505 assert_eq!(Widget::test_id(&cb), Some("terms-checkbox"));
506 assert_eq!(cb.accessible_name(), Some("Terms and Conditions"));
507 }
508
509 #[test]
510 fn test_checkbox_state_builder() {
511 let cb = Checkbox::new().state(CheckState::Indeterminate);
512 assert!(cb.is_indeterminate());
513 assert!(!cb.is_checked());
514 }
515
516 #[test]
521 fn test_checkbox_checked_true() {
522 let cb = Checkbox::new().checked(true);
523 assert!(cb.is_checked());
524 assert_eq!(cb.get_state(), CheckState::Checked);
525 }
526
527 #[test]
528 fn test_checkbox_checked_false() {
529 let cb = Checkbox::new().checked(false);
530 assert!(!cb.is_checked());
531 assert_eq!(cb.get_state(), CheckState::Unchecked);
532 }
533
534 #[test]
535 fn test_checkbox_indeterminate() {
536 let cb = Checkbox::new().state(CheckState::Indeterminate);
537 assert!(cb.is_indeterminate());
538 assert!(!cb.is_checked());
539 }
540
541 #[test]
546 fn test_checkbox_type_id() {
547 let cb = Checkbox::new();
548 assert_eq!(Widget::type_id(&cb), TypeId::of::<Checkbox>());
549 }
550
551 #[test]
552 fn test_checkbox_measure_no_label() {
553 let cb = Checkbox::new().box_size(18.0);
554 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
555 assert_eq!(size.width, 18.0 + 8.0); assert_eq!(size.height, 18.0);
557 }
558
559 #[test]
560 fn test_checkbox_measure_with_label() {
561 let cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Test");
562 let size = cb.measure(Constraints::loose(Size::new(200.0, 100.0)));
563 assert!(size.width > 18.0);
565 }
566
567 #[test]
568 fn test_checkbox_is_interactive() {
569 let cb = Checkbox::new();
570 assert!(cb.is_interactive());
571
572 let cb = Checkbox::new().disabled(true);
573 assert!(!cb.is_interactive());
574 }
575
576 #[test]
577 fn test_checkbox_is_focusable() {
578 let cb = Checkbox::new();
579 assert!(cb.is_focusable());
580
581 let cb = Checkbox::new().disabled(true);
582 assert!(!cb.is_focusable());
583 }
584
585 #[test]
586 fn test_checkbox_accessible_role() {
587 let cb = Checkbox::new();
588 assert_eq!(cb.accessible_role(), AccessibleRole::Checkbox);
589 }
590
591 #[test]
592 fn test_checkbox_accessible_name_from_label() {
593 let cb = Checkbox::new().label("My checkbox");
594 assert_eq!(cb.accessible_name(), Some("My checkbox"));
595 }
596
597 #[test]
598 fn test_checkbox_accessible_name_override() {
599 let cb = Checkbox::new()
600 .label("Short")
601 .with_accessible_name("Full accessible name");
602 assert_eq!(cb.accessible_name(), Some("Full accessible name"));
603 }
604
605 #[test]
606 fn test_checkbox_children() {
607 let cb = Checkbox::new();
608 assert!(cb.children().is_empty());
609 }
610
611 #[test]
616 fn test_checkbox_colors() {
617 let cb = Checkbox::new()
618 .checked_color(Color::RED)
619 .check_color(Color::GREEN)
620 .label_color(Color::BLUE);
621
622 assert_eq!(cb.checked_color, Color::RED);
623 assert_eq!(cb.check_color, Color::GREEN);
624 assert_eq!(cb.label_color, Color::BLUE);
625 }
626
627 #[test]
632 fn test_checkbox_layout() {
633 let mut cb = Checkbox::new();
634 let bounds = Rect::new(10.0, 20.0, 100.0, 30.0);
635 let result = cb.layout(bounds);
636 assert_eq!(result.size, bounds.size());
637 assert_eq!(cb.bounds, bounds);
638 }
639
640 #[test]
645 fn test_checkbox_box_size_min() {
646 let cb = Checkbox::new().box_size(2.0);
647 assert_eq!(cb.box_size, 8.0); }
649
650 #[test]
651 fn test_checkbox_spacing_min() {
652 let cb = Checkbox::new().spacing(-5.0);
653 assert_eq!(cb.spacing, 0.0); }
655
656 use presentar_core::draw::DrawCommand;
661 use presentar_core::RecordingCanvas;
662
663 #[test]
664 fn test_checkbox_paint_unchecked_draws_box() {
665 let mut cb = Checkbox::new().box_size(18.0);
666 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
667
668 let mut canvas = RecordingCanvas::new();
669 cb.paint(&mut canvas);
670
671 assert!(canvas.command_count() >= 1);
673 match &canvas.commands()[0] {
674 DrawCommand::Rect { bounds, style, .. } => {
675 assert_eq!(bounds.width, 18.0);
676 assert_eq!(bounds.height, 18.0);
677 assert!(style.fill.is_some());
678 }
679 _ => panic!("Expected Rect command for checkbox box"),
680 }
681 }
682
683 #[test]
684 fn test_checkbox_paint_unchecked_no_checkmark() {
685 let mut cb = Checkbox::new().box_size(18.0);
686 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
687
688 let mut canvas = RecordingCanvas::new();
689 cb.paint(&mut canvas);
690
691 assert_eq!(canvas.command_count(), 1);
693 }
694
695 #[test]
696 fn test_checkbox_paint_checked_draws_checkmark() {
697 let mut cb = Checkbox::new().box_size(18.0).checked(true);
698 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
699
700 let mut canvas = RecordingCanvas::new();
701 cb.paint(&mut canvas);
702
703 assert_eq!(canvas.command_count(), 2);
705
706 match &canvas.commands()[1] {
708 DrawCommand::Rect { bounds, .. } => {
709 assert!((bounds.width - 9.0).abs() < 0.1);
711 assert!((bounds.height - 9.0).abs() < 0.1);
712 }
713 _ => panic!("Expected Rect command for checkmark"),
714 }
715 }
716
717 #[test]
718 fn test_checkbox_paint_indeterminate_draws_line() {
719 let mut cb = Checkbox::new()
720 .box_size(18.0)
721 .state(CheckState::Indeterminate);
722 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
723
724 let mut canvas = RecordingCanvas::new();
725 cb.paint(&mut canvas);
726
727 assert_eq!(canvas.command_count(), 2);
729
730 match &canvas.commands()[1] {
732 DrawCommand::Rect { bounds, .. } => {
733 assert!((bounds.width - 10.8).abs() < 0.1);
735 assert!((bounds.height - 3.6).abs() < 0.1);
736 }
737 _ => panic!("Expected Rect command for indeterminate line"),
738 }
739 }
740
741 #[test]
742 fn test_checkbox_paint_with_label() {
743 let mut cb = Checkbox::new().box_size(18.0).label("Test label");
744 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
745
746 let mut canvas = RecordingCanvas::new();
747 cb.paint(&mut canvas);
748
749 assert_eq!(canvas.command_count(), 2);
751
752 match &canvas.commands()[1] {
754 DrawCommand::Text { content, .. } => {
755 assert_eq!(content, "Test label");
756 }
757 _ => panic!("Expected Text command for label"),
758 }
759 }
760
761 #[test]
762 fn test_checkbox_paint_checked_with_label() {
763 let mut cb = Checkbox::new().box_size(18.0).checked(true).label("Accept");
764 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
765
766 let mut canvas = RecordingCanvas::new();
767 cb.paint(&mut canvas);
768
769 assert_eq!(canvas.command_count(), 3);
771
772 match &canvas.commands()[2] {
774 DrawCommand::Text { content, .. } => {
775 assert_eq!(content, "Accept");
776 }
777 _ => panic!("Expected Text command for label"),
778 }
779 }
780
781 #[test]
782 fn test_checkbox_paint_uses_checked_color() {
783 let mut cb = Checkbox::new()
784 .box_size(18.0)
785 .checked(true)
786 .checked_color(Color::RED);
787 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
788
789 let mut canvas = RecordingCanvas::new();
790 cb.paint(&mut canvas);
791
792 match &canvas.commands()[0] {
794 DrawCommand::Rect { style, .. } => {
795 assert_eq!(style.fill, Some(Color::RED));
796 }
797 _ => panic!("Expected Rect command"),
798 }
799 }
800
801 #[test]
802 fn test_checkbox_paint_uses_check_color() {
803 let mut cb = Checkbox::new()
804 .box_size(18.0)
805 .checked(true)
806 .check_color(Color::GREEN);
807 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
808
809 let mut canvas = RecordingCanvas::new();
810 cb.paint(&mut canvas);
811
812 match &canvas.commands()[1] {
814 DrawCommand::Rect { style, .. } => {
815 assert_eq!(style.fill, Some(Color::GREEN));
816 }
817 _ => panic!("Expected Rect command for checkmark"),
818 }
819 }
820
821 #[test]
822 fn test_checkbox_paint_disabled_no_checkmark() {
823 let mut cb = Checkbox::new().box_size(18.0).checked(true).disabled(true);
824 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
825
826 let mut canvas = RecordingCanvas::new();
827 cb.paint(&mut canvas);
828
829 assert_eq!(canvas.command_count(), 1);
831 }
832
833 #[test]
834 fn test_checkbox_paint_disabled_uses_disabled_color() {
835 let mut cb = Checkbox::new()
836 .box_size(18.0)
837 .disabled(true)
838 .label("Disabled");
839 let disabled_color = cb.disabled_color;
840 cb.layout(Rect::new(0.0, 0.0, 200.0, 18.0));
841
842 let mut canvas = RecordingCanvas::new();
843 cb.paint(&mut canvas);
844
845 match &canvas.commands()[0] {
847 DrawCommand::Rect { style, .. } => {
848 assert_eq!(style.fill, Some(disabled_color));
849 }
850 _ => panic!("Expected Rect command"),
851 }
852 }
853
854 #[test]
855 fn test_checkbox_paint_label_position() {
856 let mut cb = Checkbox::new().box_size(18.0).spacing(8.0).label("Label");
857 cb.layout(Rect::new(10.0, 20.0, 200.0, 18.0));
858
859 let mut canvas = RecordingCanvas::new();
860 cb.paint(&mut canvas);
861
862 match &canvas.commands()[1] {
864 DrawCommand::Text { position, .. } => {
865 assert_eq!(position.x, 36.0);
867 }
868 _ => panic!("Expected Text command"),
869 }
870 }
871
872 #[test]
873 fn test_checkbox_paint_box_position_from_layout() {
874 let mut cb = Checkbox::new().box_size(18.0);
875 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
876
877 let mut canvas = RecordingCanvas::new();
878 cb.paint(&mut canvas);
879
880 match &canvas.commands()[0] {
881 DrawCommand::Rect { bounds, .. } => {
882 assert_eq!(bounds.x, 50.0);
883 }
884 _ => panic!("Expected Rect command"),
885 }
886 }
887
888 #[test]
889 fn test_checkbox_paint_custom_box_size() {
890 let mut cb = Checkbox::new().box_size(24.0).checked(true);
891 cb.layout(Rect::new(0.0, 0.0, 100.0, 24.0));
892
893 let mut canvas = RecordingCanvas::new();
894 cb.paint(&mut canvas);
895
896 match &canvas.commands()[0] {
898 DrawCommand::Rect { bounds, .. } => {
899 assert_eq!(bounds.width, 24.0);
900 assert_eq!(bounds.height, 24.0);
901 }
902 _ => panic!("Expected Rect command"),
903 }
904
905 match &canvas.commands()[1] {
907 DrawCommand::Rect { bounds, .. } => {
908 assert_eq!(bounds.width, 12.0);
909 assert_eq!(bounds.height, 12.0);
910 }
911 _ => panic!("Expected Rect command for checkmark"),
912 }
913 }
914
915 use presentar_core::{MouseButton, Point};
920
921 #[test]
922 fn test_checkbox_event_click_toggles_unchecked_to_checked() {
923 let mut cb = Checkbox::new().box_size(18.0);
924 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
925
926 assert!(!cb.is_checked());
927 let result = cb.event(&Event::MouseDown {
928 position: Point::new(9.0, 9.0),
929 button: MouseButton::Left,
930 });
931 assert!(cb.is_checked());
932 assert!(result.is_some());
933 }
934
935 #[test]
936 fn test_checkbox_event_click_toggles_checked_to_unchecked() {
937 let mut cb = Checkbox::new().box_size(18.0).checked(true);
938 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
939
940 assert!(cb.is_checked());
941 let result = cb.event(&Event::MouseDown {
942 position: Point::new(9.0, 9.0),
943 button: MouseButton::Left,
944 });
945 assert!(!cb.is_checked());
946 assert!(result.is_some());
947 }
948
949 #[test]
950 fn test_checkbox_event_click_indeterminate_to_unchecked() {
951 let mut cb = Checkbox::new()
952 .box_size(18.0)
953 .state(CheckState::Indeterminate);
954 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
955
956 assert!(cb.is_indeterminate());
957 let result = cb.event(&Event::MouseDown {
958 position: Point::new(9.0, 9.0),
959 button: MouseButton::Left,
960 });
961 assert!(!cb.is_checked());
963 assert!(!cb.is_indeterminate());
964 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
965 assert_eq!(msg.state, CheckState::Unchecked);
966 }
967
968 #[test]
969 fn test_checkbox_event_emits_checkbox_changed() {
970 let mut cb = Checkbox::new().box_size(18.0);
971 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
972
973 let result = cb.event(&Event::MouseDown {
974 position: Point::new(9.0, 9.0),
975 button: MouseButton::Left,
976 });
977
978 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
979 assert_eq!(msg.state, CheckState::Checked);
980 }
981
982 #[test]
983 fn test_checkbox_event_message_reflects_new_state() {
984 let mut cb = Checkbox::new().box_size(18.0).checked(true);
985 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
986
987 let result = cb.event(&Event::MouseDown {
988 position: Point::new(9.0, 9.0),
989 button: MouseButton::Left,
990 });
991
992 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
993 assert_eq!(msg.state, CheckState::Unchecked);
994 }
995
996 #[test]
997 fn test_checkbox_event_click_outside_bounds_no_toggle() {
998 let mut cb = Checkbox::new().box_size(18.0);
999 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1000
1001 let result = cb.event(&Event::MouseDown {
1002 position: Point::new(200.0, 9.0),
1003 button: MouseButton::Left,
1004 });
1005 assert!(!cb.is_checked());
1006 assert!(result.is_none());
1007 }
1008
1009 #[test]
1010 fn test_checkbox_event_right_click_no_toggle() {
1011 let mut cb = Checkbox::new().box_size(18.0);
1012 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1013
1014 let result = cb.event(&Event::MouseDown {
1015 position: Point::new(9.0, 9.0),
1016 button: MouseButton::Right,
1017 });
1018 assert!(!cb.is_checked());
1019 assert!(result.is_none());
1020 }
1021
1022 #[test]
1023 fn test_checkbox_event_mouse_move_sets_hover() {
1024 let mut cb = Checkbox::new().box_size(18.0);
1025 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1026
1027 assert!(!cb.hovered);
1028 cb.event(&Event::MouseMove {
1029 position: Point::new(50.0, 9.0),
1030 });
1031 assert!(cb.hovered);
1032 }
1033
1034 #[test]
1035 fn test_checkbox_event_mouse_move_clears_hover() {
1036 let mut cb = Checkbox::new().box_size(18.0);
1037 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1038 cb.hovered = true;
1039
1040 cb.event(&Event::MouseMove {
1041 position: Point::new(200.0, 200.0),
1042 });
1043 assert!(!cb.hovered);
1044 }
1045
1046 #[test]
1047 fn test_checkbox_event_disabled_blocks_click() {
1048 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
1049 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1050
1051 let result = cb.event(&Event::MouseDown {
1052 position: Point::new(9.0, 9.0),
1053 button: MouseButton::Left,
1054 });
1055 assert!(!cb.is_checked());
1056 assert!(result.is_none());
1057 }
1058
1059 #[test]
1060 fn test_checkbox_event_disabled_blocks_hover() {
1061 let mut cb = Checkbox::new().box_size(18.0).disabled(true);
1062 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1063
1064 cb.event(&Event::MouseMove {
1065 position: Point::new(50.0, 9.0),
1066 });
1067 assert!(!cb.hovered);
1068 }
1069
1070 #[test]
1071 fn test_checkbox_event_click_on_label_area_toggles() {
1072 let mut cb = Checkbox::new().box_size(18.0).label("Accept terms");
1073 cb.layout(Rect::new(0.0, 0.0, 150.0, 18.0));
1074
1075 let result = cb.event(&Event::MouseDown {
1077 position: Point::new(100.0, 9.0),
1078 button: MouseButton::Left,
1079 });
1080 assert!(cb.is_checked());
1081 assert!(result.is_some());
1082 }
1083
1084 #[test]
1085 fn test_checkbox_event_full_interaction_flow() {
1086 let mut cb = Checkbox::new().box_size(18.0);
1087 cb.layout(Rect::new(0.0, 0.0, 100.0, 18.0));
1088
1089 assert!(!cb.is_checked());
1091 assert!(!cb.hovered);
1092
1093 cb.event(&Event::MouseMove {
1095 position: Point::new(50.0, 9.0),
1096 });
1097 assert!(cb.hovered);
1098
1099 let result = cb.event(&Event::MouseDown {
1101 position: Point::new(9.0, 9.0),
1102 button: MouseButton::Left,
1103 });
1104 assert!(cb.is_checked());
1105 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1106 assert_eq!(msg.state, CheckState::Checked);
1107
1108 let result = cb.event(&Event::MouseDown {
1110 position: Point::new(9.0, 9.0),
1111 button: MouseButton::Left,
1112 });
1113 assert!(!cb.is_checked());
1114 let msg = result.unwrap().downcast::<CheckboxChanged>().unwrap();
1115 assert_eq!(msg.state, CheckState::Unchecked);
1116
1117 cb.event(&Event::MouseMove {
1119 position: Point::new(200.0, 200.0),
1120 });
1121 assert!(!cb.hovered);
1122 }
1123
1124 #[test]
1125 fn test_checkbox_event_with_offset_bounds() {
1126 let mut cb = Checkbox::new().box_size(18.0);
1127 cb.layout(Rect::new(50.0, 100.0, 100.0, 18.0));
1128
1129 let result = cb.event(&Event::MouseDown {
1131 position: Point::new(100.0, 109.0),
1132 button: MouseButton::Left,
1133 });
1134 assert!(cb.is_checked());
1135 assert!(result.is_some());
1136 }
1137}