1use presentar_core::{
4 widget::{
5 AccessibleRole, Brick, BrickAssertion, BrickBudget, BrickVerification, LayoutResult,
6 TextStyle,
7 },
8 Canvas, Color, Constraints, Event, MouseButton, Point, Rect, Size, TypeId, Widget,
9};
10use serde::{Deserialize, Serialize};
11use std::any::Any;
12use std::time::Duration;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct RadioOption {
17 pub value: String,
19 pub label: String,
21 pub disabled: bool,
23}
24
25impl RadioOption {
26 #[must_use]
28 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
29 Self {
30 value: value.into(),
31 label: label.into(),
32 disabled: false,
33 }
34 }
35
36 #[must_use]
38 pub const fn disabled(mut self) -> Self {
39 self.disabled = true;
40 self
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct RadioChanged {
47 pub value: String,
49 pub index: usize,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
55pub enum RadioOrientation {
56 #[default]
58 Vertical,
59 Horizontal,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct RadioGroup {
66 options: Vec<RadioOption>,
68 selected: Option<usize>,
70 orientation: RadioOrientation,
72 spacing: f32,
74 radio_size: f32,
76 label_gap: f32,
78 border_color: Color,
80 fill_color: Color,
82 label_color: Color,
84 disabled_color: Color,
86 accessible_name_value: Option<String>,
88 test_id_value: Option<String>,
90 #[serde(skip)]
92 bounds: Rect,
93}
94
95impl Default for RadioGroup {
96 fn default() -> Self {
97 Self {
98 options: Vec::new(),
99 selected: None,
100 orientation: RadioOrientation::Vertical,
101 spacing: 8.0,
102 radio_size: 20.0,
103 label_gap: 8.0,
104 border_color: Color::new(0.6, 0.6, 0.6, 1.0),
105 fill_color: Color::new(0.2, 0.47, 0.96, 1.0),
106 label_color: Color::new(0.1, 0.1, 0.1, 1.0),
107 disabled_color: Color::new(0.6, 0.6, 0.6, 1.0),
108 accessible_name_value: None,
109 test_id_value: None,
110 bounds: Rect::default(),
111 }
112 }
113}
114
115impl RadioGroup {
116 #[must_use]
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 #[must_use]
124 pub fn option(mut self, option: RadioOption) -> Self {
125 self.options.push(option);
126 self
127 }
128
129 #[must_use]
131 pub fn options(mut self, options: impl IntoIterator<Item = RadioOption>) -> Self {
132 self.options.extend(options);
133 self
134 }
135
136 #[must_use]
138 pub fn selected(mut self, value: &str) -> Self {
139 self.selected = self.options.iter().position(|o| o.value == value);
140 self
141 }
142
143 #[must_use]
145 pub fn selected_index(mut self, index: usize) -> Self {
146 if index < self.options.len() {
147 self.selected = Some(index);
148 }
149 self
150 }
151
152 #[must_use]
154 pub const fn orientation(mut self, orientation: RadioOrientation) -> Self {
155 self.orientation = orientation;
156 self
157 }
158
159 #[must_use]
161 pub fn spacing(mut self, spacing: f32) -> Self {
162 self.spacing = spacing.max(0.0);
163 self
164 }
165
166 #[must_use]
168 pub fn radio_size(mut self, size: f32) -> Self {
169 self.radio_size = size.max(12.0);
170 self
171 }
172
173 #[must_use]
175 pub fn label_gap(mut self, gap: f32) -> Self {
176 self.label_gap = gap.max(0.0);
177 self
178 }
179
180 #[must_use]
182 pub const fn border_color(mut self, color: Color) -> Self {
183 self.border_color = color;
184 self
185 }
186
187 #[must_use]
189 pub const fn fill_color(mut self, color: Color) -> Self {
190 self.fill_color = color;
191 self
192 }
193
194 #[must_use]
196 pub const fn label_color(mut self, color: Color) -> Self {
197 self.label_color = color;
198 self
199 }
200
201 #[must_use]
203 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
204 self.accessible_name_value = Some(name.into());
205 self
206 }
207
208 #[must_use]
210 pub fn test_id(mut self, id: impl Into<String>) -> Self {
211 self.test_id_value = Some(id.into());
212 self
213 }
214
215 #[must_use]
217 pub fn get_options(&self) -> &[RadioOption] {
218 &self.options
219 }
220
221 #[must_use]
223 pub fn get_selected(&self) -> Option<&str> {
224 self.selected
225 .and_then(|i| self.options.get(i))
226 .map(|o| o.value.as_str())
227 }
228
229 #[must_use]
231 pub const fn get_selected_index(&self) -> Option<usize> {
232 self.selected
233 }
234
235 #[must_use]
237 pub fn get_selected_option(&self) -> Option<&RadioOption> {
238 self.selected.and_then(|i| self.options.get(i))
239 }
240
241 #[must_use]
243 pub fn is_selected(&self, value: &str) -> bool {
244 self.get_selected() == Some(value)
245 }
246
247 #[must_use]
249 pub fn is_index_selected(&self, index: usize) -> bool {
250 self.selected == Some(index)
251 }
252
253 #[must_use]
255 pub const fn has_selection(&self) -> bool {
256 self.selected.is_some()
257 }
258
259 #[must_use]
261 pub fn option_count(&self) -> usize {
262 self.options.len()
263 }
264
265 #[must_use]
267 pub fn is_empty(&self) -> bool {
268 self.options.is_empty()
269 }
270
271 pub fn set_selected(&mut self, value: &str) {
273 if let Some(index) = self.options.iter().position(|o| o.value == value) {
274 if !self.options[index].disabled {
275 self.selected = Some(index);
276 }
277 }
278 }
279
280 pub fn set_selected_index(&mut self, index: usize) {
282 if index < self.options.len() && !self.options[index].disabled {
283 self.selected = Some(index);
284 }
285 }
286
287 pub fn clear_selection(&mut self) {
289 self.selected = None;
290 }
291
292 pub fn select_next(&mut self) {
294 if self.options.is_empty() {
295 return;
296 }
297 let start = self.selected.map_or(0, |i| i + 1);
298 for offset in 0..self.options.len() {
299 let idx = (start + offset) % self.options.len();
300 if !self.options[idx].disabled {
301 self.selected = Some(idx);
302 return;
303 }
304 }
305 }
306
307 pub fn select_prev(&mut self) {
309 if self.options.is_empty() {
310 return;
311 }
312 let start = self.selected.map_or(self.options.len() - 1, |i| {
313 if i == 0 {
314 self.options.len() - 1
315 } else {
316 i - 1
317 }
318 });
319 for offset in 0..self.options.len() {
320 let idx = if start >= offset {
321 start - offset
322 } else {
323 self.options.len() - (offset - start)
324 };
325 if !self.options[idx].disabled {
326 self.selected = Some(idx);
327 return;
328 }
329 }
330 }
331
332 fn item_size(&self) -> Size {
334 let label_width = 100.0;
336 Size::new(
337 self.radio_size + self.label_gap + label_width,
338 self.radio_size.max(20.0),
339 )
340 }
341
342 fn option_rect(&self, index: usize) -> Rect {
344 let item = self.item_size();
345 match self.orientation {
346 RadioOrientation::Vertical => {
347 let y = (index as f32).mul_add(item.height + self.spacing, self.bounds.y);
348 Rect::new(self.bounds.x, y, self.bounds.width, item.height)
349 }
350 RadioOrientation::Horizontal => {
351 let x = (index as f32).mul_add(item.width + self.spacing, self.bounds.x);
352 Rect::new(x, self.bounds.y, item.width, item.height)
353 }
354 }
355 }
356
357 fn option_at_point(&self, x: f32, y: f32) -> Option<usize> {
359 for (i, _) in self.options.iter().enumerate() {
360 let rect = self.option_rect(i);
361 if x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height {
362 return Some(i);
363 }
364 }
365 None
366 }
367}
368
369impl Widget for RadioGroup {
370 fn type_id(&self) -> TypeId {
371 TypeId::of::<Self>()
372 }
373
374 fn measure(&self, constraints: Constraints) -> Size {
375 let item = self.item_size();
376 let count = self.options.len() as f32;
377
378 let preferred = match self.orientation {
379 RadioOrientation::Vertical => {
380 let total_spacing = if count > 1.0 {
381 self.spacing * (count - 1.0)
382 } else {
383 0.0
384 };
385 Size::new(item.width, count.mul_add(item.height, total_spacing))
386 }
387 RadioOrientation::Horizontal => {
388 let total_spacing = if count > 1.0 {
389 self.spacing * (count - 1.0)
390 } else {
391 0.0
392 };
393 Size::new(count.mul_add(item.width, total_spacing), item.height)
394 }
395 };
396
397 constraints.constrain(preferred)
398 }
399
400 fn layout(&mut self, bounds: Rect) -> LayoutResult {
401 self.bounds = bounds;
402 LayoutResult {
403 size: bounds.size(),
404 }
405 }
406
407 fn paint(&self, canvas: &mut dyn Canvas) {
408 for (i, option) in self.options.iter().enumerate() {
409 let rect = self.option_rect(i);
410 let is_selected = self.selected == Some(i);
411
412 let cx = rect.x + self.radio_size / 2.0;
414 let cy = rect.y + rect.height / 2.0;
415 let radius = self.radio_size / 2.0;
416
417 let border_rect = Rect::new(cx - radius, cy - radius, self.radio_size, self.radio_size);
419 let border_color = if option.disabled {
420 self.disabled_color
421 } else if is_selected {
422 self.fill_color
423 } else {
424 self.border_color
425 };
426 canvas.stroke_rect(border_rect, border_color, 2.0);
427
428 if is_selected {
430 let inner_radius = radius * 0.5;
431 let inner_rect = Rect::new(
432 cx - inner_radius,
433 cy - inner_radius,
434 inner_radius * 2.0,
435 inner_radius * 2.0,
436 );
437 canvas.fill_rect(inner_rect, self.fill_color);
438 }
439
440 let text_color = if option.disabled {
442 self.disabled_color
443 } else {
444 self.label_color
445 };
446
447 let text_style = TextStyle {
448 size: 14.0,
449 color: text_color,
450 ..TextStyle::default()
451 };
452
453 canvas.draw_text(
454 &option.label,
455 Point::new(rect.x + self.radio_size + self.label_gap, cy),
456 &text_style,
457 );
458 }
459 }
460
461 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
462 if let Event::MouseDown {
463 position,
464 button: MouseButton::Left,
465 } = event
466 {
467 if let Some(index) = self.option_at_point(position.x, position.y) {
468 if !self.options[index].disabled && self.selected != Some(index) {
469 self.selected = Some(index);
470 return Some(Box::new(RadioChanged {
471 value: self.options[index].value.clone(),
472 index,
473 }));
474 }
475 }
476 }
477 None
478 }
479
480 fn children(&self) -> &[Box<dyn Widget>] {
481 &[]
482 }
483
484 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
485 &mut []
486 }
487
488 fn is_interactive(&self) -> bool {
489 !self.options.is_empty()
490 }
491
492 fn is_focusable(&self) -> bool {
493 !self.options.is_empty()
494 }
495
496 fn accessible_name(&self) -> Option<&str> {
497 self.accessible_name_value.as_deref()
498 }
499
500 fn accessible_role(&self) -> AccessibleRole {
501 AccessibleRole::RadioGroup
502 }
503
504 fn test_id(&self) -> Option<&str> {
505 self.test_id_value.as_deref()
506 }
507}
508
509impl Brick for RadioGroup {
511 fn brick_name(&self) -> &'static str {
512 "RadioGroup"
513 }
514
515 fn assertions(&self) -> &[BrickAssertion] {
516 &[BrickAssertion::MaxLatencyMs(16)]
517 }
518
519 fn budget(&self) -> BrickBudget {
520 BrickBudget::uniform(16)
521 }
522
523 fn verify(&self) -> BrickVerification {
524 BrickVerification {
525 passed: self.assertions().to_vec(),
526 failed: vec![],
527 verification_time: Duration::from_micros(10),
528 }
529 }
530
531 fn to_html(&self) -> String {
532 r#"<div class="brick-radiogroup"></div>"#.to_string()
533 }
534
535 fn to_css(&self) -> String {
536 ".brick-radiogroup { display: block; }".to_string()
537 }
538}
539
540#[cfg(test)]
541#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
542mod tests {
543 use super::*;
544
545 #[test]
548 fn test_radio_option_new() {
549 let opt = RadioOption::new("val", "Label");
550 assert_eq!(opt.value, "val");
551 assert_eq!(opt.label, "Label");
552 assert!(!opt.disabled);
553 }
554
555 #[test]
556 fn test_radio_option_disabled() {
557 let opt = RadioOption::new("val", "Label").disabled();
558 assert!(opt.disabled);
559 }
560
561 #[test]
562 fn test_radio_option_equality() {
563 let opt1 = RadioOption::new("a", "A");
564 let opt2 = RadioOption::new("a", "A");
565 let opt3 = RadioOption::new("b", "B");
566 assert_eq!(opt1, opt2);
567 assert_ne!(opt1, opt3);
568 }
569
570 #[test]
573 fn test_radio_changed() {
574 let msg = RadioChanged {
575 value: "option1".to_string(),
576 index: 1,
577 };
578 assert_eq!(msg.value, "option1");
579 assert_eq!(msg.index, 1);
580 }
581
582 #[test]
585 fn test_radio_orientation_default() {
586 assert_eq!(RadioOrientation::default(), RadioOrientation::Vertical);
587 }
588
589 #[test]
592 fn test_radio_group_new() {
593 let group = RadioGroup::new();
594 assert!(group.is_empty());
595 assert_eq!(group.option_count(), 0);
596 assert!(!group.has_selection());
597 }
598
599 #[test]
600 fn test_radio_group_builder() {
601 let group = RadioGroup::new()
602 .option(RadioOption::new("a", "Option A"))
603 .option(RadioOption::new("b", "Option B"))
604 .option(RadioOption::new("c", "Option C"))
605 .selected("b")
606 .orientation(RadioOrientation::Horizontal)
607 .spacing(12.0)
608 .radio_size(24.0)
609 .label_gap(10.0)
610 .accessible_name("Choose option")
611 .test_id("radio-test");
612
613 assert_eq!(group.option_count(), 3);
614 assert_eq!(group.get_selected(), Some("b"));
615 assert_eq!(group.get_selected_index(), Some(1));
616 assert_eq!(Widget::accessible_name(&group), Some("Choose option"));
617 assert_eq!(Widget::test_id(&group), Some("radio-test"));
618 }
619
620 #[test]
621 fn test_radio_group_options_iter() {
622 let opts = vec![RadioOption::new("x", "X"), RadioOption::new("y", "Y")];
623 let group = RadioGroup::new().options(opts);
624 assert_eq!(group.option_count(), 2);
625 }
626
627 #[test]
628 fn test_radio_group_selected_index() {
629 let group = RadioGroup::new()
630 .option(RadioOption::new("a", "A"))
631 .option(RadioOption::new("b", "B"))
632 .selected_index(1);
633
634 assert_eq!(group.get_selected(), Some("b"));
635 }
636
637 #[test]
638 fn test_radio_group_selected_not_found() {
639 let group = RadioGroup::new()
640 .option(RadioOption::new("a", "A"))
641 .selected("nonexistent");
642
643 assert!(!group.has_selection());
644 }
645
646 #[test]
649 fn test_radio_group_get_selected_option() {
650 let group = RadioGroup::new()
651 .option(RadioOption::new("first", "First"))
652 .option(RadioOption::new("second", "Second"))
653 .selected("second");
654
655 let opt = group.get_selected_option().unwrap();
656 assert_eq!(opt.value, "second");
657 assert_eq!(opt.label, "Second");
658 }
659
660 #[test]
661 fn test_radio_group_is_selected() {
662 let group = RadioGroup::new()
663 .option(RadioOption::new("a", "A"))
664 .option(RadioOption::new("b", "B"))
665 .selected("a");
666
667 assert!(group.is_selected("a"));
668 assert!(!group.is_selected("b"));
669 }
670
671 #[test]
672 fn test_radio_group_is_index_selected() {
673 let group = RadioGroup::new()
674 .option(RadioOption::new("a", "A"))
675 .option(RadioOption::new("b", "B"))
676 .selected_index(0);
677
678 assert!(group.is_index_selected(0));
679 assert!(!group.is_index_selected(1));
680 }
681
682 #[test]
685 fn test_radio_group_set_selected() {
686 let mut group = RadioGroup::new()
687 .option(RadioOption::new("a", "A"))
688 .option(RadioOption::new("b", "B"));
689
690 group.set_selected("b");
691 assert_eq!(group.get_selected(), Some("b"));
692 }
693
694 #[test]
695 fn test_radio_group_set_selected_disabled() {
696 let mut group = RadioGroup::new()
697 .option(RadioOption::new("a", "A"))
698 .option(RadioOption::new("b", "B").disabled());
699
700 group.set_selected("b");
701 assert!(!group.has_selection()); }
703
704 #[test]
705 fn test_radio_group_set_selected_index() {
706 let mut group = RadioGroup::new()
707 .option(RadioOption::new("a", "A"))
708 .option(RadioOption::new("b", "B"));
709
710 group.set_selected_index(1);
711 assert_eq!(group.get_selected_index(), Some(1));
712 }
713
714 #[test]
715 fn test_radio_group_set_selected_index_out_of_bounds() {
716 let mut group = RadioGroup::new().option(RadioOption::new("a", "A"));
717
718 group.set_selected_index(10);
719 assert!(!group.has_selection());
720 }
721
722 #[test]
723 fn test_radio_group_clear_selection() {
724 let mut group = RadioGroup::new()
725 .option(RadioOption::new("a", "A"))
726 .selected("a");
727
728 assert!(group.has_selection());
729 group.clear_selection();
730 assert!(!group.has_selection());
731 }
732
733 #[test]
736 fn test_radio_group_select_next() {
737 let mut group = RadioGroup::new()
738 .option(RadioOption::new("a", "A"))
739 .option(RadioOption::new("b", "B"))
740 .option(RadioOption::new("c", "C"))
741 .selected_index(0);
742
743 group.select_next();
744 assert_eq!(group.get_selected_index(), Some(1));
745
746 group.select_next();
747 assert_eq!(group.get_selected_index(), Some(2));
748
749 group.select_next(); assert_eq!(group.get_selected_index(), Some(0));
751 }
752
753 #[test]
754 fn test_radio_group_select_next_skip_disabled() {
755 let mut group = RadioGroup::new()
756 .option(RadioOption::new("a", "A"))
757 .option(RadioOption::new("b", "B").disabled())
758 .option(RadioOption::new("c", "C"))
759 .selected_index(0);
760
761 group.select_next();
762 assert_eq!(group.get_selected_index(), Some(2));
763 }
764
765 #[test]
766 fn test_radio_group_select_next_no_selection() {
767 let mut group = RadioGroup::new()
768 .option(RadioOption::new("a", "A"))
769 .option(RadioOption::new("b", "B"));
770
771 group.select_next();
772 assert_eq!(group.get_selected_index(), Some(0));
773 }
774
775 #[test]
776 fn test_radio_group_select_prev() {
777 let mut group = RadioGroup::new()
778 .option(RadioOption::new("a", "A"))
779 .option(RadioOption::new("b", "B"))
780 .option(RadioOption::new("c", "C"))
781 .selected_index(2);
782
783 group.select_prev();
784 assert_eq!(group.get_selected_index(), Some(1));
785
786 group.select_prev();
787 assert_eq!(group.get_selected_index(), Some(0));
788
789 group.select_prev(); assert_eq!(group.get_selected_index(), Some(2));
791 }
792
793 #[test]
794 fn test_radio_group_select_prev_skip_disabled() {
795 let mut group = RadioGroup::new()
796 .option(RadioOption::new("a", "A"))
797 .option(RadioOption::new("b", "B").disabled())
798 .option(RadioOption::new("c", "C"))
799 .selected_index(2);
800
801 group.select_prev();
802 assert_eq!(group.get_selected_index(), Some(0));
803 }
804
805 #[test]
808 fn test_radio_group_spacing_min() {
809 let group = RadioGroup::new().spacing(-5.0);
810 assert_eq!(group.spacing, 0.0);
811 }
812
813 #[test]
814 fn test_radio_group_radio_size_min() {
815 let group = RadioGroup::new().radio_size(5.0);
816 assert_eq!(group.radio_size, 12.0);
817 }
818
819 #[test]
820 fn test_radio_group_label_gap_min() {
821 let group = RadioGroup::new().label_gap(-5.0);
822 assert_eq!(group.label_gap, 0.0);
823 }
824
825 #[test]
828 fn test_radio_group_type_id() {
829 let group = RadioGroup::new();
830 assert_eq!(Widget::type_id(&group), TypeId::of::<RadioGroup>());
831 }
832
833 #[test]
834 fn test_radio_group_measure_vertical() {
835 let group = RadioGroup::new()
836 .option(RadioOption::new("a", "A"))
837 .option(RadioOption::new("b", "B"))
838 .option(RadioOption::new("c", "C"))
839 .orientation(RadioOrientation::Vertical)
840 .radio_size(20.0)
841 .spacing(8.0);
842
843 let size = group.measure(Constraints::loose(Size::new(500.0, 500.0)));
844 assert!(size.height > 0.0);
846 assert!(size.width > 0.0);
847 }
848
849 #[test]
850 fn test_radio_group_measure_horizontal() {
851 let group = RadioGroup::new()
852 .option(RadioOption::new("a", "A"))
853 .option(RadioOption::new("b", "B"))
854 .orientation(RadioOrientation::Horizontal)
855 .spacing(8.0);
856
857 let size = group.measure(Constraints::loose(Size::new(500.0, 500.0)));
858 assert!(size.width > 0.0);
859 assert!(size.height > 0.0);
860 }
861
862 #[test]
863 fn test_radio_group_layout() {
864 let mut group = RadioGroup::new().option(RadioOption::new("a", "A"));
865 let bounds = Rect::new(10.0, 20.0, 200.0, 100.0);
866 let result = group.layout(bounds);
867 assert_eq!(result.size, Size::new(200.0, 100.0));
868 assert_eq!(group.bounds, bounds);
869 }
870
871 #[test]
872 fn test_radio_group_children() {
873 let group = RadioGroup::new();
874 assert!(group.children().is_empty());
875 }
876
877 #[test]
878 fn test_radio_group_is_interactive() {
879 let group = RadioGroup::new();
880 assert!(!group.is_interactive()); let group = RadioGroup::new().option(RadioOption::new("a", "A"));
883 assert!(group.is_interactive());
884 }
885
886 #[test]
887 fn test_radio_group_is_focusable() {
888 let group = RadioGroup::new();
889 assert!(!group.is_focusable()); let group = RadioGroup::new().option(RadioOption::new("a", "A"));
892 assert!(group.is_focusable());
893 }
894
895 #[test]
896 fn test_radio_group_accessible_role() {
897 let group = RadioGroup::new();
898 assert_eq!(group.accessible_role(), AccessibleRole::RadioGroup);
899 }
900
901 #[test]
902 fn test_radio_group_accessible_name() {
903 let group = RadioGroup::new().accessible_name("Select size");
904 assert_eq!(Widget::accessible_name(&group), Some("Select size"));
905 }
906
907 #[test]
908 fn test_radio_group_test_id() {
909 let group = RadioGroup::new().test_id("size-radio");
910 assert_eq!(Widget::test_id(&group), Some("size-radio"));
911 }
912
913 #[test]
916 fn test_radio_group_click_selects() {
917 let mut group = RadioGroup::new()
918 .option(RadioOption::new("a", "A"))
919 .option(RadioOption::new("b", "B"))
920 .radio_size(20.0)
921 .spacing(8.0);
922 group.bounds = Rect::new(0.0, 0.0, 200.0, 56.0);
923
924 let event = Event::MouseDown {
926 position: Point::new(10.0, 38.0),
927 button: MouseButton::Left,
928 };
929
930 let result = group.event(&event);
931 assert!(result.is_some());
932 assert_eq!(group.get_selected_index(), Some(1));
933
934 let msg = result.unwrap().downcast::<RadioChanged>().unwrap();
935 assert_eq!(msg.value, "b");
936 assert_eq!(msg.index, 1);
937 }
938
939 #[test]
940 fn test_radio_group_click_disabled_no_change() {
941 let mut group = RadioGroup::new()
942 .option(RadioOption::new("a", "A"))
943 .option(RadioOption::new("b", "B").disabled())
944 .radio_size(20.0)
945 .spacing(8.0);
946 group.bounds = Rect::new(0.0, 0.0, 200.0, 56.0);
947
948 let event = Event::MouseDown {
950 position: Point::new(10.0, 38.0),
951 button: MouseButton::Left,
952 };
953
954 let result = group.event(&event);
955 assert!(result.is_none());
956 assert!(!group.has_selection());
957 }
958
959 #[test]
960 fn test_radio_group_click_same_no_event() {
961 let mut group = RadioGroup::new()
962 .option(RadioOption::new("a", "A"))
963 .option(RadioOption::new("b", "B"))
964 .selected_index(0)
965 .radio_size(20.0);
966 group.bounds = Rect::new(0.0, 0.0, 200.0, 56.0);
967
968 let event = Event::MouseDown {
970 position: Point::new(10.0, 10.0),
971 button: MouseButton::Left,
972 };
973
974 let result = group.event(&event);
975 assert!(result.is_none());
976 }
977
978 #[test]
981 fn test_radio_group_colors() {
982 let group = RadioGroup::new()
983 .border_color(Color::RED)
984 .fill_color(Color::GREEN)
985 .label_color(Color::BLUE);
986
987 assert_eq!(group.border_color, Color::RED);
988 assert_eq!(group.fill_color, Color::GREEN);
989 assert_eq!(group.label_color, Color::BLUE);
990 }
991
992 #[test]
993 fn test_radio_group_right_click_no_select() {
994 let mut group = RadioGroup::new()
995 .option(RadioOption::new("a", "A"))
996 .radio_size(20.0);
997 group.bounds = Rect::new(0.0, 0.0, 200.0, 28.0);
998
999 let result = group.event(&Event::MouseDown {
1000 position: Point::new(10.0, 10.0),
1001 button: MouseButton::Right,
1002 });
1003 assert!(group.selected.is_none());
1004 assert!(result.is_none());
1005 }
1006
1007 #[test]
1008 fn test_radio_group_click_outside_no_select() {
1009 let mut group = RadioGroup::new()
1010 .option(RadioOption::new("a", "A"))
1011 .radio_size(20.0);
1012 group.bounds = Rect::new(0.0, 0.0, 200.0, 28.0);
1013
1014 let result = group.event(&Event::MouseDown {
1015 position: Point::new(10.0, 100.0),
1016 button: MouseButton::Left,
1017 });
1018 assert!(group.selected.is_none());
1019 assert!(result.is_none());
1020 }
1021
1022 #[test]
1023 fn test_radio_group_click_with_offset_bounds() {
1024 let mut group = RadioGroup::new()
1025 .option(RadioOption::new("a", "A"))
1026 .option(RadioOption::new("b", "B"))
1027 .radio_size(20.0)
1028 .spacing(8.0);
1029 group.bounds = Rect::new(50.0, 100.0, 200.0, 56.0);
1030
1031 let result = group.event(&Event::MouseDown {
1032 position: Point::new(60.0, 138.0), button: MouseButton::Left,
1034 });
1035 assert_eq!(group.selected, Some(1));
1036 assert!(result.is_some());
1037 }
1038}