1use 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
13pub type RenderItemFn = Box<dyn Fn(usize, &ListItem) -> Box<dyn Widget> + Send + Sync>;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
18pub enum ListDirection {
19 #[default]
21 Vertical,
22 Horizontal,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
28pub enum SelectionMode {
29 #[default]
31 None,
32 Single,
34 Multiple,
36}
37
38#[derive(Debug, Clone)]
40pub struct ListItem {
41 pub key: String,
43 pub size: f32,
45 pub selected: bool,
47}
48
49impl ListItem {
50 #[must_use]
52 pub fn new(key: impl Into<String>) -> Self {
53 Self {
54 key: key.into(),
55 size: 48.0, selected: false,
57 }
58 }
59
60 #[must_use]
62 pub const fn size(mut self, size: f32) -> Self {
63 self.size = size;
64 self
65 }
66
67 #[must_use]
69 pub const fn selected(mut self, selected: bool) -> Self {
70 self.selected = selected;
71 self
72 }
73}
74
75#[derive(Serialize, Deserialize)]
77pub struct List {
78 pub direction: ListDirection,
80 pub selection_mode: SelectionMode,
82 pub item_height: Option<f32>,
84 pub gap: f32,
86 pub scroll_offset: f32,
88 #[serde(skip)]
90 items: Vec<ListItem>,
91 #[serde(skip)]
93 selected: Vec<usize>,
94 #[serde(skip)]
96 focused_index: Option<usize>,
97 #[serde(skip)]
99 bounds: Rect,
100 #[serde(skip)]
102 visible_range: Range<usize>,
103 #[serde(skip)]
105 item_positions: Vec<f32>,
106 #[serde(skip)]
108 content_size: f32,
109 test_id_value: Option<String>,
111 #[serde(skip)]
113 children: Vec<Box<dyn Widget>>,
114 #[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 #[must_use]
144 pub fn new() -> Self {
145 Self::default()
146 }
147
148 #[must_use]
150 pub const fn direction(mut self, direction: ListDirection) -> Self {
151 self.direction = direction;
152 self
153 }
154
155 #[must_use]
157 pub const fn selection_mode(mut self, mode: SelectionMode) -> Self {
158 self.selection_mode = mode;
159 self
160 }
161
162 #[must_use]
164 pub const fn item_height(mut self, height: f32) -> Self {
165 self.item_height = Some(height);
166 self
167 }
168
169 #[must_use]
171 pub const fn gap(mut self, gap: f32) -> Self {
172 self.gap = gap;
173 self
174 }
175
176 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 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 #[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 #[must_use]
201 pub fn item_count(&self) -> usize {
202 self.items.len()
203 }
204
205 #[must_use]
207 pub fn selected_indices(&self) -> &[usize] {
208 &self.selected
209 }
210
211 #[must_use]
213 pub fn visible_range(&self) -> Range<usize> {
214 self.visible_range.clone()
215 }
216
217 #[must_use]
219 pub const fn content_size(&self) -> f32 {
220 self.content_size
221 }
222
223 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 self.scroll_offset = item_pos.min(self.content_size - viewport_size).max(0.0);
237 }
238
239 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 self.scroll_offset = item_pos;
258 } else if item_end > viewport_end {
259 self.scroll_offset = (item_end - viewport_size).max(0.0);
261 }
262 }
263
264 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 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 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 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 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 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 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 let first = self
351 .item_positions
352 .partition_point(|&pos| pos + self.get_item_size(0) < start_offset);
353
354 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 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 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 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 self.calculate_visible_range(viewport_size);
411
412 self.render_visible_items();
414
415 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 canvas.push_clip(self.bounds);
450
451 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 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 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 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#[derive(Debug, Clone)]
563pub struct ListScrolled {
564 pub offset: f32,
566}
567
568#[derive(Debug, Clone)]
570pub struct ListItemClicked {
571 pub index: usize,
573}
574
575#[derive(Debug, Clone)]
577pub struct ListItemSelected {
578 pub index: usize,
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
591 fn test_list_direction_default() {
592 assert_eq!(ListDirection::default(), ListDirection::Vertical);
593 }
594
595 #[test]
600 fn test_selection_mode_default() {
601 assert_eq!(SelectionMode::default(), SelectionMode::None);
602 }
603
604 #[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 #[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 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; list = list.items(items);
676 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]); }
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); }
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 list.scroll_into_view(5);
765 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 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 #[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 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}