1use std::ops::{Deref, Range};
2
3use crate::prelude::*;
4
5#[derive(Lens)]
7pub struct VirtualList {
8 scroll_to_cursor: bool,
9 on_change: Option<Box<dyn Fn(&mut EventContext, Range<usize>)>>,
10}
11
12pub(crate) enum VirtualListEvent {
13 SetScrollY(f32),
14}
15
16#[derive(Lens)]
17struct VirtualListData {
18 num_items: usize,
19 item_height: f32,
20 visible_range: Range<usize>,
21 scroll_y: f32,
22}
23
24impl VirtualListData {
25 fn evaluate_index(index: usize, start: usize, end: usize) -> usize {
26 match end - start {
27 0 => 0,
28 len => start + (len - (start % len) + index) % len,
29 }
30 }
31
32 fn visible_item_index(index: usize) -> impl Lens<Target = usize> {
33 Self::visible_range.map(move |range| Self::evaluate_index(index, range.start, range.end))
34 }
35
36 fn recalc(&mut self, cx: &mut EventContext) {
37 if self.num_items == 0 {
38 self.visible_range = 0..0;
39 return;
40 }
41
42 let current = cx.current();
43 let current_height = cx.cache.get_height(current);
44 if current_height == f32::MAX {
45 return;
46 }
47
48 let item_height = self.item_height;
49 let total_height = item_height * (self.num_items as f32);
50 let visible_height = current_height / cx.scale_factor();
51
52 let mut num_visible_items = (visible_height / item_height).ceil();
53 num_visible_items += 1.0; let visible_items_height = item_height * num_visible_items;
56 let empty_height = (total_height - visible_items_height).max(0.0);
57
58 let visible_start = empty_height * self.scroll_y;
60 let visible_end = visible_start + visible_items_height;
61
62 let start_index = (visible_start / item_height).trunc() as usize;
64 let end_index = 1 + (visible_end / item_height).trunc() as usize;
65
66 self.visible_range = start_index..end_index.min(self.num_items);
67 }
68}
69
70impl Model for VirtualListData {
71 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
72 event.map(|virtual_list_event, _| match virtual_list_event {
73 VirtualListEvent::SetScrollY(scroll_y) => {
74 self.scroll_y = *scroll_y;
75 self.recalc(cx);
76 }
77 });
78
79 event.map(|window_event, _| match window_event {
80 WindowEvent::GeometryChanged(geo) => {
81 if geo.intersects(GeoChanged::WIDTH_CHANGED | GeoChanged::HEIGHT_CHANGED) {
82 self.recalc(cx);
83 }
84 }
85
86 _ => {}
87 });
88 }
89}
90
91impl VirtualList {
92 pub fn new<V: View, L: Lens, T: 'static>(
94 cx: &mut Context,
95 list: L,
96 item_height: f32,
97 item_content: impl 'static + Copy + Fn(&mut Context, usize, MapRef<L, T>) -> Handle<V>,
98 ) -> Handle<Self>
99 where
100 L::Target: Deref<Target = [T]>,
101 {
102 Self::new_generic(
103 cx,
104 list,
105 |list| list.len(),
106 |list, index| &list[index],
107 item_height,
108 item_content,
109 )
110 }
111
112 pub fn new_generic<V: View, L: Lens, T: 'static>(
114 cx: &mut Context,
115 list: L,
116 list_len: impl 'static + Fn(&L::Target) -> usize,
117 list_index: impl 'static + Copy + Fn(&L::Target, usize) -> &T,
118 item_height: f32,
119 item_content: impl 'static + Copy + Fn(&mut Context, usize, MapRef<L, T>) -> Handle<V>,
120 ) -> Handle<Self> {
121 let vl = cx.current;
122 let num_items = list.map(list_len);
123 Self { scroll_to_cursor: true, on_change: None }.build(cx, |cx| {
124 Binding::new(cx, num_items, move |cx, lens| {
125 let num_items = lens.get(cx);
126
127 let mut data =
128 VirtualListData { num_items, item_height, visible_range: 0..0, scroll_y: 0.0 };
129 data.recalc(&mut EventContext::new_with_current(cx, vl));
130 data.build(cx);
131 });
132
133 ScrollView::new(cx, move |cx| {
134 Binding::new(cx, num_items, move |cx, lens| {
135 let num_items = lens.get(cx);
136 cx.emit(ScrollEvent::SetY(0.0));
137 VStack::new(cx, |cx| {
140 let num_visible_items = VirtualListData::visible_range.map(Range::len);
143 Binding::new(cx, num_visible_items, move |cx, lens| {
144 for i in 0..lens.get(cx).min(num_items) {
145 let item_index = VirtualListData::visible_item_index(i);
149 Binding::new(cx, item_index, move |cx, lens| {
150 let index = lens.get(cx);
151 HStack::new(cx, move |cx| {
152 let item =
153 list.map_ref(move |list| list_index(list, index));
154 item_content(cx, index, item).height(Percentage(100.0));
155 })
156 .height(Pixels(item_height))
157 .position_type(PositionType::Absolute)
158 .bind(
159 item_index,
160 move |handle, lens| {
161 let index = lens.get(&handle);
162 handle.top(Pixels(index as f32 * item_height));
163 },
164 );
165 });
166 }
167 })
168 })
169 .height(Pixels(num_items as f32 * item_height));
170 })
171 })
172 .show_horizontal_scrollbar(false)
173 .scroll_to_cursor(true)
174 .on_scroll(|cx, _, y| {
175 if y.is_finite() {
176 cx.emit(VirtualListEvent::SetScrollY(y));
177 }
178 });
179 })
180 }
181}
182
183impl View for VirtualList {
184 fn element(&self) -> Option<&'static str> {
185 Some("virtual-list")
186 }
187}
188
189impl Handle<'_, VirtualList> {
190 pub fn scroll_to_cursor(self, flag: bool) -> Self {
192 self.modify(|virtual_list: &mut VirtualList| {
193 virtual_list.scroll_to_cursor = flag;
194 })
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn evaluate_indices(range: Range<usize>) -> Vec<usize> {
203 (0..range.len())
204 .map(|index| VirtualListData::evaluate_index(index, range.start, range.end))
205 .collect()
206 }
207
208 #[test]
209 fn test_evaluate_index() {
210 assert_eq!(evaluate_indices(0..4), [0, 1, 2, 3]);
212 assert_eq!(evaluate_indices(1..5), [4, 1, 2, 3]);
214 assert_eq!(evaluate_indices(2..6), [4, 5, 2, 3]);
216 assert_eq!(evaluate_indices(3..7), [4, 5, 6, 3]);
218 assert_eq!(evaluate_indices(4..8), [4, 5, 6, 7]);
220 assert_eq!(evaluate_indices(5..9), [8, 5, 6, 7]);
222 assert_eq!(evaluate_indices(6..10), [8, 9, 6, 7]);
224 assert_eq!(evaluate_indices(7..11), [8, 9, 10, 7]);
226 assert_eq!(evaluate_indices(8..12), [8, 9, 10, 11]);
228 assert_eq!(evaluate_indices(9..13), [12, 9, 10, 11]);
230 }
231}