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