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