1use presentar_core::{
7 widget::LayoutResult, Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas,
8 Constraints, Event, Key, Rect, Size, TypeId, Widget,
9};
10use serde::{Deserialize, Serialize};
11use std::any::Any;
12use std::ops::Range;
13use std::time::Duration;
14
15pub type RenderItemFn = Box<dyn Fn(usize, &ListItem) -> Box<dyn Widget> + Send + Sync>;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
20pub enum ListDirection {
21 #[default]
23 Vertical,
24 Horizontal,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
30pub enum SelectionMode {
31 #[default]
33 None,
34 Single,
36 Multiple,
38}
39
40#[derive(Debug, Clone)]
42pub struct ListItem {
43 pub key: String,
45 pub size: f32,
47 pub selected: bool,
49}
50
51impl ListItem {
52 #[must_use]
54 pub fn new(key: impl Into<String>) -> Self {
55 Self {
56 key: key.into(),
57 size: 48.0, selected: false,
59 }
60 }
61
62 #[must_use]
64 pub const fn size(mut self, size: f32) -> Self {
65 self.size = size;
66 self
67 }
68
69 #[must_use]
71 pub const fn selected(mut self, selected: bool) -> Self {
72 self.selected = selected;
73 self
74 }
75}
76
77#[derive(Serialize, Deserialize)]
79pub struct List {
80 pub direction: ListDirection,
82 pub selection_mode: SelectionMode,
84 pub item_height: Option<f32>,
86 pub gap: f32,
88 pub scroll_offset: f32,
90 #[serde(skip)]
92 items: Vec<ListItem>,
93 #[serde(skip)]
95 selected: Vec<usize>,
96 #[serde(skip)]
98 focused_index: Option<usize>,
99 #[serde(skip)]
101 bounds: Rect,
102 #[serde(skip)]
104 visible_range: Range<usize>,
105 #[serde(skip)]
107 item_positions: Vec<f32>,
108 #[serde(skip)]
110 content_size: f32,
111 test_id_value: Option<String>,
113 #[serde(skip)]
115 children: Vec<Box<dyn Widget>>,
116 #[serde(skip)]
118 render_item: Option<RenderItemFn>,
119}
120
121impl Default for List {
122 fn default() -> Self {
123 Self {
124 direction: ListDirection::Vertical,
125 selection_mode: SelectionMode::None,
126 item_height: Some(48.0),
127 gap: 0.0,
128 scroll_offset: 0.0,
129 items: Vec::new(),
130 selected: Vec::new(),
131 focused_index: None,
132 bounds: Rect::default(),
133 visible_range: 0..0,
134 item_positions: Vec::new(),
135 content_size: 0.0,
136 test_id_value: None,
137 children: Vec::new(),
138 render_item: None,
139 }
140 }
141}
142
143impl List {
144 #[must_use]
146 pub fn new() -> Self {
147 Self::default()
148 }
149
150 #[must_use]
152 pub const fn direction(mut self, direction: ListDirection) -> Self {
153 self.direction = direction;
154 self
155 }
156
157 #[must_use]
159 pub const fn selection_mode(mut self, mode: SelectionMode) -> Self {
160 self.selection_mode = mode;
161 self
162 }
163
164 #[must_use]
166 pub const fn item_height(mut self, height: f32) -> Self {
167 self.item_height = Some(height);
168 self
169 }
170
171 #[must_use]
173 pub const fn gap(mut self, gap: f32) -> Self {
174 self.gap = gap;
175 self
176 }
177
178 pub fn items(mut self, items: impl IntoIterator<Item = ListItem>) -> Self {
180 self.items = items.into_iter().collect();
181 self.recalculate_positions();
182 self
183 }
184
185 pub fn render_with<F>(mut self, f: F) -> Self
187 where
188 F: Fn(usize, &ListItem) -> Box<dyn Widget> + Send + Sync + 'static,
189 {
190 contract_pre_render!();
191 self.render_item = Some(Box::new(f));
192 self
193 }
194
195 #[must_use]
197 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
198 self.test_id_value = Some(id.into());
199 self
200 }
201
202 #[must_use]
204 pub fn item_count(&self) -> usize {
205 self.items.len()
206 }
207
208 #[must_use]
210 pub fn selected_indices(&self) -> &[usize] {
211 &self.selected
212 }
213
214 #[must_use]
216 pub fn visible_range(&self) -> Range<usize> {
217 self.visible_range.clone()
218 }
219
220 #[must_use]
222 pub const fn content_size(&self) -> f32 {
223 self.content_size
224 }
225
226 pub fn scroll_to(&mut self, index: usize) {
228 if index >= self.items.len() {
229 return;
230 }
231
232 let item_pos = self.item_positions.get(index).copied().unwrap_or(0.0);
233 let viewport_size = match self.direction {
234 ListDirection::Vertical => self.bounds.height,
235 ListDirection::Horizontal => self.bounds.width,
236 };
237
238 self.scroll_offset = item_pos.min(self.content_size - viewport_size).max(0.0);
240 }
241
242 pub fn scroll_into_view(&mut self, index: usize) {
244 if index >= self.items.len() {
245 return;
246 }
247
248 let item_pos = self.item_positions.get(index).copied().unwrap_or(0.0);
249 let item_size = self.get_item_size(index);
250 let viewport_size = match self.direction {
251 ListDirection::Vertical => self.bounds.height,
252 ListDirection::Horizontal => self.bounds.width,
253 };
254
255 let item_end = item_pos + item_size;
256 let viewport_end = self.scroll_offset + viewport_size;
257
258 if item_pos < self.scroll_offset {
259 self.scroll_offset = item_pos;
261 } else if item_end > viewport_end {
262 self.scroll_offset = (item_end - viewport_size).max(0.0);
264 }
265 }
266
267 pub fn select(&mut self, index: usize) {
269 match self.selection_mode {
270 SelectionMode::None => {}
271 SelectionMode::Single => {
272 self.selected.clear();
273 if index < self.items.len() {
274 self.selected.push(index);
275 self.items[index].selected = true;
276 }
277 }
278 SelectionMode::Multiple => {
279 if index < self.items.len() && !self.selected.contains(&index) {
280 self.selected.push(index);
281 self.items[index].selected = true;
282 }
283 }
284 }
285 }
286
287 pub fn deselect(&mut self, index: usize) {
289 if let Some(pos) = self.selected.iter().position(|&i| i == index) {
290 self.selected.remove(pos);
291 if index < self.items.len() {
292 self.items[index].selected = false;
293 }
294 }
295 }
296
297 pub fn toggle_selection(&mut self, index: usize) {
299 if self.selected.contains(&index) {
300 self.deselect(index);
301 } else {
302 self.select(index);
303 }
304 }
305
306 pub fn clear_selection(&mut self) {
308 for &i in &self.selected {
309 if i < self.items.len() {
310 self.items[i].selected = false;
311 }
312 }
313 self.selected.clear();
314 }
315
316 fn get_item_size(&self, index: usize) -> f32 {
318 if let Some(fixed) = self.item_height {
319 fixed
320 } else {
321 self.items.get(index).map_or(48.0, |i| i.size)
322 }
323 }
324
325 fn recalculate_positions(&mut self) {
327 self.item_positions.clear();
328 let mut pos = 0.0;
329
330 for (i, item) in self.items.iter().enumerate() {
331 self.item_positions.push(pos);
332 let size = self.item_height.unwrap_or(item.size);
333 pos += size;
334 if i < self.items.len() - 1 {
335 pos += self.gap;
336 }
337 }
338
339 self.content_size = pos;
340 }
341
342 fn calculate_visible_range(&mut self, viewport_size: f32) {
344 if self.items.is_empty() {
345 self.visible_range = 0..0;
346 return;
347 }
348
349 let start_offset = self.scroll_offset;
350 let end_offset = self.scroll_offset + viewport_size;
351
352 let first = self
354 .item_positions
355 .partition_point(|&pos| pos + self.get_item_size(0) < start_offset);
356
357 let mut last = first;
359 for i in first..self.items.len() {
360 let pos = self.item_positions.get(i).copied().unwrap_or(0.0);
361 if pos > end_offset {
362 break;
363 }
364 last = i + 1;
365 }
366
367 let buffer = 2;
369 let start = first.saturating_sub(buffer);
370 let end = (last + buffer).min(self.items.len());
371
372 self.visible_range = start..end;
373 }
374
375 fn render_visible_items(&mut self) {
377 self.children.clear();
378
379 if self.render_item.is_none() {
380 return;
381 }
382
383 for i in self.visible_range.clone() {
384 if let Some(item) = self.items.get(i) {
385 if let Some(ref render) = self.render_item {
386 let widget = render(i, item);
387 self.children.push(widget);
388 }
389 }
390 }
391 }
392}
393
394impl Widget for List {
395 fn type_id(&self) -> TypeId {
396 TypeId::of::<Self>()
397 }
398
399 fn measure(&self, constraints: Constraints) -> Size {
400 constraints.constrain(Size::new(constraints.max_width, constraints.max_height))
402 }
403
404 fn layout(&mut self, bounds: Rect) -> LayoutResult {
405 self.bounds = bounds;
406
407 let viewport_size = match self.direction {
408 ListDirection::Vertical => bounds.height,
409 ListDirection::Horizontal => bounds.width,
410 };
411
412 self.calculate_visible_range(viewport_size);
414
415 self.render_visible_items();
417
418 for (local_idx, i) in self.visible_range.clone().enumerate() {
420 if local_idx >= self.children.len() {
421 break;
422 }
423
424 let item_pos = self.item_positions.get(i).copied().unwrap_or(0.0);
425 let item_size = self.get_item_size(i);
426
427 let item_bounds = match self.direction {
428 ListDirection::Vertical => Rect::new(
429 bounds.x,
430 bounds.y + item_pos - self.scroll_offset,
431 bounds.width,
432 item_size,
433 ),
434 ListDirection::Horizontal => Rect::new(
435 bounds.x + item_pos - self.scroll_offset,
436 bounds.y,
437 item_size,
438 bounds.height,
439 ),
440 };
441
442 self.children[local_idx].layout(item_bounds);
443 }
444
445 LayoutResult {
446 size: bounds.size(),
447 }
448 }
449
450 fn paint(&self, canvas: &mut dyn Canvas) {
451 canvas.push_clip(self.bounds);
453
454 for child in &self.children {
456 child.paint(canvas);
457 }
458
459 canvas.pop_clip();
460 }
461
462 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
463 match event {
464 Event::Scroll { delta_y, .. } => {
465 let viewport_size = match self.direction {
466 ListDirection::Vertical => self.bounds.height,
467 ListDirection::Horizontal => self.bounds.width,
468 };
469
470 let max_scroll = (self.content_size - viewport_size).max(0.0);
471 self.scroll_offset = (self.scroll_offset - delta_y * 48.0).clamp(0.0, max_scroll);
472
473 self.calculate_visible_range(viewport_size);
475 self.render_visible_items();
476
477 Some(Box::new(ListScrolled {
478 offset: self.scroll_offset,
479 }))
480 }
481 Event::KeyDown { key, .. } => {
482 if let Some(focused) = self.focused_index {
483 match key {
484 Key::Up | Key::Left => {
485 if focused > 0 {
486 self.focused_index = Some(focused - 1);
487 self.scroll_into_view(focused - 1);
488 }
489 }
490 Key::Down | Key::Right => {
491 if focused < self.items.len() - 1 {
492 self.focused_index = Some(focused + 1);
493 self.scroll_into_view(focused + 1);
494 }
495 }
496 Key::Enter | Key::Space => {
497 self.toggle_selection(focused);
498 return Some(Box::new(ListItemSelected { index: focused }));
499 }
500 Key::Home => {
501 self.focused_index = Some(0);
502 self.scroll_to(0);
503 }
504 Key::End => {
505 let last = self.items.len().saturating_sub(1);
506 self.focused_index = Some(last);
507 self.scroll_to(last);
508 }
509 _ => {}
510 }
511 }
512 None
513 }
514 Event::MouseDown { position, .. } => {
515 let pos = match self.direction {
517 ListDirection::Vertical => position.y - self.bounds.y + self.scroll_offset,
518 ListDirection::Horizontal => position.x - self.bounds.x + self.scroll_offset,
519 };
520
521 for (i, &item_pos) in self.item_positions.iter().enumerate() {
522 let item_size = self.get_item_size(i);
523 if pos >= item_pos && pos < item_pos + item_size {
524 self.focused_index = Some(i);
525 self.toggle_selection(i);
526 return Some(Box::new(ListItemClicked { index: i }));
527 }
528 }
529 None
530 }
531 _ => {
532 for child in &mut self.children {
534 if let Some(msg) = child.event(event) {
535 return Some(msg);
536 }
537 }
538 None
539 }
540 }
541 }
542
543 fn children(&self) -> &[Box<dyn Widget>] {
544 &self.children
545 }
546
547 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
548 &mut self.children
549 }
550
551 fn is_focusable(&self) -> bool {
552 true
553 }
554
555 fn test_id(&self) -> Option<&str> {
556 self.test_id_value.as_deref()
557 }
558
559 fn bounds(&self) -> Rect {
560 self.bounds
561 }
562}
563
564impl Brick for List {
566 fn brick_name(&self) -> &'static str {
567 "List"
568 }
569
570 fn assertions(&self) -> &[BrickAssertion] {
571 &[BrickAssertion::MaxLatencyMs(16)]
572 }
573
574 fn budget(&self) -> BrickBudget {
575 BrickBudget::uniform(16)
576 }
577
578 fn verify(&self) -> BrickVerification {
579 BrickVerification {
580 passed: self.assertions().to_vec(),
581 failed: vec![],
582 verification_time: Duration::from_micros(10),
583 }
584 }
585
586 fn to_html(&self) -> String {
587 r#"<div class="brick-list"></div>"#.to_string()
588 }
589
590 fn to_css(&self) -> String {
591 ".brick-list { display: block; overflow: auto; }".to_string()
592 }
593
594 fn test_id(&self) -> Option<&str> {
595 self.test_id_value.as_deref()
596 }
597}
598
599#[derive(Debug, Clone)]
601pub struct ListScrolled {
602 pub offset: f32,
604}
605
606#[derive(Debug, Clone)]
608pub struct ListItemClicked {
609 pub index: usize,
611}
612
613#[derive(Debug, Clone)]
615pub struct ListItemSelected {
616 pub index: usize,
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
629 fn test_list_direction_default() {
630 assert_eq!(ListDirection::default(), ListDirection::Vertical);
631 }
632
633 #[test]
638 fn test_selection_mode_default() {
639 assert_eq!(SelectionMode::default(), SelectionMode::None);
640 }
641
642 #[test]
647 fn test_list_item_new() {
648 let item = ListItem::new("item-1");
649 assert_eq!(item.key, "item-1");
650 assert_eq!(item.size, 48.0);
651 assert!(!item.selected);
652 }
653
654 #[test]
655 fn test_list_item_builder() {
656 let item = ListItem::new("item-1").size(64.0).selected(true);
657 assert_eq!(item.size, 64.0);
658 assert!(item.selected);
659 }
660
661 #[test]
666 fn test_list_new() {
667 let list = List::new();
668 assert_eq!(list.direction, ListDirection::Vertical);
669 assert_eq!(list.selection_mode, SelectionMode::None);
670 assert_eq!(list.item_height, Some(48.0));
671 assert_eq!(list.gap, 0.0);
672 assert_eq!(list.item_count(), 0);
673 }
674
675 #[test]
676 fn test_list_builder() {
677 let list = List::new()
678 .direction(ListDirection::Horizontal)
679 .selection_mode(SelectionMode::Single)
680 .item_height(32.0)
681 .gap(8.0);
682
683 assert_eq!(list.direction, ListDirection::Horizontal);
684 assert_eq!(list.selection_mode, SelectionMode::Single);
685 assert_eq!(list.item_height, Some(32.0));
686 assert_eq!(list.gap, 8.0);
687 }
688
689 #[test]
690 fn test_list_items() {
691 let items = vec![ListItem::new("1"), ListItem::new("2"), ListItem::new("3")];
692 let list = List::new().items(items);
693 assert_eq!(list.item_count(), 3);
694 }
695
696 #[test]
697 fn test_list_content_size() {
698 let items = vec![ListItem::new("1"), ListItem::new("2"), ListItem::new("3")];
699 let list = List::new().item_height(50.0).gap(10.0).items(items);
700 assert_eq!(list.content_size(), 170.0);
702 }
703
704 #[test]
705 fn test_list_content_size_variable_height() {
706 let items = vec![
707 ListItem::new("1").size(30.0),
708 ListItem::new("2").size(40.0),
709 ListItem::new("3").size(50.0),
710 ];
711 let mut list = List::new().gap(5.0);
712 list.item_height = None; list = list.items(items);
714 assert_eq!(list.content_size(), 130.0);
716 }
717
718 #[test]
719 fn test_list_select_single() {
720 let items = vec![ListItem::new("1"), ListItem::new("2")];
721 let mut list = List::new()
722 .selection_mode(SelectionMode::Single)
723 .items(items);
724
725 list.select(0);
726 assert_eq!(list.selected_indices(), &[0]);
727
728 list.select(1);
729 assert_eq!(list.selected_indices(), &[1]); }
731
732 #[test]
733 fn test_list_select_multiple() {
734 let items = vec![ListItem::new("1"), ListItem::new("2")];
735 let mut list = List::new()
736 .selection_mode(SelectionMode::Multiple)
737 .items(items);
738
739 list.select(0);
740 list.select(1);
741 assert_eq!(list.selected_indices(), &[0, 1]);
742 }
743
744 #[test]
745 fn test_list_deselect() {
746 let items = vec![ListItem::new("1"), ListItem::new("2")];
747 let mut list = List::new()
748 .selection_mode(SelectionMode::Multiple)
749 .items(items);
750
751 list.select(0);
752 list.select(1);
753 list.deselect(0);
754 assert_eq!(list.selected_indices(), &[1]);
755 }
756
757 #[test]
758 fn test_list_toggle_selection() {
759 let items = vec![ListItem::new("1")];
760 let mut list = List::new()
761 .selection_mode(SelectionMode::Single)
762 .items(items);
763
764 list.toggle_selection(0);
765 assert_eq!(list.selected_indices(), &[0]);
766
767 list.toggle_selection(0);
768 assert!(list.selected_indices().is_empty());
769 }
770
771 #[test]
772 fn test_list_clear_selection() {
773 let items = vec![ListItem::new("1"), ListItem::new("2")];
774 let mut list = List::new()
775 .selection_mode(SelectionMode::Multiple)
776 .items(items);
777
778 list.select(0);
779 list.select(1);
780 list.clear_selection();
781 assert!(list.selected_indices().is_empty());
782 }
783
784 #[test]
785 fn test_list_scroll_to() {
786 let items: Vec<_> = (0..100).map(|i| ListItem::new(format!("{i}"))).collect();
787 let mut list = List::new().item_height(50.0).items(items);
788 list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
789
790 list.scroll_to(10);
791 assert_eq!(list.scroll_offset, 500.0); }
793
794 #[test]
795 fn test_list_scroll_into_view() {
796 let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
797 let mut list = List::new().item_height(50.0).items(items);
798 list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
799 list.scroll_offset = 0.0;
800
801 list.scroll_into_view(5);
803 assert_eq!(list.scroll_offset, 100.0);
805 }
806
807 #[test]
808 fn test_list_visible_range() {
809 let items: Vec<_> = (0..100).map(|i| ListItem::new(format!("{i}"))).collect();
810 let mut list = List::new().item_height(50.0).items(items);
811 list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
812 list.scroll_offset = 0.0;
813
814 list.calculate_visible_range(200.0);
815
816 let range = list.visible_range();
819 assert!(range.start <= 4);
820 assert!(range.end >= 4);
821 }
822
823 #[test]
824 fn test_list_measure() {
825 let list = List::new();
826 let size = list.measure(Constraints::loose(Size::new(300.0, 400.0)));
827 assert_eq!(size, Size::new(300.0, 400.0));
828 }
829
830 #[test]
831 fn test_list_layout() {
832 let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
833 let mut list = List::new().item_height(50.0).items(items);
834
835 let result = list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
836 assert_eq!(result.size, Size::new(300.0, 200.0));
837 assert_eq!(list.bounds, Rect::new(0.0, 0.0, 300.0, 200.0));
838 }
839
840 #[test]
841 fn test_list_type_id() {
842 let list = List::new();
843 assert_eq!(Widget::type_id(&list), TypeId::of::<List>());
844 }
845
846 #[test]
847 fn test_list_is_focusable() {
848 let list = List::new();
849 assert!(list.is_focusable());
850 }
851
852 #[test]
853 fn test_list_test_id() {
854 let list = List::new().with_test_id("my-list");
855 assert_eq!(Widget::test_id(&list), Some("my-list"));
856 }
857
858 #[test]
859 fn test_list_children_empty() {
860 let list = List::new();
861 assert!(list.children().is_empty());
862 }
863
864 #[test]
865 fn test_list_bounds() {
866 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
867 let mut list = List::new().items(items);
868 list.layout(Rect::new(10.0, 20.0, 300.0, 200.0));
869 assert_eq!(list.bounds(), Rect::new(10.0, 20.0, 300.0, 200.0));
870 }
871
872 #[test]
873 fn test_list_scrolled_message() {
874 let msg = ListScrolled { offset: 100.0 };
875 assert_eq!(msg.offset, 100.0);
876 }
877
878 #[test]
879 fn test_list_item_clicked_message() {
880 let msg = ListItemClicked { index: 5 };
881 assert_eq!(msg.index, 5);
882 }
883
884 #[test]
885 fn test_list_item_selected_message() {
886 let msg = ListItemSelected { index: 3 };
887 assert_eq!(msg.index, 3);
888 }
889
890 #[test]
895 fn test_list_direction_horizontal() {
896 let list = List::new().direction(ListDirection::Horizontal);
897 assert_eq!(list.direction, ListDirection::Horizontal);
898 }
899
900 #[test]
901 fn test_list_direction_is_vertical_by_default() {
902 assert_eq!(ListDirection::default(), ListDirection::Vertical);
903 }
904
905 #[test]
906 fn test_selection_mode_is_none_by_default() {
907 assert_eq!(SelectionMode::default(), SelectionMode::None);
908 }
909
910 #[test]
911 fn test_list_with_selection_mode_multiple() {
912 let list = List::new().selection_mode(SelectionMode::Multiple);
913 assert_eq!(list.selection_mode, SelectionMode::Multiple);
914 }
915
916 #[test]
917 fn test_list_with_selection_mode_single() {
918 let list = List::new().selection_mode(SelectionMode::Single);
919 assert_eq!(list.selection_mode, SelectionMode::Single);
920 }
921
922 #[test]
923 fn test_list_gap() {
924 let list = List::new().gap(10.0);
925 assert_eq!(list.gap, 10.0);
926 }
927
928 #[test]
929 fn test_list_item_height_custom() {
930 let list = List::new().item_height(60.0);
931 assert_eq!(list.item_height, Some(60.0));
932 }
933
934 #[test]
935 fn test_list_children_mut() {
936 let mut list = List::new();
937 assert!(list.children_mut().is_empty());
939 }
940
941 #[test]
942 fn test_list_content_size_calculated() {
943 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
944 let list = List::new().items(items).item_height(40.0);
945 assert!(list.content_size() > 0.0);
946 }
947
948 #[test]
949 fn test_list_item_size_custom() {
950 let item = ListItem::new("Item").size(60.0);
951 assert_eq!(item.size, 60.0);
952 }
953
954 #[test]
955 fn test_list_item_selected_state() {
956 let item = ListItem::new("Item").selected(true);
957 assert!(item.selected);
958 }
959
960 #[test]
961 fn test_list_event_returns_none_when_empty() {
962 let mut list = List::new();
963 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
964 let result = list.event(&Event::key_down(Key::Down));
965 assert!(result.is_none());
966 }
967
968 #[test]
973 fn test_list_scroll_event() {
974 let items: Vec<_> = (0..20).map(|i| ListItem::new(format!("{i}"))).collect();
975 let mut list = List::new().item_height(50.0).items(items);
976 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
977
978 let result = list.event(&Event::Scroll {
980 delta_x: 0.0,
981 delta_y: -2.0,
982 });
983 assert!(result.is_some());
984 assert!(list.scroll_offset > 0.0);
985 }
986
987 #[test]
988 fn test_list_scroll_event_clamp() {
989 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
990 let mut list = List::new().item_height(50.0).items(items);
991 list.layout(Rect::new(0.0, 0.0, 300.0, 500.0)); let _ = list.event(&Event::Scroll {
995 delta_x: 0.0,
996 delta_y: -10.0,
997 });
998 assert_eq!(list.scroll_offset, 0.0);
1000 }
1001
1002 #[test]
1003 fn test_list_key_down_focused() {
1004 let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
1005 let mut list = List::new()
1006 .selection_mode(SelectionMode::Single)
1007 .item_height(50.0)
1008 .items(items);
1009 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1010 list.focused_index = Some(5);
1011
1012 let _ = list.event(&Event::key_down(Key::Down));
1014 assert_eq!(list.focused_index, Some(6));
1015
1016 let _ = list.event(&Event::key_down(Key::Up));
1018 assert_eq!(list.focused_index, Some(5));
1019 }
1020
1021 #[test]
1022 fn test_list_key_left_right() {
1023 let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
1024 let mut list = List::new()
1025 .direction(ListDirection::Horizontal)
1026 .selection_mode(SelectionMode::Single)
1027 .item_height(50.0)
1028 .items(items);
1029 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1030 list.focused_index = Some(5);
1031
1032 let _ = list.event(&Event::key_down(Key::Right));
1034 assert_eq!(list.focused_index, Some(6));
1035
1036 let _ = list.event(&Event::key_down(Key::Left));
1038 assert_eq!(list.focused_index, Some(5));
1039 }
1040
1041 #[test]
1042 fn test_list_key_home_end() {
1043 let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
1044 let mut list = List::new()
1045 .selection_mode(SelectionMode::Single)
1046 .item_height(50.0)
1047 .items(items);
1048 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1049 list.focused_index = Some(5);
1050
1051 let _ = list.event(&Event::key_down(Key::Home));
1053 assert_eq!(list.focused_index, Some(0));
1054
1055 let _ = list.event(&Event::key_down(Key::End));
1057 assert_eq!(list.focused_index, Some(9));
1058 }
1059
1060 #[test]
1061 fn test_list_key_enter_selects() {
1062 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1063 let mut list = List::new()
1064 .selection_mode(SelectionMode::Single)
1065 .item_height(50.0)
1066 .items(items);
1067 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1068 list.focused_index = Some(2);
1069
1070 let result = list.event(&Event::key_down(Key::Enter));
1071 assert!(result.is_some());
1072 assert_eq!(list.selected_indices(), &[2]);
1073 }
1074
1075 #[test]
1076 fn test_list_key_space_selects() {
1077 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1078 let mut list = List::new()
1079 .selection_mode(SelectionMode::Single)
1080 .item_height(50.0)
1081 .items(items);
1082 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1083 list.focused_index = Some(3);
1084
1085 let result = list.event(&Event::key_down(Key::Space));
1086 assert!(result.is_some());
1087 assert_eq!(list.selected_indices(), &[3]);
1088 }
1089
1090 #[test]
1091 fn test_list_mouse_down_click() {
1092 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1093 let mut list = List::new()
1094 .selection_mode(SelectionMode::Single)
1095 .item_height(50.0)
1096 .items(items);
1097 list.layout(Rect::new(0.0, 0.0, 300.0, 300.0));
1098
1099 let result = list.event(&Event::MouseDown {
1101 position: presentar_core::Point::new(150.0, 75.0),
1102 button: presentar_core::MouseButton::Left,
1103 });
1104 assert!(result.is_some());
1105 assert_eq!(list.focused_index, Some(1));
1106 }
1107
1108 #[test]
1109 fn test_list_mouse_down_horizontal() {
1110 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1111 let mut list = List::new()
1112 .direction(ListDirection::Horizontal)
1113 .selection_mode(SelectionMode::Single)
1114 .item_height(50.0)
1115 .items(items);
1116 list.layout(Rect::new(0.0, 0.0, 300.0, 100.0));
1117
1118 let result = list.event(&Event::MouseDown {
1120 position: presentar_core::Point::new(75.0, 50.0),
1121 button: presentar_core::MouseButton::Left,
1122 });
1123 assert!(result.is_some());
1124 assert_eq!(list.focused_index, Some(1));
1125 }
1126
1127 #[test]
1128 fn test_list_mouse_down_miss() {
1129 let items: Vec<_> = (0..2).map(|i| ListItem::new(format!("{i}"))).collect();
1130 let mut list = List::new()
1131 .selection_mode(SelectionMode::Single)
1132 .item_height(50.0)
1133 .items(items);
1134 list.layout(Rect::new(0.0, 0.0, 300.0, 300.0));
1135
1136 let result = list.event(&Event::MouseDown {
1138 position: presentar_core::Point::new(150.0, 200.0),
1139 button: presentar_core::MouseButton::Left,
1140 });
1141 assert!(result.is_none());
1142 }
1143
1144 #[test]
1145 fn test_list_other_event() {
1146 let mut list = List::new();
1147 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1148
1149 let result = list.event(&Event::MouseMove {
1151 position: presentar_core::Point::new(100.0, 100.0),
1152 });
1153 assert!(result.is_none());
1154 }
1155
1156 use presentar_core::RecordingCanvas;
1161
1162 #[test]
1163 fn test_list_paint_empty() {
1164 let list = List::new();
1165 let mut canvas = RecordingCanvas::new();
1166 list.paint(&mut canvas);
1168 }
1169
1170 #[test]
1171 fn test_list_paint_with_items() {
1172 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1173 let mut list = List::new().item_height(50.0).items(items);
1174 list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
1175
1176 let mut canvas = RecordingCanvas::new();
1177 list.paint(&mut canvas);
1179 }
1180
1181 #[test]
1186 fn test_list_scroll_to_out_of_bounds() {
1187 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1188 let mut list = List::new().item_height(50.0).items(items);
1189 list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
1190
1191 list.scroll_to(100);
1193 assert_eq!(list.scroll_offset, 0.0);
1195 }
1196
1197 #[test]
1198 fn test_list_scroll_into_view_out_of_bounds() {
1199 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1200 let mut list = List::new().item_height(50.0).items(items);
1201 list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
1202
1203 list.scroll_into_view(100);
1205 assert_eq!(list.scroll_offset, 0.0);
1207 }
1208
1209 #[test]
1210 fn test_list_scroll_into_view_item_above() {
1211 let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
1212 let mut list = List::new().item_height(50.0).items(items);
1213 list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
1214 list.scroll_offset = 200.0; list.scroll_into_view(0);
1218 assert_eq!(list.scroll_offset, 0.0);
1219 }
1220
1221 #[test]
1222 fn test_list_select_none_mode() {
1223 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1224 let mut list = List::new().selection_mode(SelectionMode::None).items(items);
1225
1226 list.select(0);
1227 assert!(list.selected_indices().is_empty());
1228 }
1229
1230 #[test]
1231 fn test_list_select_out_of_bounds() {
1232 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1233 let mut list = List::new()
1234 .selection_mode(SelectionMode::Single)
1235 .items(items);
1236
1237 list.select(100);
1238 assert!(list.selected_indices().is_empty());
1239 }
1240
1241 #[test]
1242 fn test_list_select_multiple_same_item() {
1243 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1244 let mut list = List::new()
1245 .selection_mode(SelectionMode::Multiple)
1246 .items(items);
1247
1248 list.select(0);
1249 list.select(0); assert_eq!(list.selected_indices().len(), 1);
1251 }
1252
1253 #[test]
1254 fn test_list_deselect_nonexistent() {
1255 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1256 let mut list = List::new()
1257 .selection_mode(SelectionMode::Multiple)
1258 .items(items);
1259
1260 list.select(0);
1261 list.deselect(1); assert_eq!(list.selected_indices(), &[0]);
1263 }
1264
1265 #[test]
1266 fn test_list_clear_selection_with_invalid_indices() {
1267 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1268 let mut list = List::new()
1269 .selection_mode(SelectionMode::Multiple)
1270 .items(items);
1271
1272 list.select(0);
1273 list.selected.push(100); list.clear_selection();
1275 assert!(list.selected_indices().is_empty());
1276 }
1277
1278 #[test]
1279 fn test_list_horizontal_layout() {
1280 let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
1281 let mut list = List::new()
1282 .direction(ListDirection::Horizontal)
1283 .item_height(50.0)
1284 .items(items);
1285
1286 let result = list.layout(Rect::new(0.0, 0.0, 300.0, 100.0));
1287 assert_eq!(result.size, Size::new(300.0, 100.0));
1288 }
1289
1290 #[test]
1291 fn test_list_horizontal_scroll() {
1292 let items: Vec<_> = (0..20).map(|i| ListItem::new(format!("{i}"))).collect();
1293 let mut list = List::new()
1294 .direction(ListDirection::Horizontal)
1295 .item_height(50.0)
1296 .items(items);
1297 list.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
1298
1299 list.scroll_to(10);
1300 assert_eq!(list.scroll_offset, 500.0);
1301 }
1302
1303 #[test]
1304 fn test_list_horizontal_scroll_into_view() {
1305 let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
1306 let mut list = List::new()
1307 .direction(ListDirection::Horizontal)
1308 .item_height(50.0)
1309 .items(items);
1310 list.bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
1311 list.scroll_offset = 0.0;
1312
1313 list.scroll_into_view(5);
1314 assert!(list.scroll_offset > 0.0);
1315 }
1316
1317 #[test]
1318 fn test_list_visible_range_empty() {
1319 let mut list = List::new();
1320 list.calculate_visible_range(200.0);
1321 assert_eq!(list.visible_range(), 0..0);
1322 }
1323
1324 #[test]
1325 fn test_list_get_item_size_variable() {
1326 let items = vec![ListItem::new("1").size(30.0), ListItem::new("2").size(50.0)];
1327 let mut list = List::new();
1328 list.item_height = None;
1329 list = list.items(items);
1330
1331 assert_eq!(list.content_size(), 80.0);
1333 }
1334
1335 #[test]
1336 fn test_list_key_boundary_checks() {
1337 let items: Vec<_> = (0..3).map(|i| ListItem::new(format!("{i}"))).collect();
1338 let mut list = List::new()
1339 .selection_mode(SelectionMode::Single)
1340 .item_height(50.0)
1341 .items(items);
1342 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1343
1344 list.focused_index = Some(0);
1346 let _ = list.event(&Event::key_down(Key::Up));
1347 assert_eq!(list.focused_index, Some(0)); list.focused_index = Some(2);
1351 let _ = list.event(&Event::key_down(Key::Down));
1352 assert_eq!(list.focused_index, Some(2)); }
1354
1355 #[test]
1356 fn test_list_other_key_no_action() {
1357 let items: Vec<_> = (0..3).map(|i| ListItem::new(format!("{i}"))).collect();
1358 let mut list = List::new()
1359 .selection_mode(SelectionMode::Single)
1360 .item_height(50.0)
1361 .items(items);
1362 list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
1363 list.focused_index = Some(1);
1364
1365 let result = list.event(&Event::key_down(Key::Tab));
1367 assert!(result.is_none());
1368 assert_eq!(list.focused_index, Some(1));
1369 }
1370
1371 #[test]
1376 fn test_list_brick_name() {
1377 let list = List::new();
1378 assert_eq!(list.brick_name(), "List");
1379 }
1380
1381 #[test]
1382 fn test_list_brick_assertions() {
1383 let list = List::new();
1384 let assertions = list.assertions();
1385 assert!(!assertions.is_empty());
1386 }
1387
1388 #[test]
1389 fn test_list_brick_budget() {
1390 let list = List::new();
1391 let budget = list.budget();
1392 assert!(budget.layout_ms > 0);
1393 }
1394
1395 #[test]
1396 fn test_list_brick_verify() {
1397 let list = List::new();
1398 let verification = list.verify();
1399 assert!(!verification.passed.is_empty());
1400 assert!(verification.failed.is_empty());
1401 }
1402
1403 #[test]
1404 fn test_list_brick_to_html() {
1405 let list = List::new();
1406 let html = list.to_html();
1407 assert!(html.contains("brick-list"));
1408 }
1409
1410 #[test]
1411 fn test_list_brick_to_css() {
1412 let list = List::new();
1413 let css = list.to_css();
1414 assert!(css.contains("brick-list"));
1415 }
1416
1417 #[test]
1418 fn test_list_brick_test_id() {
1419 let list = List::new().with_test_id("test-list");
1420 assert_eq!(Brick::test_id(&list), Some("test-list"));
1421 }
1422}