presentar_widgets/
select.rs

1//! Select/Dropdown widget for choosing from options.
2
3use 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/// A selectable option.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct SelectOption {
14    /// Unique value for this option
15    pub value: String,
16    /// Display label
17    pub label: String,
18    /// Whether this option is disabled
19    pub disabled: bool,
20}
21
22impl SelectOption {
23    /// Create a new option.
24    #[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    /// Create an option where value equals label.
34    #[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    /// Set disabled state.
45    #[must_use]
46    pub const fn disabled(mut self, disabled: bool) -> Self {
47        self.disabled = disabled;
48        self
49    }
50}
51
52/// Message emitted when selection changes.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct SelectionChanged {
55    /// The newly selected value (None if cleared)
56    pub value: Option<String>,
57    /// Index of the selected option
58    pub index: Option<usize>,
59}
60
61/// Select/Dropdown widget.
62#[derive(Serialize, Deserialize)]
63pub struct Select {
64    /// Available options
65    options: Vec<SelectOption>,
66    /// Currently selected index (None for no selection)
67    selected: Option<usize>,
68    /// Placeholder text when nothing selected
69    placeholder: String,
70    /// Whether the dropdown is currently open
71    #[serde(skip)]
72    open: bool,
73    /// Whether the widget is disabled
74    disabled: bool,
75    /// Minimum width
76    min_width: f32,
77    /// Item height
78    item_height: f32,
79    /// Maximum visible items in dropdown
80    max_visible_items: usize,
81    /// Background color
82    background_color: Color,
83    /// Border color
84    border_color: Color,
85    /// Selected item background
86    selected_bg_color: Color,
87    /// Hover item background
88    hover_bg_color: Color,
89    /// Text color
90    text_color: Color,
91    /// Placeholder text color
92    placeholder_color: Color,
93    /// Disabled color
94    disabled_color: Color,
95    /// Test ID
96    test_id_value: Option<String>,
97    /// Accessible name
98    accessible_name_value: Option<String>,
99    /// Cached bounds
100    #[serde(skip)]
101    bounds: Rect,
102    /// Currently hovered item index
103    #[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    /// Create a new select widget.
115    #[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    /// Add an option.
141    #[must_use]
142    pub fn option(mut self, opt: SelectOption) -> Self {
143        self.options.push(opt);
144        self
145    }
146
147    /// Add multiple options.
148    #[must_use]
149    pub fn options(mut self, opts: impl IntoIterator<Item = SelectOption>) -> Self {
150        self.options.extend(opts);
151        self
152    }
153
154    /// Set options from simple string values.
155    #[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    /// Set placeholder text.
165    #[must_use]
166    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
167        self.placeholder = text.into();
168        self
169    }
170
171    /// Set selected index.
172    #[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    /// Set selected by value.
179    #[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    /// Set disabled state.
186    #[must_use]
187    pub const fn disabled(mut self, disabled: bool) -> Self {
188        self.disabled = disabled;
189        self
190    }
191
192    /// Set minimum width.
193    #[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    /// Set item height.
200    #[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    /// Set max visible items.
207    #[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    /// Set background color.
214    #[must_use]
215    pub const fn background_color(mut self, color: Color) -> Self {
216        self.background_color = color;
217        self
218    }
219
220    /// Set border color.
221    #[must_use]
222    pub const fn border_color(mut self, color: Color) -> Self {
223        self.border_color = color;
224        self
225    }
226
227    /// Set test ID.
228    #[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    /// Set accessible name.
235    #[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    /// Get selected index.
242    #[must_use]
243    pub const fn get_selected(&self) -> Option<usize> {
244        self.selected
245    }
246
247    /// Get selected value.
248    #[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    /// Get selected label.
254    #[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    /// Get all options.
260    #[must_use]
261    pub fn get_options(&self) -> &[SelectOption] {
262        &self.options
263    }
264
265    /// Check if dropdown is open.
266    #[must_use]
267    pub const fn is_open(&self) -> bool {
268        self.open
269    }
270
271    /// Check if empty (no options).
272    #[must_use]
273    pub fn is_empty(&self) -> bool {
274        self.options.is_empty()
275    }
276
277    /// Get option count.
278    #[must_use]
279    pub fn option_count(&self) -> usize {
280        self.options.len()
281    }
282
283    /// Calculate dropdown height.
284    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    /// Get item rect at index.
290    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    /// Find item at position.
296    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        // Draw main button/header
337        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        // Draw selected text or placeholder
354        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        // Draw dropdown arrow
374        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        // Draw dropdown if open
380        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            // Draw items
392            for (i, opt) in self.options.iter().take(self.max_visible_items).enumerate() {
393                let item_rect = self.item_rect(i);
394
395                // Background
396                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                // Text
406                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                    // Toggle dropdown
448                    self.open = !self.open;
449                    self.hovered_item = None;
450                } else if self.open {
451                    // Check if clicked on an item
452                    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                        // Clicked outside - close
464                        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
506// PROBAR-SPEC-009: Brick Architecture - Tests define interface
507impl 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    // =========================================================================
543    // SelectOption Tests - TESTS FIRST
544    // =========================================================================
545
546    #[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    // =========================================================================
568    // SelectionChanged Tests - TESTS FIRST
569    // =========================================================================
570
571    #[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    // =========================================================================
592    // Select Construction Tests - TESTS FIRST
593    // =========================================================================
594
595    #[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    // =========================================================================
648    // Select Selection Tests - TESTS FIRST
649    // =========================================================================
650
651    #[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); // Should clamp
676    }
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    // =========================================================================
695    // Select Widget Trait Tests - TESTS FIRST
696    // =========================================================================
697
698    #[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    // =========================================================================
743    // Select Layout Tests - TESTS FIRST
744    // =========================================================================
745
746    #[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    // =========================================================================
756    // Select Size Tests - TESTS FIRST
757    // =========================================================================
758
759    #[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); // Minimum is 50
763    }
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); // Minimum is 20
769    }
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); // Minimum is 1
775    }
776
777    // =========================================================================
778    // Select Color Tests - TESTS FIRST
779    // =========================================================================
780
781    #[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    // =========================================================================
791    // Select Dropdown Tests - TESTS FIRST
792    // =========================================================================
793
794    #[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        // 3 items * 30px = 90px
801        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        // limited to 3 items * 30px = 90px
811        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    // =========================================================================
830    // Event Handling Tests - TESTS FIRST
831    // =========================================================================
832
833    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), // In header
845            button: MouseButton::Left,
846        });
847        assert!(s.open);
848        assert!(result.is_none()); // Just opens, no selection
849    }
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), // In header
861            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        // Click second item (index 1): y = 32 + 32 + 16 = 80 (middle of item 1)
876        let result = s.event(&Event::MouseDown {
877            position: Point::new(100.0, 80.0),
878            button: MouseButton::Left,
879        });
880
881        assert!(!s.open); // Closes after selection
882        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        // Click first item (index 0): y = 32 + 16 = 48
899        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        // Click disabled item (index 1)
920        let result = s.event(&Event::MouseDown {
921            position: Point::new(100.0, 80.0),
922            button: MouseButton::Left,
923        });
924
925        assert!(s.open); // Stays open
926        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        // Click far below dropdown
939        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        // Hover over item 1
959        s.event(&Event::MouseMove {
960            position: Point::new(100.0, 80.0),
961        });
962        assert_eq!(s.hovered_item, Some(1));
963
964        // Hover over item 0
965        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        // Closed
978
979        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; // Force open
1023
1024        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        // Open dropdown
1055        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()); // Cleared
1062    }
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        // 1. Click to open
1072        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        // 2. Hover over items
1080        s.event(&Event::MouseMove {
1081            position: Point::new(100.0, 48.0), // Item 0
1082        });
1083        assert_eq!(s.hovered_item, Some(0));
1084
1085        s.event(&Event::MouseMove {
1086            position: Point::new(100.0, 112.0), // Item 2
1087        });
1088        assert_eq!(s.hovered_item, Some(2));
1089
1090        // 3. Select item 2
1091        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        // 4. Reopen and change selection
1103        s.event(&Event::MouseDown {
1104            position: Point::new(100.0, 16.0),
1105            button: MouseButton::Left,
1106        });
1107        assert!(s.open);
1108
1109        // 5. Select item 0
1110        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        // Just inside first item
1131        assert_eq!(s.item_at_position(32.0), Some(0));
1132        // Just inside second item
1133        assert_eq!(s.item_at_position(64.0), Some(1));
1134        // Past last item
1135        assert_eq!(s.item_at_position(96.0), None);
1136        // In header area
1137        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        // Item 0 starts at y = 20 + 30 = 50
1148        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        // Item 1 starts at y = 20 + 30 + 30 = 80
1154        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        // Click item 0: header is at y=50-82, item 0 is at y=82-114
1167        let result = s.event(&Event::MouseDown {
1168            position: Point::new(200.0, 98.0), // Middle of item 0
1169            button: MouseButton::Left,
1170        });
1171
1172        assert_eq!(s.get_selected(), Some(0));
1173        assert!(result.is_some());
1174    }
1175}