1use std::{
2 collections::BTreeSet,
3 ops::{Deref, Range},
4};
5
6use crate::prelude::*;
7
8#[derive(Lens)]
10pub struct VirtualList {
11 scroll_to_cursor: bool,
13 on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
15 num_items: usize,
17 item_height: f32,
19 visible_range: Range<usize>,
21 scroll_x: f32,
23 scroll_y: f32,
25 show_horizontal_scrollbar: bool,
27 show_vertical_scrollbar: bool,
29 selected: BTreeSet<usize>,
31 selectable: Selectable,
33 focused: Option<usize>,
35 selection_follows_focus: bool,
37 #[lens(ignore)]
39 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
40}
41
42impl VirtualList {
43 fn evaluate_index(index: usize, start: usize, end: usize) -> usize {
44 match end - start {
45 0 => 0,
46 len => start + (len - (start % len) + index) % len,
47 }
48 }
49
50 fn visible_item_index(index: usize) -> impl Lens<Target = usize> {
51 Self::visible_range.map(move |range| Self::evaluate_index(index, range.start, range.end))
52 }
53
54 fn recalc(&mut self, cx: &mut EventContext) {
55 if self.num_items == 0 {
56 self.visible_range = 0..0;
57 return;
58 }
59
60 let current = cx.current();
61 let current_height = cx.cache.get_height(current);
62 if current_height == f32::MAX {
63 return;
64 }
65
66 let item_height = self.item_height;
67 let total_height = item_height * (self.num_items as f32);
68 let visible_height = current_height / cx.scale_factor();
69
70 let mut num_visible_items = (visible_height / item_height).ceil();
71 num_visible_items += 1.0; let visible_items_height = item_height * num_visible_items;
74 let empty_height = (total_height - visible_items_height).max(0.0);
75
76 let visible_start = empty_height * self.scroll_y;
78 let visible_end = visible_start + visible_items_height;
79
80 let mut start_index = (visible_start / item_height).trunc() as usize;
82 let mut end_index = 1 + (visible_end / item_height).trunc() as usize;
83
84 let desired_range_size = (num_visible_items as usize) + 1;
86 end_index = end_index.min(self.num_items);
87
88 let current_range_size = end_index.saturating_sub(start_index);
89
90 if current_range_size < desired_range_size {
91 match end_index == self.num_items {
92 true => {
94 start_index =
95 start_index.saturating_sub(desired_range_size - current_range_size);
96 }
97 false if end_index < self.num_items => {
99 end_index = (start_index + desired_range_size).min(self.num_items);
100 }
101 _ => {}
102 }
103 }
104
105 self.visible_range = start_index..end_index;
106 }
107}
108
109impl VirtualList {
110 pub fn new<V: View, L: Lens, T: 'static>(
112 cx: &mut Context,
113 list: L,
114 item_height: f32,
115 item_content: impl 'static + Copy + Fn(&mut Context, usize, MapRef<L, T>) -> Handle<V>,
116 ) -> Handle<Self>
117 where
118 L::Target: Deref<Target = [T]>,
119 {
120 Self::new_generic(
121 cx,
122 list,
123 |list| list.len(),
124 |list, index| &list[index],
125 item_height,
126 item_content,
127 )
128 }
129
130 pub fn new_generic<V: View, L: Lens, T: 'static>(
132 cx: &mut Context,
133 list: L,
134 list_len: impl 'static + Fn(&L::Target) -> usize,
135 list_index: impl 'static + Copy + Fn(&L::Target, usize) -> &T,
136 item_height: f32,
137 item_content: impl 'static + Copy + Fn(&mut Context, usize, MapRef<L, T>) -> Handle<V>,
138 ) -> Handle<Self> {
139 let num_items = list.map(list_len);
140 Self {
141 scroll_to_cursor: true,
142 on_scroll: None,
143 num_items: num_items.get(cx),
144 item_height,
145 visible_range: 0..0,
146 scroll_x: 0.0,
147 scroll_y: 0.0,
148 show_horizontal_scrollbar: false,
149 show_vertical_scrollbar: true,
150 selected: BTreeSet::default(),
151 selectable: Selectable::None,
152 focused: None,
153 selection_follows_focus: false,
154 on_select: None,
155 }
156 .build(cx, |cx| {
157 Keymap::from(vec![
158 (
159 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
160 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
161 ),
162 (
163 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
164 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
165 ),
166 (
167 KeyChord::new(Modifiers::empty(), Code::Space),
168 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
169 ),
170 (
171 KeyChord::new(Modifiers::empty(), Code::Enter),
172 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
173 ),
174 ])
175 .build(cx);
176
177 ScrollView::new(cx, move |cx| {
178 Binding::new(cx, num_items, move |cx, lens| {
179 let num_items = lens.get(cx);
180 cx.emit(ScrollEvent::SetY(0.0));
181 VStack::new(cx, |cx| {
184 let num_visible_items = VirtualList::visible_range.map(Range::len);
187 Binding::new(cx, num_visible_items, move |cx, lens| {
188 for i in 0..lens.get(cx).min(num_items) {
189 let item_index = VirtualList::visible_item_index(i);
193 Binding::new(cx, item_index, move |cx, lens| {
194 let index = lens.get(cx);
195
196 let item = list.map_ref(move |list| list_index(list, index));
197
198 ListItem::new(
199 cx,
200 index,
201 item,
202 VirtualList::selected,
203 VirtualList::focused,
204 move |cx, index, item| {
205 item_content(cx, index, item).height(Percentage(100.0));
206 },
207 )
208 .min_width(Auto)
209 .height(Pixels(item_height))
210 .position_type(PositionType::Absolute)
211 .bind(
212 item_index,
213 move |handle, lens| {
214 let index = lens.get(&handle);
215 handle.top(Pixels(index as f32 * item_height));
216 },
217 );
218 });
219 }
220 })
221 })
222 .height(Pixels(num_items as f32 * item_height));
223 })
224 })
225 .show_horizontal_scrollbar(Self::show_horizontal_scrollbar)
226 .show_vertical_scrollbar(Self::show_vertical_scrollbar)
227 .scroll_to_cursor(Self::scroll_to_cursor)
228 .scroll_x(Self::scroll_x)
229 .scroll_y(Self::scroll_y)
230 .on_scroll(|cx, x, y| {
231 if y.is_finite() && x.is_finite() {
232 cx.emit(ListEvent::Scroll(x, y));
233 }
234 });
235 })
236 .toggle_class("selectable", VirtualList::selectable.map(|s| *s != Selectable::None))
237 .navigable(true)
238 .role(Role::List)
239 .bind(num_items, |handle, num_items| {
240 let ni = num_items.get(&handle);
241 handle.modify(|list| list.num_items = ni);
242 })
243 }
244}
245
246impl View for VirtualList {
247 fn element(&self) -> Option<&'static str> {
248 Some("virtual-list")
249 }
250
251 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
252 event.take(|list_event, meta| match list_event {
253 ListEvent::Select(index) => {
254 cx.focus();
255 match self.selectable {
256 Selectable::Single => {
257 if self.selected.contains(&index) {
258 self.selected.clear();
259 self.focused = None;
260 } else {
261 self.selected.clear();
262 self.selected.insert(index);
263 self.focused = Some(index);
264 if let Some(on_select) = &self.on_select {
265 on_select(cx, index);
266 }
267 }
268 }
269
270 Selectable::Multi => {
271 if self.selected.contains(&index) {
272 self.selected.remove(&index);
273 self.focused = None;
274 } else {
275 self.selected.insert(index);
276 self.focused = Some(index);
277 if let Some(on_select) = &self.on_select {
278 on_select(cx, index);
279 }
280 }
281 }
282
283 Selectable::None => {}
284 }
285
286 meta.consume();
287 }
288
289 ListEvent::SelectFocused => {
290 if let Some(focused) = &self.focused {
291 cx.emit(ListEvent::Select(*focused))
292 }
293 meta.consume();
294 }
295
296 ListEvent::ClearSelection => {
297 self.selected.clear();
298 meta.consume();
299 }
300
301 ListEvent::FocusNext => {
302 if let Some(focused) = &mut self.focused {
303 if *focused < self.num_items.saturating_sub(1) {
304 *focused = focused.saturating_add(1);
305 if self.selection_follows_focus {
306 cx.emit(ListEvent::SelectFocused);
307 }
308 }
309 } else {
310 self.focused = Some(0);
311 if self.selection_follows_focus {
312 cx.emit(ListEvent::SelectFocused);
313 }
314 }
315
316 meta.consume();
317 }
318
319 ListEvent::FocusPrev => {
320 if let Some(focused) = &mut self.focused {
321 if *focused > 0 {
322 *focused = focused.saturating_sub(1);
323 if self.selection_follows_focus {
324 cx.emit(ListEvent::SelectFocused);
325 }
326 }
327 } else {
328 self.focused = Some(self.num_items.saturating_sub(1));
329 if self.selection_follows_focus {
330 cx.emit(ListEvent::SelectFocused);
331 }
332 }
333
334 meta.consume();
335 }
336
337 ListEvent::Scroll(x, y) => {
338 self.scroll_x = x;
339 self.scroll_y = y;
340
341 self.recalc(cx);
342
343 if let Some(callback) = &self.on_scroll {
344 (callback)(cx, x, y);
345 }
346
347 meta.consume();
348 }
349 });
350
351 event.map(|window_event, _| match window_event {
352 WindowEvent::GeometryChanged(geo) => {
353 if geo.intersects(GeoChanged::WIDTH_CHANGED | GeoChanged::HEIGHT_CHANGED) {
354 self.recalc(cx);
355 }
356 }
357
358 _ => {}
359 });
360 }
361}
362
363impl Handle<'_, VirtualList> {
364 pub fn selected<S: Lens>(self, selected: S) -> Self
366 where
367 S::Target: Deref<Target = [usize]> + Data,
368 {
369 self.bind(selected, |handle, s| {
370 let ss = s.get(&handle).deref().to_vec();
371 handle.modify(|list| {
372 list.selected.clear();
373 for idx in ss {
374 list.selected.insert(idx);
375 list.focused = Some(idx);
376 }
377 });
378 })
379 }
380
381 pub fn on_select<F>(self, callback: F) -> Self
383 where
384 F: 'static + Fn(&mut EventContext, usize),
385 {
386 self.modify(|list| list.on_select = Some(Box::new(callback)))
387 }
388
389 pub fn selectable<U: Into<Selectable>>(self, selectable: impl Res<U>) -> Self {
391 self.bind(selectable, |handle, selectable| {
392 let s = selectable.get(&handle).into();
393 handle.modify(|list| list.selectable = s);
394 })
395 }
396
397 pub fn selection_follows_focus<U: Into<bool>>(self, flag: impl Res<U>) -> Self {
399 self.bind(flag, |handle, selection_follows_focus| {
400 let s = selection_follows_focus.get(&handle).into();
401 handle.modify(|list| list.selection_follows_focus = s);
402 })
403 }
404
405 pub fn scroll_to_cursor(self, flag: bool) -> Self {
407 self.modify(|virtual_list: &mut VirtualList| {
408 virtual_list.scroll_to_cursor = flag;
409 })
410 }
411
412 pub fn on_scroll(
414 self,
415 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
416 ) -> Self {
417 self.modify(|list| list.on_scroll = Some(Box::new(callback)))
418 }
419
420 pub fn scroll_x(self, scrollx: impl Res<f32>) -> Self {
422 self.bind(scrollx, |handle, scrollx| {
423 let sx = scrollx.get(&handle);
424 handle.modify(|list| list.scroll_x = sx);
425 })
426 }
427
428 pub fn scroll_y(self, scrollx: impl Res<f32>) -> Self {
430 self.bind(scrollx, |handle, scrolly| {
431 let sy = scrolly.get(&handle);
432 handle.modify(|list| list.scroll_y = sy);
433 })
434 }
435
436 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool>) -> Self {
438 self.bind(flag, |handle, show_scrollbar| {
439 let s = show_scrollbar.get(&handle);
440 handle.modify(|list| list.show_horizontal_scrollbar = s);
441 })
442 }
443
444 pub fn show_vertical_scrollbar(self, flag: impl Res<bool>) -> Self {
446 self.bind(flag, |handle, show_scrollbar| {
447 let s = show_scrollbar.get(&handle);
448 handle.modify(|list| list.show_vertical_scrollbar = s);
449 })
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 fn evaluate_indices(range: Range<usize>) -> Vec<usize> {
458 (0..range.len())
459 .map(|index| VirtualList::evaluate_index(index, range.start, range.end))
460 .collect()
461 }
462
463 #[test]
464 fn test_evaluate_index() {
465 assert_eq!(evaluate_indices(0..4), [0, 1, 2, 3]);
467 assert_eq!(evaluate_indices(1..5), [4, 1, 2, 3]);
469 assert_eq!(evaluate_indices(2..6), [4, 5, 2, 3]);
471 assert_eq!(evaluate_indices(3..7), [4, 5, 6, 3]);
473 assert_eq!(evaluate_indices(4..8), [4, 5, 6, 7]);
475 assert_eq!(evaluate_indices(5..9), [8, 5, 6, 7]);
477 assert_eq!(evaluate_indices(6..10), [8, 9, 6, 7]);
479 assert_eq!(evaluate_indices(7..11), [8, 9, 10, 7]);
481 assert_eq!(evaluate_indices(8..12), [8, 9, 10, 11]);
483 assert_eq!(evaluate_indices(9..13), [12, 9, 10, 11]);
485 }
486}