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)]
538#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
539mod tests {
540 use super::*;
541 use presentar_core::Widget;
542
543 #[test]
548 fn test_select_option_new() {
549 let opt = SelectOption::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_select_option_simple() {
557 let opt = SelectOption::simple("Same");
558 assert_eq!(opt.value, "Same");
559 assert_eq!(opt.label, "Same");
560 }
561
562 #[test]
563 fn test_select_option_disabled() {
564 let opt = SelectOption::new("v", "L").disabled(true);
565 assert!(opt.disabled);
566 }
567
568 #[test]
573 fn test_selection_changed_message() {
574 let msg = SelectionChanged {
575 value: Some("test".to_string()),
576 index: Some(0),
577 };
578 assert_eq!(msg.value, Some("test".to_string()));
579 assert_eq!(msg.index, Some(0));
580 }
581
582 #[test]
583 fn test_selection_changed_none() {
584 let msg = SelectionChanged {
585 value: None,
586 index: None,
587 };
588 assert!(msg.value.is_none());
589 assert!(msg.index.is_none());
590 }
591
592 #[test]
597 fn test_select_new() {
598 let s = Select::new();
599 assert!(s.is_empty());
600 assert_eq!(s.get_selected(), None);
601 assert!(!s.is_open());
602 assert!(!s.disabled);
603 }
604
605 #[test]
606 fn test_select_default() {
607 let s = Select::default();
608 assert!(s.is_empty());
609 }
610
611 #[test]
612 fn test_select_builder() {
613 let s = Select::new()
614 .option(SelectOption::new("a", "Option A"))
615 .option(SelectOption::new("b", "Option B"))
616 .placeholder("Choose one")
617 .selected(Some(0))
618 .min_width(200.0)
619 .item_height(40.0)
620 .with_test_id("my-select")
621 .with_accessible_name("Country");
622
623 assert_eq!(s.option_count(), 2);
624 assert_eq!(s.get_selected(), Some(0));
625 assert_eq!(Widget::test_id(&s), Some("my-select"));
626 assert_eq!(s.accessible_name(), Some("Country"));
627 }
628
629 #[test]
630 fn test_select_options() {
631 let opts = vec![
632 SelectOption::simple("One"),
633 SelectOption::simple("Two"),
634 SelectOption::simple("Three"),
635 ];
636 let s = Select::new().options(opts);
637 assert_eq!(s.option_count(), 3);
638 }
639
640 #[test]
641 fn test_select_options_from_strings() {
642 let s = Select::new().options_from_strings(["Red", "Green", "Blue"]);
643 assert_eq!(s.option_count(), 3);
644 assert_eq!(s.get_options()[0].value, "Red");
645 assert_eq!(s.get_options()[0].label, "Red");
646 }
647
648 #[test]
653 fn test_select_selected_index() {
654 let s = Select::new()
655 .options_from_strings(["A", "B", "C"])
656 .selected(Some(1));
657 assert_eq!(s.get_selected(), Some(1));
658 assert_eq!(s.get_selected_value(), Some("B"));
659 assert_eq!(s.get_selected_label(), Some("B"));
660 }
661
662 #[test]
663 fn test_select_selected_value() {
664 let s = Select::new()
665 .option(SelectOption::new("val1", "Label 1"))
666 .option(SelectOption::new("val2", "Label 2"))
667 .selected_value("val2");
668 assert_eq!(s.get_selected(), Some(1));
669 }
670
671 #[test]
672 fn test_select_selected_out_of_bounds() {
673 let s = Select::new()
674 .options_from_strings(["A", "B"])
675 .selected(Some(10));
676 assert_eq!(s.get_selected(), None); }
678
679 #[test]
680 fn test_select_selected_value_not_found() {
681 let s = Select::new()
682 .options_from_strings(["A", "B"])
683 .selected_value("C");
684 assert_eq!(s.get_selected(), None);
685 }
686
687 #[test]
688 fn test_select_no_selection() {
689 let s = Select::new().options_from_strings(["A", "B"]);
690 assert_eq!(s.get_selected(), None);
691 assert_eq!(s.get_selected_value(), None);
692 assert_eq!(s.get_selected_label(), None);
693 }
694
695 #[test]
700 fn test_select_type_id() {
701 let s = Select::new();
702 assert_eq!(Widget::type_id(&s), TypeId::of::<Select>());
703 }
704
705 #[test]
706 fn test_select_measure() {
707 let s = Select::new().min_width(150.0).item_height(32.0);
708 let size = s.measure(Constraints::loose(Size::new(400.0, 200.0)));
709 assert_eq!(size.width, 150.0);
710 assert_eq!(size.height, 32.0);
711 }
712
713 #[test]
714 fn test_select_is_interactive() {
715 let s = Select::new();
716 assert!(s.is_interactive());
717
718 let s = Select::new().disabled(true);
719 assert!(!s.is_interactive());
720 }
721
722 #[test]
723 fn test_select_is_focusable() {
724 let s = Select::new();
725 assert!(s.is_focusable());
726
727 let s = Select::new().disabled(true);
728 assert!(!s.is_focusable());
729 }
730
731 #[test]
732 fn test_select_accessible_role() {
733 let s = Select::new();
734 assert_eq!(s.accessible_role(), AccessibleRole::ComboBox);
735 }
736
737 #[test]
738 fn test_select_children() {
739 let s = Select::new();
740 assert!(s.children().is_empty());
741 }
742
743 #[test]
748 fn test_select_layout() {
749 let mut s = Select::new();
750 let bounds = Rect::new(10.0, 20.0, 200.0, 32.0);
751 let result = s.layout(bounds);
752 assert_eq!(result.size, bounds.size());
753 assert_eq!(s.bounds, bounds);
754 }
755
756 #[test]
761 fn test_select_min_width_min() {
762 let s = Select::new().min_width(10.0);
763 assert_eq!(s.min_width, 50.0); }
765
766 #[test]
767 fn test_select_item_height_min() {
768 let s = Select::new().item_height(5.0);
769 assert_eq!(s.item_height, 20.0); }
771
772 #[test]
773 fn test_select_max_visible_items_min() {
774 let s = Select::new().max_visible_items(0);
775 assert_eq!(s.max_visible_items, 1); }
777
778 #[test]
783 fn test_select_colors() {
784 let s = Select::new()
785 .background_color(Color::RED)
786 .border_color(Color::GREEN);
787 assert_eq!(s.background_color, Color::RED);
788 assert_eq!(s.border_color, Color::GREEN);
789 }
790
791 #[test]
796 fn test_select_dropdown_height() {
797 let s = Select::new()
798 .options_from_strings(["A", "B", "C"])
799 .item_height(30.0)
800 .max_visible_items(10);
801 assert_eq!(s.dropdown_height(), 90.0);
803 }
804
805 #[test]
806 fn test_select_dropdown_height_limited() {
807 let s = Select::new()
808 .options_from_strings(["A", "B", "C", "D", "E"])
809 .item_height(30.0)
810 .max_visible_items(3);
811 assert_eq!(s.dropdown_height(), 90.0);
813 }
814
815 #[test]
816 fn test_select_is_empty() {
817 let s = Select::new();
818 assert!(s.is_empty());
819
820 let s = Select::new().options_from_strings(["A"]);
821 assert!(!s.is_empty());
822 }
823
824 #[test]
825 fn test_select_option_count() {
826 let s = Select::new().options_from_strings(["A", "B", "C"]);
827 assert_eq!(s.option_count(), 3);
828 }
829
830 use presentar_core::Point;
835
836 #[test]
837 fn test_select_event_click_header_opens_dropdown() {
838 let mut s = Select::new()
839 .options_from_strings(["A", "B", "C"])
840 .item_height(32.0);
841 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
842
843 assert!(!s.open);
844 let result = s.event(&Event::MouseDown {
845 position: Point::new(100.0, 16.0), button: MouseButton::Left,
847 });
848 assert!(s.open);
849 assert!(result.is_none()); }
851
852 #[test]
853 fn test_select_event_click_header_closes_dropdown() {
854 let mut s = Select::new()
855 .options_from_strings(["A", "B", "C"])
856 .item_height(32.0);
857 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
858 s.open = true;
859
860 let result = s.event(&Event::MouseDown {
861 position: Point::new(100.0, 16.0), button: MouseButton::Left,
863 });
864 assert!(!s.open);
865 assert!(result.is_none());
866 }
867
868 #[test]
869 fn test_select_event_click_item_selects() {
870 let mut s = Select::new()
871 .options_from_strings(["Apple", "Banana", "Cherry"])
872 .item_height(32.0);
873 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
874 s.open = true;
875
876 let result = s.event(&Event::MouseDown {
878 position: Point::new(100.0, 80.0),
879 button: MouseButton::Left,
880 });
881
882 assert!(!s.open); assert_eq!(s.get_selected(), Some(1));
884 assert!(result.is_some());
885
886 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
887 assert_eq!(msg.value, Some("Banana".to_string()));
888 assert_eq!(msg.index, Some(1));
889 }
890
891 #[test]
892 fn test_select_event_click_first_item() {
893 let mut s = Select::new()
894 .options_from_strings(["First", "Second", "Third"])
895 .item_height(32.0);
896 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
897 s.open = true;
898
899 let result = s.event(&Event::MouseDown {
901 position: Point::new(100.0, 48.0),
902 button: MouseButton::Left,
903 });
904
905 assert_eq!(s.get_selected(), Some(0));
906 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
907 assert_eq!(msg.value, Some("First".to_string()));
908 assert_eq!(msg.index, Some(0));
909 }
910
911 #[test]
912 fn test_select_event_click_disabled_item_no_select() {
913 let mut s = Select::new()
914 .option(SelectOption::simple("Enabled"))
915 .option(SelectOption::simple("Disabled").disabled(true))
916 .item_height(32.0);
917 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
918 s.open = true;
919
920 let result = s.event(&Event::MouseDown {
922 position: Point::new(100.0, 80.0),
923 button: MouseButton::Left,
924 });
925
926 assert!(s.open); assert!(s.get_selected().is_none());
928 assert!(result.is_none());
929 }
930
931 #[test]
932 fn test_select_event_click_outside_closes() {
933 let mut s = Select::new()
934 .options_from_strings(["A", "B", "C"])
935 .item_height(32.0);
936 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
937 s.open = true;
938
939 let result = s.event(&Event::MouseDown {
941 position: Point::new(100.0, 500.0),
942 button: MouseButton::Left,
943 });
944
945 assert!(!s.open);
946 assert!(result.is_none());
947 }
948
949 #[test]
950 fn test_select_event_mouse_move_updates_hover() {
951 let mut s = Select::new()
952 .options_from_strings(["A", "B", "C"])
953 .item_height(32.0);
954 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
955 s.open = true;
956
957 assert!(s.hovered_item.is_none());
958
959 s.event(&Event::MouseMove {
961 position: Point::new(100.0, 80.0),
962 });
963 assert_eq!(s.hovered_item, Some(1));
964
965 s.event(&Event::MouseMove {
967 position: Point::new(100.0, 48.0),
968 });
969 assert_eq!(s.hovered_item, Some(0));
970 }
971
972 #[test]
973 fn test_select_event_mouse_move_when_closed_no_hover() {
974 let mut s = Select::new()
975 .options_from_strings(["A", "B", "C"])
976 .item_height(32.0);
977 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
978 s.event(&Event::MouseMove {
981 position: Point::new(100.0, 80.0),
982 });
983 assert!(s.hovered_item.is_none());
984 }
985
986 #[test]
987 fn test_select_event_focus_out_closes() {
988 let mut s = Select::new()
989 .options_from_strings(["A", "B", "C"])
990 .item_height(32.0);
991 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
992 s.open = true;
993
994 let result = s.event(&Event::FocusOut);
995 assert!(!s.open);
996 assert!(result.is_none());
997 }
998
999 #[test]
1000 fn test_select_event_disabled_blocks_click() {
1001 let mut s = Select::new()
1002 .options_from_strings(["A", "B", "C"])
1003 .item_height(32.0)
1004 .disabled(true);
1005 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1006
1007 let result = s.event(&Event::MouseDown {
1008 position: Point::new(100.0, 16.0),
1009 button: MouseButton::Left,
1010 });
1011
1012 assert!(!s.open);
1013 assert!(result.is_none());
1014 }
1015
1016 #[test]
1017 fn test_select_event_disabled_blocks_mouse_move() {
1018 let mut s = Select::new()
1019 .options_from_strings(["A", "B", "C"])
1020 .item_height(32.0)
1021 .disabled(true);
1022 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1023 s.open = true; s.event(&Event::MouseMove {
1026 position: Point::new(100.0, 80.0),
1027 });
1028 assert!(s.hovered_item.is_none());
1029 }
1030
1031 #[test]
1032 fn test_select_event_right_click_no_effect() {
1033 let mut s = Select::new()
1034 .options_from_strings(["A", "B", "C"])
1035 .item_height(32.0);
1036 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1037
1038 let result = s.event(&Event::MouseDown {
1039 position: Point::new(100.0, 16.0),
1040 button: MouseButton::Right,
1041 });
1042
1043 assert!(!s.open);
1044 assert!(result.is_none());
1045 }
1046
1047 #[test]
1048 fn test_select_event_click_header_clears_hover() {
1049 let mut s = Select::new()
1050 .options_from_strings(["A", "B", "C"])
1051 .item_height(32.0);
1052 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1053 s.hovered_item = Some(1);
1054
1055 s.event(&Event::MouseDown {
1057 position: Point::new(100.0, 16.0),
1058 button: MouseButton::Left,
1059 });
1060
1061 assert!(s.open);
1062 assert!(s.hovered_item.is_none()); }
1064
1065 #[test]
1066 fn test_select_event_full_interaction_flow() {
1067 let mut s = Select::new()
1068 .options_from_strings(["Red", "Green", "Blue"])
1069 .item_height(32.0);
1070 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1071
1072 s.event(&Event::MouseDown {
1074 position: Point::new(100.0, 16.0),
1075 button: MouseButton::Left,
1076 });
1077 assert!(s.open);
1078 assert!(s.selected.is_none());
1079
1080 s.event(&Event::MouseMove {
1082 position: Point::new(100.0, 48.0), });
1084 assert_eq!(s.hovered_item, Some(0));
1085
1086 s.event(&Event::MouseMove {
1087 position: Point::new(100.0, 112.0), });
1089 assert_eq!(s.hovered_item, Some(2));
1090
1091 let result = s.event(&Event::MouseDown {
1093 position: Point::new(100.0, 112.0),
1094 button: MouseButton::Left,
1095 });
1096 assert!(!s.open);
1097 assert_eq!(s.get_selected(), Some(2));
1098 assert_eq!(s.get_selected_value(), Some("Blue"));
1099
1100 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
1101 assert_eq!(msg.value, Some("Blue".to_string()));
1102
1103 s.event(&Event::MouseDown {
1105 position: Point::new(100.0, 16.0),
1106 button: MouseButton::Left,
1107 });
1108 assert!(s.open);
1109
1110 let result = s.event(&Event::MouseDown {
1112 position: Point::new(100.0, 48.0),
1113 button: MouseButton::Left,
1114 });
1115 assert_eq!(s.get_selected_value(), Some("Red"));
1116
1117 let msg = result.unwrap().downcast::<SelectionChanged>().unwrap();
1118 assert_eq!(msg.value, Some("Red".to_string()));
1119 assert_eq!(msg.index, Some(0));
1120 }
1121
1122 #[test]
1123 fn test_select_event_item_at_position_edge_cases() {
1124 let mut s = Select::new()
1125 .options_from_strings(["A", "B"])
1126 .item_height(32.0)
1127 .max_visible_items(2);
1128 s.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1129 s.open = true;
1130
1131 assert_eq!(s.item_at_position(32.0), Some(0));
1133 assert_eq!(s.item_at_position(64.0), Some(1));
1135 assert_eq!(s.item_at_position(96.0), None);
1137 assert_eq!(s.item_at_position(16.0), None);
1139 }
1140
1141 #[test]
1142 fn test_select_event_item_rect_positions() {
1143 let mut s = Select::new()
1144 .options_from_strings(["A", "B", "C"])
1145 .item_height(30.0);
1146 s.layout(Rect::new(10.0, 20.0, 200.0, 30.0));
1147
1148 let rect0 = s.item_rect(0);
1150 assert_eq!(rect0.x, 10.0);
1151 assert_eq!(rect0.y, 50.0);
1152 assert_eq!(rect0.height, 30.0);
1153
1154 let rect1 = s.item_rect(1);
1156 assert_eq!(rect1.y, 80.0);
1157 }
1158
1159 #[test]
1160 fn test_select_event_with_offset_bounds() {
1161 let mut s = Select::new()
1162 .options_from_strings(["X", "Y", "Z"])
1163 .item_height(32.0);
1164 s.layout(Rect::new(100.0, 50.0, 200.0, 32.0));
1165 s.open = true;
1166
1167 let result = s.event(&Event::MouseDown {
1169 position: Point::new(200.0, 98.0), button: MouseButton::Left,
1171 });
1172
1173 assert_eq!(s.get_selected(), Some(0));
1174 assert!(result.is_some());
1175 }
1176}