presentar_widgets/
select.rs

1//! Select/Dropdown widget for choosing from options.
2
3use 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/// A selectable option.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct SelectOption {
13    /// Unique value for this option
14    pub value: String,
15    /// Display label
16    pub label: String,
17    /// Whether this option is disabled
18    pub disabled: bool,
19}
20
21impl SelectOption {
22    /// Create a new option.
23    #[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    /// Create an option where value equals label.
33    #[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    /// Set disabled state.
44    #[must_use]
45    pub const fn disabled(mut self, disabled: bool) -> Self {
46        self.disabled = disabled;
47        self
48    }
49}
50
51/// Message emitted when selection changes.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct SelectionChanged {
54    /// The newly selected value (None if cleared)
55    pub value: Option<String>,
56    /// Index of the selected option
57    pub index: Option<usize>,
58}
59
60/// Select/Dropdown widget.
61#[derive(Serialize, Deserialize)]
62pub struct Select {
63    /// Available options
64    options: Vec<SelectOption>,
65    /// Currently selected index (None for no selection)
66    selected: Option<usize>,
67    /// Placeholder text when nothing selected
68    placeholder: String,
69    /// Whether the dropdown is currently open
70    #[serde(skip)]
71    open: bool,
72    /// Whether the widget is disabled
73    disabled: bool,
74    /// Minimum width
75    min_width: f32,
76    /// Item height
77    item_height: f32,
78    /// Maximum visible items in dropdown
79    max_visible_items: usize,
80    /// Background color
81    background_color: Color,
82    /// Border color
83    border_color: Color,
84    /// Selected item background
85    selected_bg_color: Color,
86    /// Hover item background
87    hover_bg_color: Color,
88    /// Text color
89    text_color: Color,
90    /// Placeholder text color
91    placeholder_color: Color,
92    /// Disabled color
93    disabled_color: Color,
94    /// Test ID
95    test_id_value: Option<String>,
96    /// Accessible name
97    accessible_name_value: Option<String>,
98    /// Cached bounds
99    #[serde(skip)]
100    bounds: Rect,
101    /// Currently hovered item index
102    #[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    /// Create a new select widget.
114    #[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    /// Add an option.
140    #[must_use]
141    pub fn option(mut self, opt: SelectOption) -> Self {
142        self.options.push(opt);
143        self
144    }
145
146    /// Add multiple options.
147    #[must_use]
148    pub fn options(mut self, opts: impl IntoIterator<Item = SelectOption>) -> Self {
149        self.options.extend(opts);
150        self
151    }
152
153    /// Set options from simple string values.
154    #[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    /// Set placeholder text.
164    #[must_use]
165    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
166        self.placeholder = text.into();
167        self
168    }
169
170    /// Set selected index.
171    #[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    /// Set selected by value.
178    #[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    /// Set disabled state.
185    #[must_use]
186    pub const fn disabled(mut self, disabled: bool) -> Self {
187        self.disabled = disabled;
188        self
189    }
190
191    /// Set minimum width.
192    #[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    /// Set item height.
199    #[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    /// Set max visible items.
206    #[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    /// Set background color.
213    #[must_use]
214    pub const fn background_color(mut self, color: Color) -> Self {
215        self.background_color = color;
216        self
217    }
218
219    /// Set border color.
220    #[must_use]
221    pub const fn border_color(mut self, color: Color) -> Self {
222        self.border_color = color;
223        self
224    }
225
226    /// Set test ID.
227    #[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    /// Set accessible name.
234    #[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    /// Get selected index.
241    #[must_use]
242    pub const fn get_selected(&self) -> Option<usize> {
243        self.selected
244    }
245
246    /// Get selected value.
247    #[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    /// Get selected label.
253    #[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    /// Get all options.
259    #[must_use]
260    pub fn get_options(&self) -> &[SelectOption] {
261        &self.options
262    }
263
264    /// Check if dropdown is open.
265    #[must_use]
266    pub const fn is_open(&self) -> bool {
267        self.open
268    }
269
270    /// Check if empty (no options).
271    #[must_use]
272    pub fn is_empty(&self) -> bool {
273        self.options.is_empty()
274    }
275
276    /// Get option count.
277    #[must_use]
278    pub fn option_count(&self) -> usize {
279        self.options.len()
280    }
281
282    /// Calculate dropdown height.
283    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    /// Get item rect at index.
289    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    /// Find item at position.
295    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        // Draw main button/header
336        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        // Draw selected text or placeholder
353        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        // Draw dropdown arrow
373        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        // Draw dropdown if open
379        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            // Draw items
391            for (i, opt) in self.options.iter().take(self.max_visible_items).enumerate() {
392                let item_rect = self.item_rect(i);
393
394                // Background
395                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                // Text
405                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                    // Toggle dropdown
447                    self.open = !self.open;
448                    self.hovered_item = None;
449                } else if self.open {
450                    // Check if clicked on an item
451                    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                        // Clicked outside - close
463                        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    // =========================================================================
511    // SelectOption Tests - TESTS FIRST
512    // =========================================================================
513
514    #[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    // =========================================================================
536    // SelectionChanged Tests - TESTS FIRST
537    // =========================================================================
538
539    #[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    // =========================================================================
560    // Select Construction Tests - TESTS FIRST
561    // =========================================================================
562
563    #[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    // =========================================================================
616    // Select Selection Tests - TESTS FIRST
617    // =========================================================================
618
619    #[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); // Should clamp
644    }
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    // =========================================================================
663    // Select Widget Trait Tests - TESTS FIRST
664    // =========================================================================
665
666    #[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    // =========================================================================
711    // Select Layout Tests - TESTS FIRST
712    // =========================================================================
713
714    #[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    // =========================================================================
724    // Select Size Tests - TESTS FIRST
725    // =========================================================================
726
727    #[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); // Minimum is 50
731    }
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); // Minimum is 20
737    }
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); // Minimum is 1
743    }
744
745    // =========================================================================
746    // Select Color Tests - TESTS FIRST
747    // =========================================================================
748
749    #[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    // =========================================================================
759    // Select Dropdown Tests - TESTS FIRST
760    // =========================================================================
761
762    #[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        // 3 items * 30px = 90px
769        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        // limited to 3 items * 30px = 90px
779        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    // =========================================================================
798    // Event Handling Tests - TESTS FIRST
799    // =========================================================================
800
801    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), // In header
813            button: MouseButton::Left,
814        });
815        assert!(s.open);
816        assert!(result.is_none()); // Just opens, no selection
817    }
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), // In header
829            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        // Click second item (index 1): y = 32 + 32 + 16 = 80 (middle of item 1)
844        let result = s.event(&Event::MouseDown {
845            position: Point::new(100.0, 80.0),
846            button: MouseButton::Left,
847        });
848
849        assert!(!s.open); // Closes after selection
850        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        // Click first item (index 0): y = 32 + 16 = 48
867        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        // Click disabled item (index 1)
888        let result = s.event(&Event::MouseDown {
889            position: Point::new(100.0, 80.0),
890            button: MouseButton::Left,
891        });
892
893        assert!(s.open); // Stays open
894        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        // Click far below dropdown
907        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        // Hover over item 1
927        s.event(&Event::MouseMove {
928            position: Point::new(100.0, 80.0),
929        });
930        assert_eq!(s.hovered_item, Some(1));
931
932        // Hover over item 0
933        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        // Closed
946
947        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; // Force open
991
992        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        // Open dropdown
1023        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()); // Cleared
1030    }
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        // 1. Click to open
1040        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        // 2. Hover over items
1048        s.event(&Event::MouseMove {
1049            position: Point::new(100.0, 48.0), // Item 0
1050        });
1051        assert_eq!(s.hovered_item, Some(0));
1052
1053        s.event(&Event::MouseMove {
1054            position: Point::new(100.0, 112.0), // Item 2
1055        });
1056        assert_eq!(s.hovered_item, Some(2));
1057
1058        // 3. Select item 2
1059        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        // 4. Reopen and change selection
1071        s.event(&Event::MouseDown {
1072            position: Point::new(100.0, 16.0),
1073            button: MouseButton::Left,
1074        });
1075        assert!(s.open);
1076
1077        // 5. Select item 0
1078        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        // Just inside first item
1099        assert_eq!(s.item_at_position(32.0), Some(0));
1100        // Just inside second item
1101        assert_eq!(s.item_at_position(64.0), Some(1));
1102        // Past last item
1103        assert_eq!(s.item_at_position(96.0), None);
1104        // In header area
1105        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        // Item 0 starts at y = 20 + 30 = 50
1116        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        // Item 1 starts at y = 20 + 30 + 30 = 80
1122        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        // Click item 0: header is at y=50-82, item 0 is at y=82-114
1135        let result = s.event(&Event::MouseDown {
1136            position: Point::new(200.0, 98.0), // Middle of item 0
1137            button: MouseButton::Left,
1138        });
1139
1140        assert_eq!(s.get_selected(), Some(0));
1141        assert!(result.is_some());
1142    }
1143}