1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult},
5 Canvas, Color, Constraints, Event, MouseButton, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct SelectOption {
13 pub value: String,
15 pub label: String,
17 pub disabled: bool,
19}
20
21impl SelectOption {
22 #[must_use]
24 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
25 Self {
26 value: value.into(),
27 label: label.into(),
28 disabled: false,
29 }
30 }
31
32 #[must_use]
34 pub fn simple(text: impl Into<String>) -> Self {
35 let text = text.into();
36 Self {
37 value: text.clone(),
38 label: text,
39 disabled: false,
40 }
41 }
42
43 #[must_use]
45 pub const fn disabled(mut self, disabled: bool) -> Self {
46 self.disabled = disabled;
47 self
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct SelectionChanged {
54 pub value: Option<String>,
56 pub index: Option<usize>,
58}
59
60#[derive(Serialize, Deserialize)]
62pub struct Select {
63 options: Vec<SelectOption>,
65 selected: Option<usize>,
67 placeholder: String,
69 #[serde(skip)]
71 open: bool,
72 disabled: bool,
74 min_width: f32,
76 item_height: f32,
78 max_visible_items: usize,
80 background_color: Color,
82 border_color: Color,
84 selected_bg_color: Color,
86 hover_bg_color: Color,
88 text_color: Color,
90 placeholder_color: Color,
92 disabled_color: Color,
94 test_id_value: Option<String>,
96 accessible_name_value: Option<String>,
98 #[serde(skip)]
100 bounds: Rect,
101 #[serde(skip)]
103 hovered_item: Option<usize>,
104}
105
106impl Default for Select {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl Select {
113 #[must_use]
115 pub fn new() -> Self {
116 Self {
117 options: Vec::new(),
118 selected: None,
119 placeholder: "Select...".to_string(),
120 open: false,
121 disabled: false,
122 min_width: 150.0,
123 item_height: 32.0,
124 max_visible_items: 8,
125 background_color: Color::WHITE,
126 border_color: Color::new(0.8, 0.8, 0.8, 1.0),
127 selected_bg_color: Color::new(0.9, 0.95, 1.0, 1.0),
128 hover_bg_color: Color::new(0.95, 0.95, 0.95, 1.0),
129 text_color: Color::BLACK,
130 placeholder_color: Color::new(0.6, 0.6, 0.6, 1.0),
131 disabled_color: Color::new(0.7, 0.7, 0.7, 1.0),
132 test_id_value: None,
133 accessible_name_value: None,
134 bounds: Rect::default(),
135 hovered_item: None,
136 }
137 }
138
139 #[must_use]
141 pub fn option(mut self, opt: SelectOption) -> Self {
142 self.options.push(opt);
143 self
144 }
145
146 #[must_use]
148 pub fn options(mut self, opts: impl IntoIterator<Item = SelectOption>) -> Self {
149 self.options.extend(opts);
150 self
151 }
152
153 #[must_use]
155 pub fn options_from_strings(
156 mut self,
157 values: impl IntoIterator<Item = impl Into<String>>,
158 ) -> Self {
159 self.options = values.into_iter().map(SelectOption::simple).collect();
160 self
161 }
162
163 #[must_use]
165 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
166 self.placeholder = text.into();
167 self
168 }
169
170 #[must_use]
172 pub fn selected(mut self, index: Option<usize>) -> Self {
173 self.selected = index.filter(|&i| i < self.options.len());
174 self
175 }
176
177 #[must_use]
179 pub fn selected_value(mut self, value: &str) -> Self {
180 self.selected = self.options.iter().position(|o| o.value == value);
181 self
182 }
183
184 #[must_use]
186 pub const fn disabled(mut self, disabled: bool) -> Self {
187 self.disabled = disabled;
188 self
189 }
190
191 #[must_use]
193 pub fn min_width(mut self, width: f32) -> Self {
194 self.min_width = width.max(50.0);
195 self
196 }
197
198 #[must_use]
200 pub fn item_height(mut self, height: f32) -> Self {
201 self.item_height = height.max(20.0);
202 self
203 }
204
205 #[must_use]
207 pub fn max_visible_items(mut self, count: usize) -> Self {
208 self.max_visible_items = count.max(1);
209 self
210 }
211
212 #[must_use]
214 pub const fn background_color(mut self, color: Color) -> Self {
215 self.background_color = color;
216 self
217 }
218
219 #[must_use]
221 pub const fn border_color(mut self, color: Color) -> Self {
222 self.border_color = color;
223 self
224 }
225
226 #[must_use]
228 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
229 self.test_id_value = Some(id.into());
230 self
231 }
232
233 #[must_use]
235 pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
236 self.accessible_name_value = Some(name.into());
237 self
238 }
239
240 #[must_use]
242 pub const fn get_selected(&self) -> Option<usize> {
243 self.selected
244 }
245
246 #[must_use]
248 pub fn get_selected_value(&self) -> Option<&str> {
249 self.selected.map(|i| self.options[i].value.as_str())
250 }
251
252 #[must_use]
254 pub fn get_selected_label(&self) -> Option<&str> {
255 self.selected.map(|i| self.options[i].label.as_str())
256 }
257
258 #[must_use]
260 pub fn get_options(&self) -> &[SelectOption] {
261 &self.options
262 }
263
264 #[must_use]
266 pub const fn is_open(&self) -> bool {
267 self.open
268 }
269
270 #[must_use]
272 pub fn is_empty(&self) -> bool {
273 self.options.is_empty()
274 }
275
276 #[must_use]
278 pub fn option_count(&self) -> usize {
279 self.options.len()
280 }
281
282 fn dropdown_height(&self) -> f32 {
284 let visible = self.options.len().min(self.max_visible_items);
285 visible as f32 * self.item_height
286 }
287
288 fn item_rect(&self, index: usize) -> Rect {
290 let y = (index as f32).mul_add(self.item_height, self.bounds.y + self.item_height);
291 Rect::new(self.bounds.x, y, self.bounds.width, self.item_height)
292 }
293
294 fn item_at_position(&self, y: f32) -> Option<usize> {
296 if !self.open {
297 return None;
298 }
299
300 let dropdown_top = self.bounds.y + self.item_height;
301 if y < dropdown_top {
302 return None;
303 }
304
305 let relative_y = y - dropdown_top;
306 let index = (relative_y / self.item_height) as usize;
307
308 if index < self.options.len() && index < self.max_visible_items {
309 Some(index)
310 } else {
311 None
312 }
313 }
314}
315
316impl Widget for Select {
317 fn type_id(&self) -> TypeId {
318 TypeId::of::<Self>()
319 }
320
321 fn measure(&self, constraints: Constraints) -> Size {
322 let width = self.min_width;
323 let height = self.item_height;
324 constraints.constrain(Size::new(width, height))
325 }
326
327 fn layout(&mut self, bounds: Rect) -> LayoutResult {
328 self.bounds = bounds;
329 LayoutResult {
330 size: bounds.size(),
331 }
332 }
333
334 fn paint(&self, canvas: &mut dyn Canvas) {
335 let header_rect = Rect::new(
337 self.bounds.x,
338 self.bounds.y,
339 self.bounds.width,
340 self.item_height,
341 );
342
343 let bg_color = if self.disabled {
344 self.disabled_color
345 } else {
346 self.background_color
347 };
348
349 canvas.fill_rect(header_rect, bg_color);
350 canvas.stroke_rect(header_rect, self.border_color, 1.0);
351
352 let text = self.get_selected_label().unwrap_or(&self.placeholder);
354 let text_color = if self.disabled {
355 self.disabled_color
356 } else if self.selected.is_some() {
357 self.text_color
358 } else {
359 self.placeholder_color
360 };
361
362 let text_style = presentar_core::widget::TextStyle {
363 color: text_color,
364 ..Default::default()
365 };
366 let text_pos = presentar_core::Point::new(
367 self.bounds.x + 8.0,
368 self.bounds.y + (self.item_height - 16.0) / 2.0,
369 );
370 canvas.draw_text(text, text_pos, &text_style);
371
372 let arrow_x = self.bounds.x + self.bounds.width - 20.0;
374 let arrow_y = self.bounds.y + self.item_height / 2.0;
375 let arrow_rect = Rect::new(arrow_x, arrow_y - 3.0, 8.0, 6.0);
376 canvas.fill_rect(arrow_rect, self.text_color);
377
378 if self.open && !self.options.is_empty() {
380 let dropdown_rect = Rect::new(
381 self.bounds.x,
382 self.bounds.y + self.item_height,
383 self.bounds.width,
384 self.dropdown_height(),
385 );
386
387 canvas.fill_rect(dropdown_rect, self.background_color);
388 canvas.stroke_rect(dropdown_rect, self.border_color, 1.0);
389
390 for (i, opt) in self.options.iter().take(self.max_visible_items).enumerate() {
392 let item_rect = self.item_rect(i);
393
394 let item_bg = if Some(i) == self.selected {
396 self.selected_bg_color
397 } else if Some(i) == self.hovered_item {
398 self.hover_bg_color
399 } else {
400 self.background_color
401 };
402 canvas.fill_rect(item_rect, item_bg);
403
404 let item_color = if opt.disabled {
406 self.disabled_color
407 } else {
408 self.text_color
409 };
410 let item_style = presentar_core::widget::TextStyle {
411 color: item_color,
412 ..Default::default()
413 };
414 let item_pos = presentar_core::Point::new(
415 item_rect.x + 8.0,
416 item_rect.y + (self.item_height - 16.0) / 2.0,
417 );
418 canvas.draw_text(&opt.label, item_pos, &item_style);
419 }
420 }
421 }
422
423 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
424 if self.disabled {
425 return None;
426 }
427
428 match event {
429 Event::MouseMove { position } => {
430 if self.open {
431 self.hovered_item = self.item_at_position(position.y);
432 }
433 }
434 Event::MouseDown {
435 position,
436 button: MouseButton::Left,
437 } => {
438 let header_rect = Rect::new(
439 self.bounds.x,
440 self.bounds.y,
441 self.bounds.width,
442 self.item_height,
443 );
444
445 if header_rect.contains_point(position) {
446 self.open = !self.open;
448 self.hovered_item = None;
449 } else if self.open {
450 if let Some(index) = self.item_at_position(position.y) {
452 let opt = &self.options[index];
453 if !opt.disabled {
454 self.selected = Some(index);
455 self.open = false;
456 return Some(Box::new(SelectionChanged {
457 value: Some(opt.value.clone()),
458 index: Some(index),
459 }));
460 }
461 } else {
462 self.open = false;
464 }
465 }
466 }
467 Event::FocusOut => {
468 self.open = false;
469 }
470 _ => {}
471 }
472
473 None
474 }
475
476 fn children(&self) -> &[Box<dyn Widget>] {
477 &[]
478 }
479
480 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
481 &mut []
482 }
483
484 fn is_interactive(&self) -> bool {
485 !self.disabled
486 }
487
488 fn is_focusable(&self) -> bool {
489 !self.disabled
490 }
491
492 fn accessible_name(&self) -> Option<&str> {
493 self.accessible_name_value.as_deref()
494 }
495
496 fn accessible_role(&self) -> AccessibleRole {
497 AccessibleRole::ComboBox
498 }
499
500 fn test_id(&self) -> Option<&str> {
501 self.test_id_value.as_deref()
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use presentar_core::Widget;
509
510 #[test]
515 fn test_select_option_new() {
516 let opt = SelectOption::new("val", "Label");
517 assert_eq!(opt.value, "val");
518 assert_eq!(opt.label, "Label");
519 assert!(!opt.disabled);
520 }
521
522 #[test]
523 fn test_select_option_simple() {
524 let opt = SelectOption::simple("Same");
525 assert_eq!(opt.value, "Same");
526 assert_eq!(opt.label, "Same");
527 }
528
529 #[test]
530 fn test_select_option_disabled() {
531 let opt = SelectOption::new("v", "L").disabled(true);
532 assert!(opt.disabled);
533 }
534
535 #[test]
540 fn test_selection_changed_message() {
541 let msg = SelectionChanged {
542 value: Some("test".to_string()),
543 index: Some(0),
544 };
545 assert_eq!(msg.value, Some("test".to_string()));
546 assert_eq!(msg.index, Some(0));
547 }
548
549 #[test]
550 fn test_selection_changed_none() {
551 let msg = SelectionChanged {
552 value: None,
553 index: None,
554 };
555 assert!(msg.value.is_none());
556 assert!(msg.index.is_none());
557 }
558
559 #[test]
564 fn test_select_new() {
565 let s = Select::new();
566 assert!(s.is_empty());
567 assert_eq!(s.get_selected(), None);
568 assert!(!s.is_open());
569 assert!(!s.disabled);
570 }
571
572 #[test]
573 fn test_select_default() {
574 let s = Select::default();
575 assert!(s.is_empty());
576 }
577
578 #[test]
579 fn test_select_builder() {
580 let s = Select::new()
581 .option(SelectOption::new("a", "Option A"))
582 .option(SelectOption::new("b", "Option B"))
583 .placeholder("Choose one")
584 .selected(Some(0))
585 .min_width(200.0)
586 .item_height(40.0)
587 .with_test_id("my-select")
588 .with_accessible_name("Country");
589
590 assert_eq!(s.option_count(), 2);
591 assert_eq!(s.get_selected(), Some(0));
592 assert_eq!(Widget::test_id(&s), Some("my-select"));
593 assert_eq!(s.accessible_name(), Some("Country"));
594 }
595
596 #[test]
597 fn test_select_options() {
598 let opts = vec![
599 SelectOption::simple("One"),
600 SelectOption::simple("Two"),
601 SelectOption::simple("Three"),
602 ];
603 let s = Select::new().options(opts);
604 assert_eq!(s.option_count(), 3);
605 }
606
607 #[test]
608 fn test_select_options_from_strings() {
609 let s = Select::new().options_from_strings(["Red", "Green", "Blue"]);
610 assert_eq!(s.option_count(), 3);
611 assert_eq!(s.get_options()[0].value, "Red");
612 assert_eq!(s.get_options()[0].label, "Red");
613 }
614
615 #[test]
620 fn test_select_selected_index() {
621 let s = Select::new()
622 .options_from_strings(["A", "B", "C"])
623 .selected(Some(1));
624 assert_eq!(s.get_selected(), Some(1));
625 assert_eq!(s.get_selected_value(), Some("B"));
626 assert_eq!(s.get_selected_label(), Some("B"));
627 }
628
629 #[test]
630 fn test_select_selected_value() {
631 let s = Select::new()
632 .option(SelectOption::new("val1", "Label 1"))
633 .option(SelectOption::new("val2", "Label 2"))
634 .selected_value("val2");
635 assert_eq!(s.get_selected(), Some(1));
636 }
637
638 #[test]
639 fn test_select_selected_out_of_bounds() {
640 let s = Select::new()
641 .options_from_strings(["A", "B"])
642 .selected(Some(10));
643 assert_eq!(s.get_selected(), None); }
645
646 #[test]
647 fn test_select_selected_value_not_found() {
648 let s = Select::new()
649 .options_from_strings(["A", "B"])
650 .selected_value("C");
651 assert_eq!(s.get_selected(), None);
652 }
653
654 #[test]
655 fn test_select_no_selection() {
656 let s = Select::new().options_from_strings(["A", "B"]);
657 assert_eq!(s.get_selected(), None);
658 assert_eq!(s.get_selected_value(), None);
659 assert_eq!(s.get_selected_label(), None);
660 }
661
662 #[test]
667 fn test_select_type_id() {
668 let s = Select::new();
669 assert_eq!(Widget::type_id(&s), TypeId::of::<Select>());
670 }
671
672 #[test]
673 fn test_select_measure() {
674 let s = Select::new().min_width(150.0).item_height(32.0);
675 let size = s.measure(Constraints::loose(Size::new(400.0, 200.0)));
676 assert_eq!(size.width, 150.0);
677 assert_eq!(size.height, 32.0);
678 }
679
680 #[test]
681 fn test_select_is_interactive() {
682 let s = Select::new();
683 assert!(s.is_interactive());
684
685 let s = Select::new().disabled(true);
686 assert!(!s.is_interactive());
687 }
688
689 #[test]
690 fn test_select_is_focusable() {
691 let s = Select::new();
692 assert!(s.is_focusable());
693
694 let s = Select::new().disabled(true);
695 assert!(!s.is_focusable());
696 }
697
698 #[test]
699 fn test_select_accessible_role() {
700 let s = Select::new();
701 assert_eq!(s.accessible_role(), AccessibleRole::ComboBox);
702 }
703
704 #[test]
705 fn test_select_children() {
706 let s = Select::new();
707 assert!(s.children().is_empty());
708 }
709
710 #[test]
715 fn test_select_layout() {
716 let mut s = Select::new();
717 let bounds = Rect::new(10.0, 20.0, 200.0, 32.0);
718 let result = s.layout(bounds);
719 assert_eq!(result.size, bounds.size());
720 assert_eq!(s.bounds, bounds);
721 }
722
723 #[test]
728 fn test_select_min_width_min() {
729 let s = Select::new().min_width(10.0);
730 assert_eq!(s.min_width, 50.0); }
732
733 #[test]
734 fn test_select_item_height_min() {
735 let s = Select::new().item_height(5.0);
736 assert_eq!(s.item_height, 20.0); }
738
739 #[test]
740 fn test_select_max_visible_items_min() {
741 let s = Select::new().max_visible_items(0);
742 assert_eq!(s.max_visible_items, 1); }
744
745 #[test]
750 fn test_select_colors() {
751 let s = Select::new()
752 .background_color(Color::RED)
753 .border_color(Color::GREEN);
754 assert_eq!(s.background_color, Color::RED);
755 assert_eq!(s.border_color, Color::GREEN);
756 }
757
758 #[test]
763 fn test_select_dropdown_height() {
764 let s = Select::new()
765 .options_from_strings(["A", "B", "C"])
766 .item_height(30.0)
767 .max_visible_items(10);
768 assert_eq!(s.dropdown_height(), 90.0);
770 }
771
772 #[test]
773 fn test_select_dropdown_height_limited() {
774 let s = Select::new()
775 .options_from_strings(["A", "B", "C", "D", "E"])
776 .item_height(30.0)
777 .max_visible_items(3);
778 assert_eq!(s.dropdown_height(), 90.0);
780 }
781
782 #[test]
783 fn test_select_is_empty() {
784 let s = Select::new();
785 assert!(s.is_empty());
786
787 let s = Select::new().options_from_strings(["A"]);
788 assert!(!s.is_empty());
789 }
790
791 #[test]
792 fn test_select_option_count() {
793 let s = Select::new().options_from_strings(["A", "B", "C"]);
794 assert_eq!(s.option_count(), 3);
795 }
796
797 use presentar_core::Point;
802
803 #[test]
804 fn test_select_event_click_header_opens_dropdown() {
805 let mut s = Select::new()
806 .options_from_strings(["A", "B", "C"])
807 .item_height(32.0);
808 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
809
810 assert!(!s.open);
811 let result = s.event(&Event::MouseDown {
812 position: Point::new(100.0, 16.0), button: MouseButton::Left,
814 });
815 assert!(s.open);
816 assert!(result.is_none()); }
818
819 #[test]
820 fn test_select_event_click_header_closes_dropdown() {
821 let mut s = Select::new()
822 .options_from_strings(["A", "B", "C"])
823 .item_height(32.0);
824 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
825 s.open = true;
826
827 let result = s.event(&Event::MouseDown {
828 position: Point::new(100.0, 16.0), button: MouseButton::Left,
830 });
831 assert!(!s.open);
832 assert!(result.is_none());
833 }
834
835 #[test]
836 fn test_select_event_click_item_selects() {
837 let mut s = Select::new()
838 .options_from_strings(["Apple", "Banana", "Cherry"])
839 .item_height(32.0);
840 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
841 s.open = true;
842
843 let result = s.event(&Event::MouseDown {
845 position: Point::new(100.0, 80.0),
846 button: MouseButton::Left,
847 });
848
849 assert!(!s.open); assert_eq!(s.get_selected(), Some(1));
851 assert!(result.is_some());
852
853 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
854 assert_eq!(msg.value, Some("Banana".to_string()));
855 assert_eq!(msg.index, Some(1));
856 }
857
858 #[test]
859 fn test_select_event_click_first_item() {
860 let mut s = Select::new()
861 .options_from_strings(["First", "Second", "Third"])
862 .item_height(32.0);
863 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
864 s.open = true;
865
866 let result = s.event(&Event::MouseDown {
868 position: Point::new(100.0, 48.0),
869 button: MouseButton::Left,
870 });
871
872 assert_eq!(s.get_selected(), Some(0));
873 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
874 assert_eq!(msg.value, Some("First".to_string()));
875 assert_eq!(msg.index, Some(0));
876 }
877
878 #[test]
879 fn test_select_event_click_disabled_item_no_select() {
880 let mut s = Select::new()
881 .option(SelectOption::simple("Enabled"))
882 .option(SelectOption::simple("Disabled").disabled(true))
883 .item_height(32.0);
884 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
885 s.open = true;
886
887 let result = s.event(&Event::MouseDown {
889 position: Point::new(100.0, 80.0),
890 button: MouseButton::Left,
891 });
892
893 assert!(s.open); assert!(s.get_selected().is_none());
895 assert!(result.is_none());
896 }
897
898 #[test]
899 fn test_select_event_click_outside_closes() {
900 let mut s = Select::new()
901 .options_from_strings(["A", "B", "C"])
902 .item_height(32.0);
903 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
904 s.open = true;
905
906 let result = s.event(&Event::MouseDown {
908 position: Point::new(100.0, 500.0),
909 button: MouseButton::Left,
910 });
911
912 assert!(!s.open);
913 assert!(result.is_none());
914 }
915
916 #[test]
917 fn test_select_event_mouse_move_updates_hover() {
918 let mut s = Select::new()
919 .options_from_strings(["A", "B", "C"])
920 .item_height(32.0);
921 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
922 s.open = true;
923
924 assert!(s.hovered_item.is_none());
925
926 s.event(&Event::MouseMove {
928 position: Point::new(100.0, 80.0),
929 });
930 assert_eq!(s.hovered_item, Some(1));
931
932 s.event(&Event::MouseMove {
934 position: Point::new(100.0, 48.0),
935 });
936 assert_eq!(s.hovered_item, Some(0));
937 }
938
939 #[test]
940 fn test_select_event_mouse_move_when_closed_no_hover() {
941 let mut s = Select::new()
942 .options_from_strings(["A", "B", "C"])
943 .item_height(32.0);
944 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
945 s.event(&Event::MouseMove {
948 position: Point::new(100.0, 80.0),
949 });
950 assert!(s.hovered_item.is_none());
951 }
952
953 #[test]
954 fn test_select_event_focus_out_closes() {
955 let mut s = Select::new()
956 .options_from_strings(["A", "B", "C"])
957 .item_height(32.0);
958 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
959 s.open = true;
960
961 let result = s.event(&Event::FocusOut);
962 assert!(!s.open);
963 assert!(result.is_none());
964 }
965
966 #[test]
967 fn test_select_event_disabled_blocks_click() {
968 let mut s = Select::new()
969 .options_from_strings(["A", "B", "C"])
970 .item_height(32.0)
971 .disabled(true);
972 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
973
974 let result = s.event(&Event::MouseDown {
975 position: Point::new(100.0, 16.0),
976 button: MouseButton::Left,
977 });
978
979 assert!(!s.open);
980 assert!(result.is_none());
981 }
982
983 #[test]
984 fn test_select_event_disabled_blocks_mouse_move() {
985 let mut s = Select::new()
986 .options_from_strings(["A", "B", "C"])
987 .item_height(32.0)
988 .disabled(true);
989 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
990 s.open = true; s.event(&Event::MouseMove {
993 position: Point::new(100.0, 80.0),
994 });
995 assert!(s.hovered_item.is_none());
996 }
997
998 #[test]
999 fn test_select_event_right_click_no_effect() {
1000 let mut s = Select::new()
1001 .options_from_strings(["A", "B", "C"])
1002 .item_height(32.0);
1003 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1004
1005 let result = s.event(&Event::MouseDown {
1006 position: Point::new(100.0, 16.0),
1007 button: MouseButton::Right,
1008 });
1009
1010 assert!(!s.open);
1011 assert!(result.is_none());
1012 }
1013
1014 #[test]
1015 fn test_select_event_click_header_clears_hover() {
1016 let mut s = Select::new()
1017 .options_from_strings(["A", "B", "C"])
1018 .item_height(32.0);
1019 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1020 s.hovered_item = Some(1);
1021
1022 s.event(&Event::MouseDown {
1024 position: Point::new(100.0, 16.0),
1025 button: MouseButton::Left,
1026 });
1027
1028 assert!(s.open);
1029 assert!(s.hovered_item.is_none()); }
1031
1032 #[test]
1033 fn test_select_event_full_interaction_flow() {
1034 let mut s = Select::new()
1035 .options_from_strings(["Red", "Green", "Blue"])
1036 .item_height(32.0);
1037 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1038
1039 s.event(&Event::MouseDown {
1041 position: Point::new(100.0, 16.0),
1042 button: MouseButton::Left,
1043 });
1044 assert!(s.open);
1045 assert!(s.selected.is_none());
1046
1047 s.event(&Event::MouseMove {
1049 position: Point::new(100.0, 48.0), });
1051 assert_eq!(s.hovered_item, Some(0));
1052
1053 s.event(&Event::MouseMove {
1054 position: Point::new(100.0, 112.0), });
1056 assert_eq!(s.hovered_item, Some(2));
1057
1058 let result = s.event(&Event::MouseDown {
1060 position: Point::new(100.0, 112.0),
1061 button: MouseButton::Left,
1062 });
1063 assert!(!s.open);
1064 assert_eq!(s.get_selected(), Some(2));
1065 assert_eq!(s.get_selected_value(), Some("Blue"));
1066
1067 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
1068 assert_eq!(msg.value, Some("Blue".to_string()));
1069
1070 s.event(&Event::MouseDown {
1072 position: Point::new(100.0, 16.0),
1073 button: MouseButton::Left,
1074 });
1075 assert!(s.open);
1076
1077 let result = s.event(&Event::MouseDown {
1079 position: Point::new(100.0, 48.0),
1080 button: MouseButton::Left,
1081 });
1082 assert_eq!(s.get_selected_value(), Some("Red"));
1083
1084 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
1085 assert_eq!(msg.value, Some("Red".to_string()));
1086 assert_eq!(msg.index, Some(0));
1087 }
1088
1089 #[test]
1090 fn test_select_event_item_at_position_edge_cases() {
1091 let mut s = Select::new()
1092 .options_from_strings(["A", "B"])
1093 .item_height(32.0)
1094 .max_visible_items(2);
1095 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1096 s.open = true;
1097
1098 assert_eq!(s.item_at_position(32.0), Some(0));
1100 assert_eq!(s.item_at_position(64.0), Some(1));
1102 assert_eq!(s.item_at_position(96.0), None);
1104 assert_eq!(s.item_at_position(16.0), None);
1106 }
1107
1108 #[test]
1109 fn test_select_event_item_rect_positions() {
1110 let mut s = Select::new()
1111 .options_from_strings(["A", "B", "C"])
1112 .item_height(30.0);
1113 s.layout(Rect::new(10.0, 20.0, 200.0, 30.0));
1114
1115 let rect0 = s.item_rect(0);
1117 assert_eq!(rect0.x, 10.0);
1118 assert_eq!(rect0.y, 50.0);
1119 assert_eq!(rect0.height, 30.0);
1120
1121 let rect1 = s.item_rect(1);
1123 assert_eq!(rect1.y, 80.0);
1124 }
1125
1126 #[test]
1127 fn test_select_event_with_offset_bounds() {
1128 let mut s = Select::new()
1129 .options_from_strings(["X", "Y", "Z"])
1130 .item_height(32.0);
1131 s.layout(Rect::new(100.0, 50.0, 200.0, 32.0));
1132 s.open = true;
1133
1134 let result = s.event(&Event::MouseDown {
1136 position: Point::new(200.0, 98.0), button: MouseButton::Left,
1138 });
1139
1140 assert_eq!(s.get_selected(), Some(0));
1141 assert!(result.is_some());
1142 }
1143}