presentar_widgets/
list.rs

1//! Virtualized list widget for efficient scrolling.
2//!
3//! The List widget only renders items that are visible in the viewport,
4//! enabling smooth scrolling of thousands of items.
5
6use presentar_core::{
7    widget::LayoutResult, Canvas, Constraints, Event, Key, Rect, Size, TypeId, Widget,
8};
9use serde::{Deserialize, Serialize};
10use std::any::Any;
11use std::ops::Range;
12
13/// Type alias for the render item callback.
14pub type RenderItemFn = Box<dyn Fn(usize, &ListItem) -> Box<dyn Widget> + Send + Sync>;
15
16/// Direction of the list.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
18pub enum ListDirection {
19    /// Vertical scrolling (default)
20    #[default]
21    Vertical,
22    /// Horizontal scrolling
23    Horizontal,
24}
25
26/// Selection mode for list items.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
28pub enum SelectionMode {
29    /// No selection
30    #[default]
31    None,
32    /// Single item selection
33    Single,
34    /// Multiple item selection
35    Multiple,
36}
37
38/// List item data for virtualization.
39#[derive(Debug, Clone)]
40pub struct ListItem {
41    /// Unique key for this item
42    pub key: String,
43    /// Item height (or width in horizontal mode)
44    pub size: f32,
45    /// Whether item is selected
46    pub selected: bool,
47}
48
49impl ListItem {
50    /// Create a new list item.
51    #[must_use]
52    pub fn new(key: impl Into<String>) -> Self {
53        Self {
54            key: key.into(),
55            size: 48.0, // Default item height
56            selected: false,
57        }
58    }
59
60    /// Set the item size.
61    #[must_use]
62    pub const fn size(mut self, size: f32) -> Self {
63        self.size = size;
64        self
65    }
66
67    /// Set selection state.
68    #[must_use]
69    pub const fn selected(mut self, selected: bool) -> Self {
70        self.selected = selected;
71        self
72    }
73}
74
75/// Virtualized list widget.
76#[derive(Serialize, Deserialize)]
77pub struct List {
78    /// Scroll direction
79    pub direction: ListDirection,
80    /// Selection mode
81    pub selection_mode: SelectionMode,
82    /// Fixed item height (if None, items have variable height)
83    pub item_height: Option<f32>,
84    /// Gap between items
85    pub gap: f32,
86    /// Current scroll offset
87    pub scroll_offset: f32,
88    /// List items (keys and sizes)
89    #[serde(skip)]
90    items: Vec<ListItem>,
91    /// Selected indices
92    #[serde(skip)]
93    selected: Vec<usize>,
94    /// Focused item index
95    #[serde(skip)]
96    focused_index: Option<usize>,
97    /// Cached bounds after layout
98    #[serde(skip)]
99    bounds: Rect,
100    /// Cached visible range
101    #[serde(skip)]
102    visible_range: Range<usize>,
103    /// Cached item positions (start position for each item)
104    #[serde(skip)]
105    item_positions: Vec<f32>,
106    /// Total content size
107    #[serde(skip)]
108    content_size: f32,
109    /// Test ID
110    test_id_value: Option<String>,
111    /// Child widgets (rendered items only)
112    #[serde(skip)]
113    children: Vec<Box<dyn Widget>>,
114    /// Render callback (item index -> widget)
115    #[serde(skip)]
116    render_item: Option<RenderItemFn>,
117}
118
119impl Default for List {
120    fn default() -> Self {
121        Self {
122            direction: ListDirection::Vertical,
123            selection_mode: SelectionMode::None,
124            item_height: Some(48.0),
125            gap: 0.0,
126            scroll_offset: 0.0,
127            items: Vec::new(),
128            selected: Vec::new(),
129            focused_index: None,
130            bounds: Rect::default(),
131            visible_range: 0..0,
132            item_positions: Vec::new(),
133            content_size: 0.0,
134            test_id_value: None,
135            children: Vec::new(),
136            render_item: None,
137        }
138    }
139}
140
141impl List {
142    /// Create a new empty list.
143    #[must_use]
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    /// Set the scroll direction.
149    #[must_use]
150    pub const fn direction(mut self, direction: ListDirection) -> Self {
151        self.direction = direction;
152        self
153    }
154
155    /// Set the selection mode.
156    #[must_use]
157    pub const fn selection_mode(mut self, mode: SelectionMode) -> Self {
158        self.selection_mode = mode;
159        self
160    }
161
162    /// Set fixed item height.
163    #[must_use]
164    pub const fn item_height(mut self, height: f32) -> Self {
165        self.item_height = Some(height);
166        self
167    }
168
169    /// Set gap between items.
170    #[must_use]
171    pub const fn gap(mut self, gap: f32) -> Self {
172        self.gap = gap;
173        self
174    }
175
176    /// Add items to the list.
177    pub fn items(mut self, items: impl IntoIterator<Item = ListItem>) -> Self {
178        self.items = items.into_iter().collect();
179        self.recalculate_positions();
180        self
181    }
182
183    /// Set the render callback for items.
184    pub fn render_with<F>(mut self, f: F) -> Self
185    where
186        F: Fn(usize, &ListItem) -> Box<dyn Widget> + Send + Sync + 'static,
187    {
188        self.render_item = Some(Box::new(f));
189        self
190    }
191
192    /// Set the test ID.
193    #[must_use]
194    pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
195        self.test_id_value = Some(id.into());
196        self
197    }
198
199    /// Get total item count.
200    #[must_use]
201    pub fn item_count(&self) -> usize {
202        self.items.len()
203    }
204
205    /// Get selected indices.
206    #[must_use]
207    pub fn selected_indices(&self) -> &[usize] {
208        &self.selected
209    }
210
211    /// Get visible range.
212    #[must_use]
213    pub fn visible_range(&self) -> Range<usize> {
214        self.visible_range.clone()
215    }
216
217    /// Get total content size.
218    #[must_use]
219    pub const fn content_size(&self) -> f32 {
220        self.content_size
221    }
222
223    /// Scroll to a specific item.
224    pub fn scroll_to(&mut self, index: usize) {
225        if index >= self.items.len() {
226            return;
227        }
228
229        let item_pos = self.item_positions.get(index).copied().unwrap_or(0.0);
230        let viewport_size = match self.direction {
231            ListDirection::Vertical => self.bounds.height,
232            ListDirection::Horizontal => self.bounds.width,
233        };
234
235        // Scroll so item is at top/left of viewport
236        self.scroll_offset = item_pos.min(self.content_size - viewport_size).max(0.0);
237    }
238
239    /// Scroll to ensure an item is visible.
240    pub fn scroll_into_view(&mut self, index: usize) {
241        if index >= self.items.len() {
242            return;
243        }
244
245        let item_pos = self.item_positions.get(index).copied().unwrap_or(0.0);
246        let item_size = self.get_item_size(index);
247        let viewport_size = match self.direction {
248            ListDirection::Vertical => self.bounds.height,
249            ListDirection::Horizontal => self.bounds.width,
250        };
251
252        let item_end = item_pos + item_size;
253        let viewport_end = self.scroll_offset + viewport_size;
254
255        if item_pos < self.scroll_offset {
256            // Item is above viewport - scroll up
257            self.scroll_offset = item_pos;
258        } else if item_end > viewport_end {
259            // Item is below viewport - scroll down
260            self.scroll_offset = (item_end - viewport_size).max(0.0);
261        }
262    }
263
264    /// Select an item.
265    pub fn select(&mut self, index: usize) {
266        match self.selection_mode {
267            SelectionMode::None => {}
268            SelectionMode::Single => {
269                self.selected.clear();
270                if index < self.items.len() {
271                    self.selected.push(index);
272                    self.items[index].selected = true;
273                }
274            }
275            SelectionMode::Multiple => {
276                if index < self.items.len() && !self.selected.contains(&index) {
277                    self.selected.push(index);
278                    self.items[index].selected = true;
279                }
280            }
281        }
282    }
283
284    /// Deselect an item.
285    pub fn deselect(&mut self, index: usize) {
286        if let Some(pos) = self.selected.iter().position(|&i| i == index) {
287            self.selected.remove(pos);
288            if index < self.items.len() {
289                self.items[index].selected = false;
290            }
291        }
292    }
293
294    /// Toggle item selection.
295    pub fn toggle_selection(&mut self, index: usize) {
296        if self.selected.contains(&index) {
297            self.deselect(index);
298        } else {
299            self.select(index);
300        }
301    }
302
303    /// Clear all selections.
304    pub fn clear_selection(&mut self) {
305        for &i in &self.selected {
306            if i < self.items.len() {
307                self.items[i].selected = false;
308            }
309        }
310        self.selected.clear();
311    }
312
313    /// Get item size at index.
314    fn get_item_size(&self, index: usize) -> f32 {
315        if let Some(fixed) = self.item_height {
316            fixed
317        } else {
318            self.items.get(index).map_or(48.0, |i| i.size)
319        }
320    }
321
322    /// Recalculate item positions after items change.
323    fn recalculate_positions(&mut self) {
324        self.item_positions.clear();
325        let mut pos = 0.0;
326
327        for (i, item) in self.items.iter().enumerate() {
328            self.item_positions.push(pos);
329            let size = self.item_height.unwrap_or(item.size);
330            pos += size;
331            if i < self.items.len() - 1 {
332                pos += self.gap;
333            }
334        }
335
336        self.content_size = pos;
337    }
338
339    /// Calculate visible range based on scroll offset and viewport.
340    fn calculate_visible_range(&mut self, viewport_size: f32) {
341        if self.items.is_empty() {
342            self.visible_range = 0..0;
343            return;
344        }
345
346        let start_offset = self.scroll_offset;
347        let end_offset = self.scroll_offset + viewport_size;
348
349        // Binary search for first visible item
350        let first = self
351            .item_positions
352            .partition_point(|&pos| pos + self.get_item_size(0) < start_offset);
353
354        // Linear scan for last visible (typically few items visible)
355        let mut last = first;
356        for i in first..self.items.len() {
357            let pos = self.item_positions.get(i).copied().unwrap_or(0.0);
358            if pos > end_offset {
359                break;
360            }
361            last = i + 1;
362        }
363
364        // Add buffer for smooth scrolling
365        let buffer = 2;
366        let start = first.saturating_sub(buffer);
367        let end = (last + buffer).min(self.items.len());
368
369        self.visible_range = start..end;
370    }
371
372    /// Render visible items.
373    fn render_visible_items(&mut self) {
374        self.children.clear();
375
376        if self.render_item.is_none() {
377            return;
378        }
379
380        for i in self.visible_range.clone() {
381            if let Some(item) = self.items.get(i) {
382                if let Some(ref render) = self.render_item {
383                    let widget = render(i, item);
384                    self.children.push(widget);
385                }
386            }
387        }
388    }
389}
390
391impl Widget for List {
392    fn type_id(&self) -> TypeId {
393        TypeId::of::<Self>()
394    }
395
396    fn measure(&self, constraints: Constraints) -> Size {
397        // List takes available space (scrollable content)
398        constraints.constrain(Size::new(constraints.max_width, constraints.max_height))
399    }
400
401    fn layout(&mut self, bounds: Rect) -> LayoutResult {
402        self.bounds = bounds;
403
404        let viewport_size = match self.direction {
405            ListDirection::Vertical => bounds.height,
406            ListDirection::Horizontal => bounds.width,
407        };
408
409        // Calculate visible range
410        self.calculate_visible_range(viewport_size);
411
412        // Render visible items
413        self.render_visible_items();
414
415        // Layout visible children
416        for (local_idx, i) in self.visible_range.clone().enumerate() {
417            if local_idx >= self.children.len() {
418                break;
419            }
420
421            let item_pos = self.item_positions.get(i).copied().unwrap_or(0.0);
422            let item_size = self.get_item_size(i);
423
424            let item_bounds = match self.direction {
425                ListDirection::Vertical => Rect::new(
426                    bounds.x,
427                    bounds.y + item_pos - self.scroll_offset,
428                    bounds.width,
429                    item_size,
430                ),
431                ListDirection::Horizontal => Rect::new(
432                    bounds.x + item_pos - self.scroll_offset,
433                    bounds.y,
434                    item_size,
435                    bounds.height,
436                ),
437            };
438
439            self.children[local_idx].layout(item_bounds);
440        }
441
442        LayoutResult {
443            size: bounds.size(),
444        }
445    }
446
447    fn paint(&self, canvas: &mut dyn Canvas) {
448        // Clip to bounds
449        canvas.push_clip(self.bounds);
450
451        // Paint visible children
452        for child in &self.children {
453            child.paint(canvas);
454        }
455
456        canvas.pop_clip();
457    }
458
459    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
460        match event {
461            Event::Scroll { delta_y, .. } => {
462                let viewport_size = match self.direction {
463                    ListDirection::Vertical => self.bounds.height,
464                    ListDirection::Horizontal => self.bounds.width,
465                };
466
467                let max_scroll = (self.content_size - viewport_size).max(0.0);
468                self.scroll_offset = (self.scroll_offset - delta_y * 48.0).clamp(0.0, max_scroll);
469
470                // Recalculate visible range
471                self.calculate_visible_range(viewport_size);
472                self.render_visible_items();
473
474                Some(Box::new(ListScrolled {
475                    offset: self.scroll_offset,
476                }))
477            }
478            Event::KeyDown { key } => {
479                if let Some(focused) = self.focused_index {
480                    match key {
481                        Key::Up | Key::Left => {
482                            if focused > 0 {
483                                self.focused_index = Some(focused - 1);
484                                self.scroll_into_view(focused - 1);
485                            }
486                        }
487                        Key::Down | Key::Right => {
488                            if focused < self.items.len() - 1 {
489                                self.focused_index = Some(focused + 1);
490                                self.scroll_into_view(focused + 1);
491                            }
492                        }
493                        Key::Enter | Key::Space => {
494                            self.toggle_selection(focused);
495                            return Some(Box::new(ListItemSelected { index: focused }));
496                        }
497                        Key::Home => {
498                            self.focused_index = Some(0);
499                            self.scroll_to(0);
500                        }
501                        Key::End => {
502                            let last = self.items.len().saturating_sub(1);
503                            self.focused_index = Some(last);
504                            self.scroll_to(last);
505                        }
506                        _ => {}
507                    }
508                }
509                None
510            }
511            Event::MouseDown { position, .. } => {
512                // Find clicked item
513                let pos = match self.direction {
514                    ListDirection::Vertical => position.y - self.bounds.y + self.scroll_offset,
515                    ListDirection::Horizontal => position.x - self.bounds.x + self.scroll_offset,
516                };
517
518                for (i, &item_pos) in self.item_positions.iter().enumerate() {
519                    let item_size = self.get_item_size(i);
520                    if pos >= item_pos && pos < item_pos + item_size {
521                        self.focused_index = Some(i);
522                        self.toggle_selection(i);
523                        return Some(Box::new(ListItemClicked { index: i }));
524                    }
525                }
526                None
527            }
528            _ => {
529                // Propagate to visible children
530                for child in &mut self.children {
531                    if let Some(msg) = child.event(event) {
532                        return Some(msg);
533                    }
534                }
535                None
536            }
537        }
538    }
539
540    fn children(&self) -> &[Box<dyn Widget>] {
541        &self.children
542    }
543
544    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
545        &mut self.children
546    }
547
548    fn is_focusable(&self) -> bool {
549        true
550    }
551
552    fn test_id(&self) -> Option<&str> {
553        self.test_id_value.as_deref()
554    }
555
556    fn bounds(&self) -> Rect {
557        self.bounds
558    }
559}
560
561/// Message emitted when list is scrolled.
562#[derive(Debug, Clone)]
563pub struct ListScrolled {
564    /// New scroll offset
565    pub offset: f32,
566}
567
568/// Message emitted when a list item is clicked.
569#[derive(Debug, Clone)]
570pub struct ListItemClicked {
571    /// Index of clicked item
572    pub index: usize,
573}
574
575/// Message emitted when a list item is selected.
576#[derive(Debug, Clone)]
577pub struct ListItemSelected {
578    /// Index of selected item
579    pub index: usize,
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585
586    // =========================================================================
587    // ListDirection Tests
588    // =========================================================================
589
590    #[test]
591    fn test_list_direction_default() {
592        assert_eq!(ListDirection::default(), ListDirection::Vertical);
593    }
594
595    // =========================================================================
596    // SelectionMode Tests
597    // =========================================================================
598
599    #[test]
600    fn test_selection_mode_default() {
601        assert_eq!(SelectionMode::default(), SelectionMode::None);
602    }
603
604    // =========================================================================
605    // ListItem Tests
606    // =========================================================================
607
608    #[test]
609    fn test_list_item_new() {
610        let item = ListItem::new("item-1");
611        assert_eq!(item.key, "item-1");
612        assert_eq!(item.size, 48.0);
613        assert!(!item.selected);
614    }
615
616    #[test]
617    fn test_list_item_builder() {
618        let item = ListItem::new("item-1").size(64.0).selected(true);
619        assert_eq!(item.size, 64.0);
620        assert!(item.selected);
621    }
622
623    // =========================================================================
624    // List Tests
625    // =========================================================================
626
627    #[test]
628    fn test_list_new() {
629        let list = List::new();
630        assert_eq!(list.direction, ListDirection::Vertical);
631        assert_eq!(list.selection_mode, SelectionMode::None);
632        assert_eq!(list.item_height, Some(48.0));
633        assert_eq!(list.gap, 0.0);
634        assert_eq!(list.item_count(), 0);
635    }
636
637    #[test]
638    fn test_list_builder() {
639        let list = List::new()
640            .direction(ListDirection::Horizontal)
641            .selection_mode(SelectionMode::Single)
642            .item_height(32.0)
643            .gap(8.0);
644
645        assert_eq!(list.direction, ListDirection::Horizontal);
646        assert_eq!(list.selection_mode, SelectionMode::Single);
647        assert_eq!(list.item_height, Some(32.0));
648        assert_eq!(list.gap, 8.0);
649    }
650
651    #[test]
652    fn test_list_items() {
653        let items = vec![ListItem::new("1"), ListItem::new("2"), ListItem::new("3")];
654        let list = List::new().items(items);
655        assert_eq!(list.item_count(), 3);
656    }
657
658    #[test]
659    fn test_list_content_size() {
660        let items = vec![ListItem::new("1"), ListItem::new("2"), ListItem::new("3")];
661        let list = List::new().item_height(50.0).gap(10.0).items(items);
662        // 3 items * 50px + 2 gaps * 10px = 170px
663        assert_eq!(list.content_size(), 170.0);
664    }
665
666    #[test]
667    fn test_list_content_size_variable_height() {
668        let items = vec![
669            ListItem::new("1").size(30.0),
670            ListItem::new("2").size(40.0),
671            ListItem::new("3").size(50.0),
672        ];
673        let mut list = List::new().gap(5.0);
674        list.item_height = None; // Variable height mode
675        list = list.items(items);
676        // 30 + 5 + 40 + 5 + 50 = 130px
677        assert_eq!(list.content_size(), 130.0);
678    }
679
680    #[test]
681    fn test_list_select_single() {
682        let items = vec![ListItem::new("1"), ListItem::new("2")];
683        let mut list = List::new()
684            .selection_mode(SelectionMode::Single)
685            .items(items);
686
687        list.select(0);
688        assert_eq!(list.selected_indices(), &[0]);
689
690        list.select(1);
691        assert_eq!(list.selected_indices(), &[1]); // Single mode replaces
692    }
693
694    #[test]
695    fn test_list_select_multiple() {
696        let items = vec![ListItem::new("1"), ListItem::new("2")];
697        let mut list = List::new()
698            .selection_mode(SelectionMode::Multiple)
699            .items(items);
700
701        list.select(0);
702        list.select(1);
703        assert_eq!(list.selected_indices(), &[0, 1]);
704    }
705
706    #[test]
707    fn test_list_deselect() {
708        let items = vec![ListItem::new("1"), ListItem::new("2")];
709        let mut list = List::new()
710            .selection_mode(SelectionMode::Multiple)
711            .items(items);
712
713        list.select(0);
714        list.select(1);
715        list.deselect(0);
716        assert_eq!(list.selected_indices(), &[1]);
717    }
718
719    #[test]
720    fn test_list_toggle_selection() {
721        let items = vec![ListItem::new("1")];
722        let mut list = List::new()
723            .selection_mode(SelectionMode::Single)
724            .items(items);
725
726        list.toggle_selection(0);
727        assert_eq!(list.selected_indices(), &[0]);
728
729        list.toggle_selection(0);
730        assert!(list.selected_indices().is_empty());
731    }
732
733    #[test]
734    fn test_list_clear_selection() {
735        let items = vec![ListItem::new("1"), ListItem::new("2")];
736        let mut list = List::new()
737            .selection_mode(SelectionMode::Multiple)
738            .items(items);
739
740        list.select(0);
741        list.select(1);
742        list.clear_selection();
743        assert!(list.selected_indices().is_empty());
744    }
745
746    #[test]
747    fn test_list_scroll_to() {
748        let items: Vec<_> = (0..100).map(|i| ListItem::new(format!("{i}"))).collect();
749        let mut list = List::new().item_height(50.0).items(items);
750        list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
751
752        list.scroll_to(10);
753        assert_eq!(list.scroll_offset, 500.0); // 10 * 50px
754    }
755
756    #[test]
757    fn test_list_scroll_into_view() {
758        let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
759        let mut list = List::new().item_height(50.0).items(items);
760        list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
761        list.scroll_offset = 0.0;
762
763        // Item 5 is at 250px, below viewport (0-200)
764        list.scroll_into_view(5);
765        // Should scroll so item end (300px) is at viewport end
766        assert_eq!(list.scroll_offset, 100.0);
767    }
768
769    #[test]
770    fn test_list_visible_range() {
771        let items: Vec<_> = (0..100).map(|i| ListItem::new(format!("{i}"))).collect();
772        let mut list = List::new().item_height(50.0).items(items);
773        list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
774        list.scroll_offset = 0.0;
775
776        list.calculate_visible_range(200.0);
777
778        // At scroll 0 with 200px viewport and 50px items, ~4 items visible
779        // Plus buffer of 2 on each side
780        let range = list.visible_range();
781        assert!(range.start <= 4);
782        assert!(range.end >= 4);
783    }
784
785    #[test]
786    fn test_list_measure() {
787        let list = List::new();
788        let size = list.measure(Constraints::loose(Size::new(300.0, 400.0)));
789        assert_eq!(size, Size::new(300.0, 400.0));
790    }
791
792    #[test]
793    fn test_list_layout() {
794        let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
795        let mut list = List::new().item_height(50.0).items(items);
796
797        let result = list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
798        assert_eq!(result.size, Size::new(300.0, 200.0));
799        assert_eq!(list.bounds, Rect::new(0.0, 0.0, 300.0, 200.0));
800    }
801
802    #[test]
803    fn test_list_type_id() {
804        let list = List::new();
805        assert_eq!(Widget::type_id(&list), TypeId::of::<List>());
806    }
807
808    #[test]
809    fn test_list_is_focusable() {
810        let list = List::new();
811        assert!(list.is_focusable());
812    }
813
814    #[test]
815    fn test_list_test_id() {
816        let list = List::new().with_test_id("my-list");
817        assert_eq!(list.test_id(), Some("my-list"));
818    }
819
820    #[test]
821    fn test_list_children_empty() {
822        let list = List::new();
823        assert!(list.children().is_empty());
824    }
825
826    #[test]
827    fn test_list_bounds() {
828        let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
829        let mut list = List::new().items(items);
830        list.layout(Rect::new(10.0, 20.0, 300.0, 200.0));
831        assert_eq!(list.bounds(), Rect::new(10.0, 20.0, 300.0, 200.0));
832    }
833
834    #[test]
835    fn test_list_scrolled_message() {
836        let msg = ListScrolled { offset: 100.0 };
837        assert_eq!(msg.offset, 100.0);
838    }
839
840    #[test]
841    fn test_list_item_clicked_message() {
842        let msg = ListItemClicked { index: 5 };
843        assert_eq!(msg.index, 5);
844    }
845
846    #[test]
847    fn test_list_item_selected_message() {
848        let msg = ListItemSelected { index: 3 };
849        assert_eq!(msg.index, 3);
850    }
851
852    // =========================================================================
853    // Additional Coverage Tests
854    // =========================================================================
855
856    #[test]
857    fn test_list_direction_horizontal() {
858        let list = List::new().direction(ListDirection::Horizontal);
859        assert_eq!(list.direction, ListDirection::Horizontal);
860    }
861
862    #[test]
863    fn test_list_direction_is_vertical_by_default() {
864        assert_eq!(ListDirection::default(), ListDirection::Vertical);
865    }
866
867    #[test]
868    fn test_selection_mode_is_none_by_default() {
869        assert_eq!(SelectionMode::default(), SelectionMode::None);
870    }
871
872    #[test]
873    fn test_list_with_selection_mode_multiple() {
874        let list = List::new().selection_mode(SelectionMode::Multiple);
875        assert_eq!(list.selection_mode, SelectionMode::Multiple);
876    }
877
878    #[test]
879    fn test_list_with_selection_mode_single() {
880        let list = List::new().selection_mode(SelectionMode::Single);
881        assert_eq!(list.selection_mode, SelectionMode::Single);
882    }
883
884    #[test]
885    fn test_list_gap() {
886        let list = List::new().gap(10.0);
887        assert_eq!(list.gap, 10.0);
888    }
889
890    #[test]
891    fn test_list_item_height_custom() {
892        let list = List::new().item_height(60.0);
893        assert_eq!(list.item_height, Some(60.0));
894    }
895
896    #[test]
897    fn test_list_children_mut() {
898        let mut list = List::new();
899        // Children are empty when no render callback or visible items
900        assert!(list.children_mut().is_empty());
901    }
902
903    #[test]
904    fn test_list_content_size_calculated() {
905        let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
906        let list = List::new().items(items).item_height(40.0);
907        assert!(list.content_size() > 0.0);
908    }
909
910    #[test]
911    fn test_list_item_size_custom() {
912        let item = ListItem::new("Item").size(60.0);
913        assert_eq!(item.size, 60.0);
914    }
915
916    #[test]
917    fn test_list_item_selected_state() {
918        let item = ListItem::new("Item").selected(true);
919        assert!(item.selected);
920    }
921
922    #[test]
923    fn test_list_event_returns_none_when_empty() {
924        let mut list = List::new();
925        list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
926        let result = list.event(&Event::KeyDown { key: Key::Down });
927        assert!(result.is_none());
928    }
929}