1use presentar_core::{
4 widget::{AccessibleRole, Brick, BrickAssertion, BrickBudget, BrickVerification, LayoutResult},
5 Canvas, Color, Constraints, Event, MouseButton, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::time::Duration;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct SelectOption {
14 pub value: String,
16 pub label: String,
18 pub disabled: bool,
20}
21
22impl SelectOption {
23 #[must_use]
25 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
26 Self {
27 value: value.into(),
28 label: label.into(),
29 disabled: false,
30 }
31 }
32
33 #[must_use]
35 pub fn simple(text: impl Into<String>) -> Self {
36 let text = text.into();
37 Self {
38 value: text.clone(),
39 label: text,
40 disabled: false,
41 }
42 }
43
44 #[must_use]
46 pub const fn disabled(mut self, disabled: bool) -> Self {
47 self.disabled = disabled;
48 self
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct SelectionChanged {
55 pub value: Option<String>,
57 pub index: Option<usize>,
59}
60
61#[derive(Serialize, Deserialize)]
63pub struct Select {
64 options: Vec<SelectOption>,
66 selected: Option<usize>,
68 placeholder: String,
70 #[serde(skip)]
72 open: bool,
73 disabled: bool,
75 min_width: f32,
77 item_height: f32,
79 max_visible_items: usize,
81 background_color: Color,
83 border_color: Color,
85 selected_bg_color: Color,
87 hover_bg_color: Color,
89 text_color: Color,
91 placeholder_color: Color,
93 disabled_color: Color,
95 test_id_value: Option<String>,
97 accessible_name_value: Option<String>,
99 #[serde(skip)]
101 bounds: Rect,
102 #[serde(skip)]
104 hovered_item: Option<usize>,
105}
106
107impl Default for Select {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl Select {
114 #[must_use]
116 pub fn new() -> Self {
117 Self {
118 options: Vec::new(),
119 selected: None,
120 placeholder: "Select...".to_string(),
121 open: false,
122 disabled: false,
123 min_width: 150.0,
124 item_height: 32.0,
125 max_visible_items: 8,
126 background_color: Color::WHITE,
127 border_color: Color::new(0.8, 0.8, 0.8, 1.0),
128 selected_bg_color: Color::new(0.9, 0.95, 1.0, 1.0),
129 hover_bg_color: Color::new(0.95, 0.95, 0.95, 1.0),
130 text_color: Color::BLACK,
131 placeholder_color: Color::new(0.6, 0.6, 0.6, 1.0),
132 disabled_color: Color::new(0.7, 0.7, 0.7, 1.0),
133 test_id_value: None,
134 accessible_name_value: None,
135 bounds: Rect::default(),
136 hovered_item: None,
137 }
138 }
139
140 #[must_use]
142 pub fn option(mut self, opt: SelectOption) -> Self {
143 self.options.push(opt);
144 self
145 }
146
147 #[must_use]
149 pub fn options(mut self, opts: impl IntoIterator<Item = SelectOption>) -> Self {
150 self.options.extend(opts);
151 self
152 }
153
154 #[must_use]
156 pub fn options_from_strings(
157 mut self,
158 values: impl IntoIterator<Item = impl Into<String>>,
159 ) -> Self {
160 self.options = values.into_iter().map(SelectOption::simple).collect();
161 self
162 }
163
164 #[must_use]
166 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
167 self.placeholder = text.into();
168 self
169 }
170
171 #[must_use]
173 pub fn selected(mut self, index: Option<usize>) -> Self {
174 self.selected = index.filter(|&i| i < self.options.len());
175 self
176 }
177
178 #[must_use]
180 pub fn selected_value(mut self, value: &str) -> Self {
181 self.selected = self.options.iter().position(|o| o.value == value);
182 self
183 }
184
185 #[must_use]
187 pub const fn disabled(mut self, disabled: bool) -> Self {
188 self.disabled = disabled;
189 self
190 }
191
192 #[must_use]
194 pub fn min_width(mut self, width: f32) -> Self {
195 self.min_width = width.max(50.0);
196 self
197 }
198
199 #[must_use]
201 pub fn item_height(mut self, height: f32) -> Self {
202 self.item_height = height.max(20.0);
203 self
204 }
205
206 #[must_use]
208 pub fn max_visible_items(mut self, count: usize) -> Self {
209 self.max_visible_items = count.max(1);
210 self
211 }
212
213 #[must_use]
215 pub const fn background_color(mut self, color: Color) -> Self {
216 self.background_color = color;
217 self
218 }
219
220 #[must_use]
222 pub const fn border_color(mut self, color: Color) -> Self {
223 self.border_color = color;
224 self
225 }
226
227 #[must_use]
229 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
230 self.test_id_value = Some(id.into());
231 self
232 }
233
234 #[must_use]
236 pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
237 self.accessible_name_value = Some(name.into());
238 self
239 }
240
241 #[must_use]
243 pub const fn get_selected(&self) -> Option<usize> {
244 self.selected
245 }
246
247 #[must_use]
249 pub fn get_selected_value(&self) -> Option<&str> {
250 self.selected.map(|i| self.options[i].value.as_str())
251 }
252
253 #[must_use]
255 pub fn get_selected_label(&self) -> Option<&str> {
256 self.selected.map(|i| self.options[i].label.as_str())
257 }
258
259 #[must_use]
261 pub fn get_options(&self) -> &[SelectOption] {
262 &self.options
263 }
264
265 #[must_use]
267 pub const fn is_open(&self) -> bool {
268 self.open
269 }
270
271 #[must_use]
273 pub fn is_empty(&self) -> bool {
274 self.options.is_empty()
275 }
276
277 #[must_use]
279 pub fn option_count(&self) -> usize {
280 self.options.len()
281 }
282
283 fn dropdown_height(&self) -> f32 {
285 let visible = self.options.len().min(self.max_visible_items);
286 visible as f32 * self.item_height
287 }
288
289 fn item_rect(&self, index: usize) -> Rect {
291 let y = (index as f32).mul_add(self.item_height, self.bounds.y + self.item_height);
292 Rect::new(self.bounds.x, y, self.bounds.width, self.item_height)
293 }
294
295 fn item_at_position(&self, y: f32) -> Option<usize> {
297 if !self.open {
298 return None;
299 }
300
301 let dropdown_top = self.bounds.y + self.item_height;
302 if y < dropdown_top {
303 return None;
304 }
305
306 let relative_y = y - dropdown_top;
307 let index = (relative_y / self.item_height) as usize;
308
309 if index < self.options.len() && index < self.max_visible_items {
310 Some(index)
311 } else {
312 None
313 }
314 }
315}
316
317impl Widget for Select {
318 fn type_id(&self) -> TypeId {
319 TypeId::of::<Self>()
320 }
321
322 fn measure(&self, constraints: Constraints) -> Size {
323 let width = self.min_width;
324 let height = self.item_height;
325 constraints.constrain(Size::new(width, height))
326 }
327
328 fn layout(&mut self, bounds: Rect) -> LayoutResult {
329 self.bounds = bounds;
330 LayoutResult {
331 size: bounds.size(),
332 }
333 }
334
335 fn paint(&self, canvas: &mut dyn Canvas) {
336 let header_rect = Rect::new(
338 self.bounds.x,
339 self.bounds.y,
340 self.bounds.width,
341 self.item_height,
342 );
343
344 let bg_color = if self.disabled {
345 self.disabled_color
346 } else {
347 self.background_color
348 };
349
350 canvas.fill_rect(header_rect, bg_color);
351 canvas.stroke_rect(header_rect, self.border_color, 1.0);
352
353 let text = self.get_selected_label().unwrap_or(&self.placeholder);
355 let text_color = if self.disabled {
356 self.disabled_color
357 } else if self.selected.is_some() {
358 self.text_color
359 } else {
360 self.placeholder_color
361 };
362
363 let text_style = presentar_core::widget::TextStyle {
364 color: text_color,
365 ..Default::default()
366 };
367 let text_pos = presentar_core::Point::new(
368 self.bounds.x + 8.0,
369 self.bounds.y + (self.item_height - 16.0) / 2.0,
370 );
371 canvas.draw_text(text, text_pos, &text_style);
372
373 let arrow_x = self.bounds.x + self.bounds.width - 20.0;
375 let arrow_y = self.bounds.y + self.item_height / 2.0;
376 let arrow_rect = Rect::new(arrow_x, arrow_y - 3.0, 8.0, 6.0);
377 canvas.fill_rect(arrow_rect, self.text_color);
378
379 if self.open && !self.options.is_empty() {
381 let dropdown_rect = Rect::new(
382 self.bounds.x,
383 self.bounds.y + self.item_height,
384 self.bounds.width,
385 self.dropdown_height(),
386 );
387
388 canvas.fill_rect(dropdown_rect, self.background_color);
389 canvas.stroke_rect(dropdown_rect, self.border_color, 1.0);
390
391 for (i, opt) in self.options.iter().take(self.max_visible_items).enumerate() {
393 let item_rect = self.item_rect(i);
394
395 let item_bg = if Some(i) == self.selected {
397 self.selected_bg_color
398 } else if Some(i) == self.hovered_item {
399 self.hover_bg_color
400 } else {
401 self.background_color
402 };
403 canvas.fill_rect(item_rect, item_bg);
404
405 let item_color = if opt.disabled {
407 self.disabled_color
408 } else {
409 self.text_color
410 };
411 let item_style = presentar_core::widget::TextStyle {
412 color: item_color,
413 ..Default::default()
414 };
415 let item_pos = presentar_core::Point::new(
416 item_rect.x + 8.0,
417 item_rect.y + (self.item_height - 16.0) / 2.0,
418 );
419 canvas.draw_text(&opt.label, item_pos, &item_style);
420 }
421 }
422 }
423
424 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
425 if self.disabled {
426 return None;
427 }
428
429 match event {
430 Event::MouseMove { position } => {
431 if self.open {
432 self.hovered_item = self.item_at_position(position.y);
433 }
434 }
435 Event::MouseDown {
436 position,
437 button: MouseButton::Left,
438 } => {
439 let header_rect = Rect::new(
440 self.bounds.x,
441 self.bounds.y,
442 self.bounds.width,
443 self.item_height,
444 );
445
446 if header_rect.contains_point(position) {
447 self.open = !self.open;
449 self.hovered_item = None;
450 } else if self.open {
451 if let Some(index) = self.item_at_position(position.y) {
453 let opt = &self.options[index];
454 if !opt.disabled {
455 self.selected = Some(index);
456 self.open = false;
457 return Some(Box::new(SelectionChanged {
458 value: Some(opt.value.clone()),
459 index: Some(index),
460 }));
461 }
462 } else {
463 self.open = false;
465 }
466 }
467 }
468 Event::FocusOut => {
469 self.open = false;
470 }
471 _ => {}
472 }
473
474 None
475 }
476
477 fn children(&self) -> &[Box<dyn Widget>] {
478 &[]
479 }
480
481 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
482 &mut []
483 }
484
485 fn is_interactive(&self) -> bool {
486 !self.disabled
487 }
488
489 fn is_focusable(&self) -> bool {
490 !self.disabled
491 }
492
493 fn accessible_name(&self) -> Option<&str> {
494 self.accessible_name_value.as_deref()
495 }
496
497 fn accessible_role(&self) -> AccessibleRole {
498 AccessibleRole::ComboBox
499 }
500
501 fn test_id(&self) -> Option<&str> {
502 self.test_id_value.as_deref()
503 }
504}
505
506impl Brick for Select {
508 fn brick_name(&self) -> &'static str {
509 "Select"
510 }
511
512 fn assertions(&self) -> &[BrickAssertion] {
513 &[BrickAssertion::MaxLatencyMs(16)]
514 }
515
516 fn budget(&self) -> BrickBudget {
517 BrickBudget::uniform(16)
518 }
519
520 fn verify(&self) -> BrickVerification {
521 BrickVerification {
522 passed: self.assertions().to_vec(),
523 failed: vec![],
524 verification_time: Duration::from_micros(10),
525 }
526 }
527
528 fn to_html(&self) -> String {
529 r#"<div class="brick-select"></div>"#.to_string()
530 }
531
532 fn to_css(&self) -> String {
533 ".brick-select { display: block; }".to_string()
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use presentar_core::Widget;
541
542 #[test]
547 fn test_select_option_new() {
548 let opt = SelectOption::new("val", "Label");
549 assert_eq!(opt.value, "val");
550 assert_eq!(opt.label, "Label");
551 assert!(!opt.disabled);
552 }
553
554 #[test]
555 fn test_select_option_simple() {
556 let opt = SelectOption::simple("Same");
557 assert_eq!(opt.value, "Same");
558 assert_eq!(opt.label, "Same");
559 }
560
561 #[test]
562 fn test_select_option_disabled() {
563 let opt = SelectOption::new("v", "L").disabled(true);
564 assert!(opt.disabled);
565 }
566
567 #[test]
572 fn test_selection_changed_message() {
573 let msg = SelectionChanged {
574 value: Some("test".to_string()),
575 index: Some(0),
576 };
577 assert_eq!(msg.value, Some("test".to_string()));
578 assert_eq!(msg.index, Some(0));
579 }
580
581 #[test]
582 fn test_selection_changed_none() {
583 let msg = SelectionChanged {
584 value: None,
585 index: None,
586 };
587 assert!(msg.value.is_none());
588 assert!(msg.index.is_none());
589 }
590
591 #[test]
596 fn test_select_new() {
597 let s = Select::new();
598 assert!(s.is_empty());
599 assert_eq!(s.get_selected(), None);
600 assert!(!s.is_open());
601 assert!(!s.disabled);
602 }
603
604 #[test]
605 fn test_select_default() {
606 let s = Select::default();
607 assert!(s.is_empty());
608 }
609
610 #[test]
611 fn test_select_builder() {
612 let s = Select::new()
613 .option(SelectOption::new("a", "Option A"))
614 .option(SelectOption::new("b", "Option B"))
615 .placeholder("Choose one")
616 .selected(Some(0))
617 .min_width(200.0)
618 .item_height(40.0)
619 .with_test_id("my-select")
620 .with_accessible_name("Country");
621
622 assert_eq!(s.option_count(), 2);
623 assert_eq!(s.get_selected(), Some(0));
624 assert_eq!(Widget::test_id(&s), Some("my-select"));
625 assert_eq!(s.accessible_name(), Some("Country"));
626 }
627
628 #[test]
629 fn test_select_options() {
630 let opts = vec![
631 SelectOption::simple("One"),
632 SelectOption::simple("Two"),
633 SelectOption::simple("Three"),
634 ];
635 let s = Select::new().options(opts);
636 assert_eq!(s.option_count(), 3);
637 }
638
639 #[test]
640 fn test_select_options_from_strings() {
641 let s = Select::new().options_from_strings(["Red", "Green", "Blue"]);
642 assert_eq!(s.option_count(), 3);
643 assert_eq!(s.get_options()[0].value, "Red");
644 assert_eq!(s.get_options()[0].label, "Red");
645 }
646
647 #[test]
652 fn test_select_selected_index() {
653 let s = Select::new()
654 .options_from_strings(["A", "B", "C"])
655 .selected(Some(1));
656 assert_eq!(s.get_selected(), Some(1));
657 assert_eq!(s.get_selected_value(), Some("B"));
658 assert_eq!(s.get_selected_label(), Some("B"));
659 }
660
661 #[test]
662 fn test_select_selected_value() {
663 let s = Select::new()
664 .option(SelectOption::new("val1", "Label 1"))
665 .option(SelectOption::new("val2", "Label 2"))
666 .selected_value("val2");
667 assert_eq!(s.get_selected(), Some(1));
668 }
669
670 #[test]
671 fn test_select_selected_out_of_bounds() {
672 let s = Select::new()
673 .options_from_strings(["A", "B"])
674 .selected(Some(10));
675 assert_eq!(s.get_selected(), None); }
677
678 #[test]
679 fn test_select_selected_value_not_found() {
680 let s = Select::new()
681 .options_from_strings(["A", "B"])
682 .selected_value("C");
683 assert_eq!(s.get_selected(), None);
684 }
685
686 #[test]
687 fn test_select_no_selection() {
688 let s = Select::new().options_from_strings(["A", "B"]);
689 assert_eq!(s.get_selected(), None);
690 assert_eq!(s.get_selected_value(), None);
691 assert_eq!(s.get_selected_label(), None);
692 }
693
694 #[test]
699 fn test_select_type_id() {
700 let s = Select::new();
701 assert_eq!(Widget::type_id(&s), TypeId::of::<Select>());
702 }
703
704 #[test]
705 fn test_select_measure() {
706 let s = Select::new().min_width(150.0).item_height(32.0);
707 let size = s.measure(Constraints::loose(Size::new(400.0, 200.0)));
708 assert_eq!(size.width, 150.0);
709 assert_eq!(size.height, 32.0);
710 }
711
712 #[test]
713 fn test_select_is_interactive() {
714 let s = Select::new();
715 assert!(s.is_interactive());
716
717 let s = Select::new().disabled(true);
718 assert!(!s.is_interactive());
719 }
720
721 #[test]
722 fn test_select_is_focusable() {
723 let s = Select::new();
724 assert!(s.is_focusable());
725
726 let s = Select::new().disabled(true);
727 assert!(!s.is_focusable());
728 }
729
730 #[test]
731 fn test_select_accessible_role() {
732 let s = Select::new();
733 assert_eq!(s.accessible_role(), AccessibleRole::ComboBox);
734 }
735
736 #[test]
737 fn test_select_children() {
738 let s = Select::new();
739 assert!(s.children().is_empty());
740 }
741
742 #[test]
747 fn test_select_layout() {
748 let mut s = Select::new();
749 let bounds = Rect::new(10.0, 20.0, 200.0, 32.0);
750 let result = s.layout(bounds);
751 assert_eq!(result.size, bounds.size());
752 assert_eq!(s.bounds, bounds);
753 }
754
755 #[test]
760 fn test_select_min_width_min() {
761 let s = Select::new().min_width(10.0);
762 assert_eq!(s.min_width, 50.0); }
764
765 #[test]
766 fn test_select_item_height_min() {
767 let s = Select::new().item_height(5.0);
768 assert_eq!(s.item_height, 20.0); }
770
771 #[test]
772 fn test_select_max_visible_items_min() {
773 let s = Select::new().max_visible_items(0);
774 assert_eq!(s.max_visible_items, 1); }
776
777 #[test]
782 fn test_select_colors() {
783 let s = Select::new()
784 .background_color(Color::RED)
785 .border_color(Color::GREEN);
786 assert_eq!(s.background_color, Color::RED);
787 assert_eq!(s.border_color, Color::GREEN);
788 }
789
790 #[test]
795 fn test_select_dropdown_height() {
796 let s = Select::new()
797 .options_from_strings(["A", "B", "C"])
798 .item_height(30.0)
799 .max_visible_items(10);
800 assert_eq!(s.dropdown_height(), 90.0);
802 }
803
804 #[test]
805 fn test_select_dropdown_height_limited() {
806 let s = Select::new()
807 .options_from_strings(["A", "B", "C", "D", "E"])
808 .item_height(30.0)
809 .max_visible_items(3);
810 assert_eq!(s.dropdown_height(), 90.0);
812 }
813
814 #[test]
815 fn test_select_is_empty() {
816 let s = Select::new();
817 assert!(s.is_empty());
818
819 let s = Select::new().options_from_strings(["A"]);
820 assert!(!s.is_empty());
821 }
822
823 #[test]
824 fn test_select_option_count() {
825 let s = Select::new().options_from_strings(["A", "B", "C"]);
826 assert_eq!(s.option_count(), 3);
827 }
828
829 use presentar_core::Point;
834
835 #[test]
836 fn test_select_event_click_header_opens_dropdown() {
837 let mut s = Select::new()
838 .options_from_strings(["A", "B", "C"])
839 .item_height(32.0);
840 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
841
842 assert!(!s.open);
843 let result = s.event(&Event::MouseDown {
844 position: Point::new(100.0, 16.0), button: MouseButton::Left,
846 });
847 assert!(s.open);
848 assert!(result.is_none()); }
850
851 #[test]
852 fn test_select_event_click_header_closes_dropdown() {
853 let mut s = Select::new()
854 .options_from_strings(["A", "B", "C"])
855 .item_height(32.0);
856 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
857 s.open = true;
858
859 let result = s.event(&Event::MouseDown {
860 position: Point::new(100.0, 16.0), button: MouseButton::Left,
862 });
863 assert!(!s.open);
864 assert!(result.is_none());
865 }
866
867 #[test]
868 fn test_select_event_click_item_selects() {
869 let mut s = Select::new()
870 .options_from_strings(["Apple", "Banana", "Cherry"])
871 .item_height(32.0);
872 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
873 s.open = true;
874
875 let result = s.event(&Event::MouseDown {
877 position: Point::new(100.0, 80.0),
878 button: MouseButton::Left,
879 });
880
881 assert!(!s.open); assert_eq!(s.get_selected(), Some(1));
883 assert!(result.is_some());
884
885 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
886 assert_eq!(msg.value, Some("Banana".to_string()));
887 assert_eq!(msg.index, Some(1));
888 }
889
890 #[test]
891 fn test_select_event_click_first_item() {
892 let mut s = Select::new()
893 .options_from_strings(["First", "Second", "Third"])
894 .item_height(32.0);
895 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
896 s.open = true;
897
898 let result = s.event(&Event::MouseDown {
900 position: Point::new(100.0, 48.0),
901 button: MouseButton::Left,
902 });
903
904 assert_eq!(s.get_selected(), Some(0));
905 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
906 assert_eq!(msg.value, Some("First".to_string()));
907 assert_eq!(msg.index, Some(0));
908 }
909
910 #[test]
911 fn test_select_event_click_disabled_item_no_select() {
912 let mut s = Select::new()
913 .option(SelectOption::simple("Enabled"))
914 .option(SelectOption::simple("Disabled").disabled(true))
915 .item_height(32.0);
916 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
917 s.open = true;
918
919 let result = s.event(&Event::MouseDown {
921 position: Point::new(100.0, 80.0),
922 button: MouseButton::Left,
923 });
924
925 assert!(s.open); assert!(s.get_selected().is_none());
927 assert!(result.is_none());
928 }
929
930 #[test]
931 fn test_select_event_click_outside_closes() {
932 let mut s = Select::new()
933 .options_from_strings(["A", "B", "C"])
934 .item_height(32.0);
935 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
936 s.open = true;
937
938 let result = s.event(&Event::MouseDown {
940 position: Point::new(100.0, 500.0),
941 button: MouseButton::Left,
942 });
943
944 assert!(!s.open);
945 assert!(result.is_none());
946 }
947
948 #[test]
949 fn test_select_event_mouse_move_updates_hover() {
950 let mut s = Select::new()
951 .options_from_strings(["A", "B", "C"])
952 .item_height(32.0);
953 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
954 s.open = true;
955
956 assert!(s.hovered_item.is_none());
957
958 s.event(&Event::MouseMove {
960 position: Point::new(100.0, 80.0),
961 });
962 assert_eq!(s.hovered_item, Some(1));
963
964 s.event(&Event::MouseMove {
966 position: Point::new(100.0, 48.0),
967 });
968 assert_eq!(s.hovered_item, Some(0));
969 }
970
971 #[test]
972 fn test_select_event_mouse_move_when_closed_no_hover() {
973 let mut s = Select::new()
974 .options_from_strings(["A", "B", "C"])
975 .item_height(32.0);
976 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
977 s.event(&Event::MouseMove {
980 position: Point::new(100.0, 80.0),
981 });
982 assert!(s.hovered_item.is_none());
983 }
984
985 #[test]
986 fn test_select_event_focus_out_closes() {
987 let mut s = Select::new()
988 .options_from_strings(["A", "B", "C"])
989 .item_height(32.0);
990 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
991 s.open = true;
992
993 let result = s.event(&Event::FocusOut);
994 assert!(!s.open);
995 assert!(result.is_none());
996 }
997
998 #[test]
999 fn test_select_event_disabled_blocks_click() {
1000 let mut s = Select::new()
1001 .options_from_strings(["A", "B", "C"])
1002 .item_height(32.0)
1003 .disabled(true);
1004 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1005
1006 let result = s.event(&Event::MouseDown {
1007 position: Point::new(100.0, 16.0),
1008 button: MouseButton::Left,
1009 });
1010
1011 assert!(!s.open);
1012 assert!(result.is_none());
1013 }
1014
1015 #[test]
1016 fn test_select_event_disabled_blocks_mouse_move() {
1017 let mut s = Select::new()
1018 .options_from_strings(["A", "B", "C"])
1019 .item_height(32.0)
1020 .disabled(true);
1021 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1022 s.open = true; s.event(&Event::MouseMove {
1025 position: Point::new(100.0, 80.0),
1026 });
1027 assert!(s.hovered_item.is_none());
1028 }
1029
1030 #[test]
1031 fn test_select_event_right_click_no_effect() {
1032 let mut s = Select::new()
1033 .options_from_strings(["A", "B", "C"])
1034 .item_height(32.0);
1035 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1036
1037 let result = s.event(&Event::MouseDown {
1038 position: Point::new(100.0, 16.0),
1039 button: MouseButton::Right,
1040 });
1041
1042 assert!(!s.open);
1043 assert!(result.is_none());
1044 }
1045
1046 #[test]
1047 fn test_select_event_click_header_clears_hover() {
1048 let mut s = Select::new()
1049 .options_from_strings(["A", "B", "C"])
1050 .item_height(32.0);
1051 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1052 s.hovered_item = Some(1);
1053
1054 s.event(&Event::MouseDown {
1056 position: Point::new(100.0, 16.0),
1057 button: MouseButton::Left,
1058 });
1059
1060 assert!(s.open);
1061 assert!(s.hovered_item.is_none()); }
1063
1064 #[test]
1065 fn test_select_event_full_interaction_flow() {
1066 let mut s = Select::new()
1067 .options_from_strings(["Red", "Green", "Blue"])
1068 .item_height(32.0);
1069 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1070
1071 s.event(&Event::MouseDown {
1073 position: Point::new(100.0, 16.0),
1074 button: MouseButton::Left,
1075 });
1076 assert!(s.open);
1077 assert!(s.selected.is_none());
1078
1079 s.event(&Event::MouseMove {
1081 position: Point::new(100.0, 48.0), });
1083 assert_eq!(s.hovered_item, Some(0));
1084
1085 s.event(&Event::MouseMove {
1086 position: Point::new(100.0, 112.0), });
1088 assert_eq!(s.hovered_item, Some(2));
1089
1090 let result = s.event(&Event::MouseDown {
1092 position: Point::new(100.0, 112.0),
1093 button: MouseButton::Left,
1094 });
1095 assert!(!s.open);
1096 assert_eq!(s.get_selected(), Some(2));
1097 assert_eq!(s.get_selected_value(), Some("Blue"));
1098
1099 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
1100 assert_eq!(msg.value, Some("Blue".to_string()));
1101
1102 s.event(&Event::MouseDown {
1104 position: Point::new(100.0, 16.0),
1105 button: MouseButton::Left,
1106 });
1107 assert!(s.open);
1108
1109 let result = s.event(&Event::MouseDown {
1111 position: Point::new(100.0, 48.0),
1112 button: MouseButton::Left,
1113 });
1114 assert_eq!(s.get_selected_value(), Some("Red"));
1115
1116 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
1117 assert_eq!(msg.value, Some("Red".to_string()));
1118 assert_eq!(msg.index, Some(0));
1119 }
1120
1121 #[test]
1122 fn test_select_event_item_at_position_edge_cases() {
1123 let mut s = Select::new()
1124 .options_from_strings(["A", "B"])
1125 .item_height(32.0)
1126 .max_visible_items(2);
1127 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1128 s.open = true;
1129
1130 assert_eq!(s.item_at_position(32.0), Some(0));
1132 assert_eq!(s.item_at_position(64.0), Some(1));
1134 assert_eq!(s.item_at_position(96.0), None);
1136 assert_eq!(s.item_at_position(16.0), None);
1138 }
1139
1140 #[test]
1141 fn test_select_event_item_rect_positions() {
1142 let mut s = Select::new()
1143 .options_from_strings(["A", "B", "C"])
1144 .item_height(30.0);
1145 s.layout(Rect::new(10.0, 20.0, 200.0, 30.0));
1146
1147 let rect0 = s.item_rect(0);
1149 assert_eq!(rect0.x, 10.0);
1150 assert_eq!(rect0.y, 50.0);
1151 assert_eq!(rect0.height, 30.0);
1152
1153 let rect1 = s.item_rect(1);
1155 assert_eq!(rect1.y, 80.0);
1156 }
1157
1158 #[test]
1159 fn test_select_event_with_offset_bounds() {
1160 let mut s = Select::new()
1161 .options_from_strings(["X", "Y", "Z"])
1162 .item_height(32.0);
1163 s.layout(Rect::new(100.0, 50.0, 200.0, 32.0));
1164 s.open = true;
1165
1166 let result = s.event(&Event::MouseDown {
1168 position: Point::new(200.0, 98.0), button: MouseButton::Left,
1170 });
1171
1172 assert_eq!(s.get_selected(), Some(0));
1173 assert!(result.is_some());
1174 }
1175}