1use std::{
2 collections::BTreeSet,
3 ops::{Deref, Range},
4 time::{Duration, Instant},
5};
6
7use crate::prelude::*;
8
9pub struct VirtualList {
11 scroll_to_cursor: Signal<bool>,
13 on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
15 num_items: Signal<usize>,
17 item_height: f32,
19 orientation: Signal<Orientation>,
21 visible_range: Signal<Range<usize>>,
23 scroll_x: Signal<f32>,
25 scroll_y: Signal<f32>,
27 show_horizontal_scrollbar: Signal<bool>,
29 show_vertical_scrollbar: Signal<bool>,
31 selection: Signal<BTreeSet<usize>>,
33 selectable: Signal<Selectable>,
35 focused: Signal<Option<usize>>,
37 focus_visibility: Signal<bool>,
39 selection_follows_focus: Signal<bool>,
41 space_selects_focused: Signal<bool>,
43 min_selected: Signal<usize>,
45 max_selected: Signal<usize>,
47 focus_first_item_on_focus_in: Signal<bool>,
49 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
51 on_focus: Option<Box<dyn Fn(&mut EventContext, usize)>>,
53 type_ahead_text: Option<Box<dyn Fn(&mut EventContext, usize) -> Option<String>>>,
55 type_ahead_buffer: String,
57 type_ahead_last_input: Option<Instant>,
59 type_ahead_timeout: Duration,
61}
62
63impl VirtualList {
64 fn set_focused_with_callback(&mut self, cx: &mut EventContext, focused: Option<usize>) {
65 let previous = self.focused.get();
66 self.focused.set(focused);
67
68 if previous != focused {
69 if let (Some(index), Some(callback)) = (focused, self.on_focus.as_ref()) {
70 callback(cx, index);
71 }
72 }
73 }
74
75 fn find_type_ahead_match(
76 &self,
77 cx: &mut EventContext,
78 query: &str,
79 start_index: usize,
80 ) -> Option<usize> {
81 let get_text = self.type_ahead_text.as_ref()?;
82 let num_items = self.num_items.get();
83 if num_items == 0 {
84 return None;
85 }
86
87 for offset in 0..num_items {
88 let index = (start_index + offset) % num_items;
89 let item_text = get_text(cx, index)
90 .map(|text| text.trim_start().to_lowercase())
91 .unwrap_or_default();
92
93 if !item_text.is_empty() && item_text.starts_with(query) {
94 return Some(index);
95 }
96 }
97
98 None
99 }
100
101 fn try_type_ahead(&mut self, cx: &mut EventContext, typed: char) -> bool {
102 if self.type_ahead_text.is_none() {
103 return false;
104 }
105
106 let num_items = self.num_items.get();
107 if num_items == 0 || typed.is_control() || typed.is_whitespace() {
108 return false;
109 }
110
111 let now = Instant::now();
112 let within_timeout = self
113 .type_ahead_last_input
114 .is_some_and(|last| now.saturating_duration_since(last) <= self.type_ahead_timeout);
115
116 let ch = typed.to_lowercase().collect::<String>();
117 let query = if within_timeout {
118 let repeated_char_cycle = !self.type_ahead_buffer.is_empty()
119 && self.type_ahead_buffer.chars().all(|c| c == typed.to_ascii_lowercase());
120
121 if repeated_char_cycle {
122 ch.clone()
123 } else {
124 format!("{}{}", self.type_ahead_buffer, ch)
125 }
126 } else {
127 ch.clone()
128 };
129
130 let start_index = self.focused.get().map(|focused| (focused + 1) % num_items).unwrap_or(0);
131
132 if let Some(index) = self.find_type_ahead_match(cx, &query, start_index) {
133 self.type_ahead_buffer = query;
134 self.type_ahead_last_input = Some(now);
135 self.focus_visibility.set(true);
136 self.set_focused_with_callback(cx, Some(index));
137
138 if self.selection_follows_focus.get() {
139 cx.emit(ListEvent::SelectFocused);
140 }
141
142 true
143 } else {
144 self.type_ahead_buffer.clear();
145 self.type_ahead_last_input = Some(now);
146 false
147 }
148 }
149
150 fn evaluate_index(index: usize, start: usize, end: usize) -> usize {
151 let len = end.saturating_sub(start);
152 if len == 0 { 0 } else { start + ((index + len - (start % len)) % len) }
153 }
154
155 fn resolve_item<L, T>(
164 list: impl SignalWith<L> + Copy + 'static,
165 index: usize,
166 list_len: impl 'static + Fn(&L) -> usize,
167 list_index: impl 'static + Fn(&L, usize) -> T,
168 ) -> Memo<T>
169 where
170 L: 'static,
171 T: Clone + PartialEq + 'static,
172 {
173 Memo::new(move |prev| {
174 list.with(|list| {
175 let len = list_len(list);
176 if len == 0 {
177 prev.cloned().unwrap_or_else(|| list_index(list, 0))
180 } else {
181 list_index(list, index.min(len - 1))
182 }
183 })
184 })
185 }
186
187 fn recalc(&self, cx: &mut EventContext) {
188 let num_items = self.num_items.get();
189 if num_items == 0 {
190 self.visible_range.set_if_changed(0..0);
191 return;
192 }
193
194 let current = cx.current();
195 let current_extent = match self.orientation.get() {
196 Orientation::Horizontal => cx.cache.get_width(current),
197 Orientation::Vertical => cx.cache.get_height(current),
198 };
199 if current_extent == f32::MAX {
200 return;
201 }
202
203 let item_extent = self.item_height;
204 let total_extent = item_extent * (num_items as f32);
205 let visible_extent = current_extent / cx.scale_factor();
206
207 let mut num_visible_items = (visible_extent / item_extent).ceil();
208 num_visible_items += 1.0; let visible_items_extent = item_extent * num_visible_items;
211 let empty_extent = (total_extent - visible_items_extent).max(0.0);
212
213 let axis_scroll = match self.orientation.get() {
215 Orientation::Horizontal => self.scroll_x.get(),
216 Orientation::Vertical => self.scroll_y.get(),
217 };
218 let visible_start = empty_extent * axis_scroll;
219 let visible_end = visible_start + visible_items_extent;
220
221 let mut start_index = (visible_start / item_extent).trunc() as usize;
223 let mut end_index = 1 + (visible_end / item_extent).trunc() as usize;
224
225 let desired_range_size = (num_visible_items as usize) + 1;
227 end_index = end_index.min(num_items);
228
229 let current_range_size = end_index.saturating_sub(start_index);
230
231 if current_range_size < desired_range_size {
232 match end_index == num_items {
233 true => {
235 start_index =
236 start_index.saturating_sub(desired_range_size - current_range_size);
237 }
238 false if end_index < num_items => {
240 end_index = (start_index + desired_range_size).min(num_items);
241 }
242 _ => {}
243 }
244 }
245
246 self.visible_range.set_if_changed(start_index..end_index);
247 }
248
249 fn selection_limits(&self) -> (usize, usize) {
250 let mut min_selected = self.min_selected.get();
251 let mut max_selected = self.max_selected.get();
252
253 match self.selectable.get() {
254 Selectable::None => {
255 min_selected = 0;
256 max_selected = 0;
257 }
258
259 Selectable::Single => {
260 min_selected = min_selected.min(1);
261 max_selected = 1;
262 }
263
264 Selectable::Multi => {}
265 }
266
267 max_selected = max_selected.min(self.num_items.get());
268 min_selected = min_selected.min(max_selected);
269
270 (min_selected, max_selected)
271 }
272
273 fn normalize_selection_state(&mut self) {
274 let (min_selected, max_selected) = self.selection_limits();
275 let num_items = self.num_items.get();
276
277 let mut selection = self.selection.get();
278 selection.retain(|index| *index < num_items);
279
280 while selection.len() > max_selected {
281 if let Some(last) = selection.iter().next_back().copied() {
282 selection.remove(&last);
283 } else {
284 break;
285 }
286 }
287
288 if selection.len() < min_selected {
289 for index in 0..num_items {
290 selection.insert(index);
291 if selection.len() >= min_selected {
292 break;
293 }
294 }
295 }
296
297 let mut focused = self.focused.get();
298 if focused.is_some_and(|index| index >= num_items) {
299 focused = num_items.checked_sub(1);
300 }
301
302 self.selection.set(selection);
303 self.focused.set(focused);
304 }
305}
306
307impl VirtualList {
308 pub fn new<V: View, S, L, T>(
310 cx: &mut Context,
311 list: S,
312 item_height: f32,
313 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
314 ) -> Handle<Self>
315 where
316 S: Res<L> + 'static,
317 L: Deref<Target = [T]> + Clone + 'static,
318 T: Clone + PartialEq + 'static,
319 {
320 Self::new_generic(
321 cx,
322 list,
323 |list| list.len(),
324 |list, index| {
325 list.get(index).cloned().unwrap_or_else(|| {
326 list.last().cloned().expect("virtual list item requested from empty list")
327 })
328 },
329 item_height,
330 item_content,
331 )
332 }
333
334 pub fn new_custom_items_with_selection<V: View, S, L, T>(
336 cx: &mut Context,
337 list: S,
338 item_height: f32,
339 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>, Memo<bool>) -> Handle<V>,
340 ) -> Handle<Self>
341 where
342 S: Res<L> + 'static,
343 L: Deref<Target = [T]> + Clone + 'static,
344 T: Clone + PartialEq + 'static,
345 {
346 Self::new_generic_custom_items_with_selection(
347 cx,
348 list,
349 |list| list.len(),
350 |list, index| {
351 list.get(index).cloned().unwrap_or_else(|| {
352 list.last().cloned().expect("virtual list item requested from empty list")
353 })
354 },
355 item_height,
356 item_content,
357 )
358 }
359
360 pub fn new_custom_items<V: View, S, L, T>(
363 cx: &mut Context,
364 list: S,
365 item_height: f32,
366 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
367 ) -> Handle<Self>
368 where
369 S: Res<L> + 'static,
370 L: Deref<Target = [T]> + Clone + 'static,
371 T: Clone + PartialEq + 'static,
372 {
373 Self::new_custom_items_with_selection(cx, list, item_height, move |cx, index, item, _| {
374 item_content(cx, index, item)
375 })
376 }
377
378 pub fn new_generic<V: View, S, L, T>(
380 cx: &mut Context,
381 list: S,
382 list_len: impl 'static + Copy + Fn(&L) -> usize,
383 list_index: impl 'static + Copy + Fn(&L, usize) -> T,
384 item_height: f32,
385 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
386 ) -> Handle<Self>
387 where
388 S: Res<L> + 'static,
389 L: Clone + 'static,
390 T: Clone + PartialEq + 'static,
391 {
392 let list = list.to_signal(cx);
393 let num_items = list.map(list_len).to_signal(cx);
394 let visible_range = Signal::new(0..0);
395 let scroll_x = Signal::new(0.0);
396 let scroll_y = Signal::new(0.0);
397 let show_horizontal_scrollbar = Signal::new(true);
398 let show_vertical_scrollbar = Signal::new(true);
399 let orientation = Signal::new(Orientation::Vertical);
400 let selection = Signal::new(BTreeSet::default());
401 let selectable = Signal::new(Selectable::None);
402 let focused = Signal::new(None);
403 let focus_visibility = Signal::new(false);
404 let selection_follows_focus = Signal::new(false);
405 let scroll_to_cursor = Signal::new(false);
406 let min_selected = Signal::new(0);
407 let max_selected = Signal::new(usize::MAX);
408 let focus_first_item_on_focus_in = Signal::new(true);
409
410 Self {
411 scroll_to_cursor,
412 on_scroll: None,
413 num_items,
414 item_height,
415 orientation,
416 visible_range,
417 scroll_x,
418 scroll_y,
419 show_horizontal_scrollbar,
420 show_vertical_scrollbar,
421 selection,
422 selectable,
423 focused,
424 focus_visibility,
425 selection_follows_focus,
426 space_selects_focused: Signal::new(true),
427 min_selected,
428 max_selected,
429 focus_first_item_on_focus_in,
430 on_select: None,
431 on_focus: None,
432 type_ahead_text: None,
433 type_ahead_buffer: String::new(),
434 type_ahead_last_input: None,
435 type_ahead_timeout: Duration::from_millis(1000),
436 }
437 .build(cx, |cx| {
438 Keymap::from(vec![
439 (
440 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
441 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
442 ),
443 (
444 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
445 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
446 ),
447 (
448 KeyChord::new(Modifiers::empty(), Code::Home),
449 KeymapEntry::new("Focus First", |cx| cx.emit(ListEvent::FocusFirst)),
450 ),
451 (
452 KeyChord::new(Modifiers::empty(), Code::End),
453 KeymapEntry::new("Focus Last", |cx| cx.emit(ListEvent::FocusLast)),
454 ),
455 (
456 KeyChord::new(Modifiers::empty(), Code::Enter),
457 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
458 ),
459 ])
460 .build(cx);
461
462 Binding::new(cx, orientation, move |cx| {
463 let orientation = orientation.get();
464 if orientation == Orientation::Horizontal {
465 cx.emit(KeymapEvent::RemoveAction(
466 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
467 "Focus Next",
468 ));
469
470 cx.emit(KeymapEvent::RemoveAction(
471 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
472 "Focus Previous",
473 ));
474
475 cx.emit(KeymapEvent::InsertAction(
476 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
477 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
478 ));
479
480 cx.emit(KeymapEvent::InsertAction(
481 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
482 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
483 ));
484 } else {
485 cx.emit(KeymapEvent::RemoveAction(
486 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
487 "Focus Next",
488 ));
489
490 cx.emit(KeymapEvent::RemoveAction(
491 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
492 "Focus Previous",
493 ));
494
495 cx.emit(KeymapEvent::InsertAction(
496 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
497 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
498 ));
499
500 cx.emit(KeymapEvent::InsertAction(
501 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
502 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
503 ));
504 }
505 });
506
507 ScrollView::new(cx, move |cx| {
508 Binding::new(cx, orientation, move |cx| {
509 let orientation = orientation.get();
510 Binding::new(cx, num_items, move |cx| {
511 let num_items = num_items.get();
512
513 match orientation {
514 Orientation::Horizontal => cx.emit(ScrollEvent::SetX(0.0)),
515 Orientation::Vertical => cx.emit(ScrollEvent::SetY(0.0)),
516 }
517
518 let num_visible_items = visible_range.map(Range::len);
519
520 match orientation {
521 Orientation::Horizontal => {
522 HStack::new(cx, |cx| {
523 Binding::new(cx, num_visible_items, move |cx| {
524 for i in 0..num_visible_items.get().min(num_items) {
525 let item_index = visible_range.map(move |range| {
526 Self::evaluate_index(i, range.start, range.end)
527 });
528 Binding::new(cx, item_index, move |cx| {
529 let index = item_index.get();
530 let item = Self::resolve_item(
531 list, index, list_len, list_index,
532 );
533
534 ListItem::new(
535 cx,
536 index,
537 item,
538 selection,
539 focused,
540 focus_visibility,
541 move |cx, index, item| {
542 item_content(cx, index, item)
543 .height(Percentage(100.0));
544 },
545 )
546 .min_size(Auto)
547 .width(Pixels(item_height))
548 .height(Percentage(100.0))
549 .position_type(PositionType::Absolute)
550 .bind(item_index, move |handle| {
551 let index = item_index.get();
552 handle.left(Pixels(index as f32 * item_height));
553 });
554 });
555 }
556 })
557 })
558 .width(Pixels(num_items as f32 * item_height))
559 .height(Stretch(1.0));
560 }
561
562 Orientation::Vertical => {
563 VStack::new(cx, |cx| {
564 Binding::new(cx, num_visible_items, move |cx| {
565 for i in 0..num_visible_items.get().min(num_items) {
566 let item_index = visible_range.map(move |range| {
567 Self::evaluate_index(i, range.start, range.end)
568 });
569 Binding::new(cx, item_index, move |cx| {
570 let index = item_index.get();
571 let item = Self::resolve_item(
572 list, index, list_len, list_index,
573 );
574
575 ListItem::new(
576 cx,
577 index,
578 item,
579 selection,
580 focused,
581 focus_visibility,
582 move |cx, index, item| {
583 item_content(cx, index, item)
584 .height(Percentage(100.0));
585 },
586 )
587 .min_width(Auto)
588 .height(Pixels(item_height))
589 .position_type(PositionType::Absolute)
590 .bind(item_index, move |handle| {
591 let index = item_index.get();
592 handle.top(Pixels(index as f32 * item_height));
593 });
594 });
595 }
596 })
597 })
598 .height(Pixels(num_items as f32 * item_height));
599 }
600 }
601 })
602 })
603 })
604 .show_horizontal_scrollbar(show_horizontal_scrollbar)
605 .show_vertical_scrollbar(show_vertical_scrollbar)
606 .scroll_to_cursor(scroll_to_cursor)
607 .scroll_x(scroll_x)
608 .scroll_y(scroll_y)
609 .on_scroll(|cx, x, y| {
610 if y.is_finite() && x.is_finite() {
611 cx.emit(ListEvent::Scroll(x, y));
612 }
613 });
614 })
615 .toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
616 .multiselectable(selectable.map(|s| *s == Selectable::Multi))
617 .orientation(orientation)
618 .navigable(true)
619 .role(Role::ListBox)
620 }
621
622 pub fn new_generic_custom_items_with_selection<V: View, S, L, T>(
624 cx: &mut Context,
625 list: S,
626 list_len: impl 'static + Fn(&L) -> usize,
627 list_index: impl 'static + Copy + Fn(&L, usize) -> T,
628 item_height: f32,
629 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>, Memo<bool>) -> Handle<V>,
630 ) -> Handle<Self>
631 where
632 S: Res<L> + 'static,
633 L: Clone + 'static,
634 T: Clone + PartialEq + 'static,
635 {
636 let list = list.to_signal(cx);
637 let num_items = list.map(list_len).to_signal(cx);
638 let visible_range = Signal::new(0..0);
639 let scroll_x = Signal::new(0.0);
640 let scroll_y = Signal::new(0.0);
641 let show_horizontal_scrollbar = Signal::new(true);
642 let show_vertical_scrollbar = Signal::new(true);
643 let orientation = Signal::new(Orientation::Vertical);
644 let selection = Signal::new(BTreeSet::default());
645 let selectable = Signal::new(Selectable::None);
646 let focused = Signal::new(None);
647 let focus_visibility = Signal::new(false);
648 let selection_follows_focus = Signal::new(false);
649 let scroll_to_cursor = Signal::new(false);
650 let min_selected = Signal::new(0);
651 let max_selected = Signal::new(usize::MAX);
652 let focus_first_item_on_focus_in = Signal::new(true);
653
654 Self {
655 scroll_to_cursor,
656 on_scroll: None,
657 num_items,
658 item_height,
659 orientation,
660 visible_range,
661 scroll_x,
662 scroll_y,
663 show_horizontal_scrollbar,
664 show_vertical_scrollbar,
665 selection,
666 selectable,
667 focused,
668 focus_visibility,
669 selection_follows_focus,
670 space_selects_focused: Signal::new(true),
671 min_selected,
672 max_selected,
673 focus_first_item_on_focus_in,
674 on_select: None,
675 on_focus: None,
676 type_ahead_text: None,
677 type_ahead_buffer: String::new(),
678 type_ahead_last_input: None,
679 type_ahead_timeout: Duration::from_millis(1000),
680 }
681 .build(cx, |cx| {
682 Keymap::from(vec![
683 (
684 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
685 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
686 ),
687 (
688 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
689 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
690 ),
691 (
692 KeyChord::new(Modifiers::empty(), Code::Home),
693 KeymapEntry::new("Focus First", |cx| cx.emit(ListEvent::FocusFirst)),
694 ),
695 (
696 KeyChord::new(Modifiers::empty(), Code::End),
697 KeymapEntry::new("Focus Last", |cx| cx.emit(ListEvent::FocusLast)),
698 ),
699 (
700 KeyChord::new(Modifiers::empty(), Code::Enter),
701 KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
702 ),
703 ])
704 .build(cx);
705
706 Binding::new(cx, orientation, move |cx| {
707 let orientation = orientation.get();
708 if orientation == Orientation::Horizontal {
709 cx.emit(KeymapEvent::RemoveAction(
710 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
711 "Focus Next",
712 ));
713
714 cx.emit(KeymapEvent::RemoveAction(
715 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
716 "Focus Previous",
717 ));
718
719 cx.emit(KeymapEvent::InsertAction(
720 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
721 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
722 ));
723
724 cx.emit(KeymapEvent::InsertAction(
725 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
726 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
727 ));
728 } else {
729 cx.emit(KeymapEvent::RemoveAction(
730 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
731 "Focus Next",
732 ));
733
734 cx.emit(KeymapEvent::RemoveAction(
735 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
736 "Focus Previous",
737 ));
738
739 cx.emit(KeymapEvent::InsertAction(
740 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
741 KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
742 ));
743
744 cx.emit(KeymapEvent::InsertAction(
745 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
746 KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
747 ));
748 }
749 });
750
751 ScrollView::new(cx, move |cx| {
752 Binding::new(cx, num_items, move |cx| {
753 let num_items = num_items.get();
754 cx.emit(ScrollEvent::SetY(0.0));
755 VStack::new(cx, |cx| {
756 let num_visible_items = visible_range.map(Range::len);
757 Binding::new(cx, num_visible_items, move |cx| {
758 for i in 0..num_visible_items.get().min(num_items) {
759 let item_index = visible_range.map(move |range| {
760 Self::evaluate_index(i, range.start, range.end)
761 });
762 Binding::new(cx, item_index, move |cx| {
763 let max_index = num_items.saturating_sub(1);
764 let index = item_index.get().min(max_index);
765 let item = list.map(move |list| list_index(list, index));
766 let is_selected =
767 selection.map(move |selection| selection.contains(&index));
768 let is_focused = focused
769 .map(move |focused| focused.is_some_and(|f| f == index));
770
771 item_content(cx, index, item, is_selected)
772 .focusable(true)
773 .navigable(false)
774 .focused_with_visibility(is_focused, focus_visibility)
775 .on_press(move |cx| cx.emit(ListEvent::Select(index)))
776 .min_width(Auto)
777 .height(Pixels(item_height))
778 .position_type(PositionType::Absolute)
779 .bind(item_index, move |handle| {
780 let index = item_index.get();
781 handle.top(Pixels(index as f32 * item_height));
782 });
783 });
784 }
785 })
786 })
787 .height(Pixels(num_items as f32 * item_height));
788 })
789 })
790 .show_horizontal_scrollbar(show_horizontal_scrollbar)
791 .show_vertical_scrollbar(show_vertical_scrollbar)
792 .scroll_to_cursor(scroll_to_cursor)
793 .scroll_x(scroll_x)
794 .scroll_y(scroll_y)
795 .on_scroll(|cx, x, y| {
796 if y.is_finite() && x.is_finite() {
797 cx.emit(ListEvent::Scroll(x, y));
798 }
799 });
800 })
801 .toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
802 .multiselectable(selectable.map(|s| *s == Selectable::Multi))
803 .orientation(orientation)
804 .navigable(true)
805 .role(Role::ListBox)
806 }
807
808 pub fn new_generic_custom_items<V: View, S, L, T>(
811 cx: &mut Context,
812 list: S,
813 list_len: impl 'static + Fn(&L) -> usize,
814 list_index: impl 'static + Copy + Fn(&L, usize) -> T,
815 item_height: f32,
816 item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
817 ) -> Handle<Self>
818 where
819 S: Res<L> + 'static,
820 L: Clone + 'static,
821 T: Clone + PartialEq + 'static,
822 {
823 Self::new_generic_custom_items_with_selection(
824 cx,
825 list,
826 list_len,
827 list_index,
828 item_height,
829 move |cx, index, item, _| item_content(cx, index, item),
830 )
831 }
832}
833
834impl View for VirtualList {
835 fn element(&self) -> Option<&'static str> {
836 Some("virtual-list")
837 }
838
839 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
840 event.take(|list_event, meta| match list_event {
841 ListEvent::Select(index) => {
842 let selectable = self.selectable.get();
843 let (min_selected, max_selected) = self.selection_limits();
844 let mut selection = self.selection.get();
845 let mut focused = self.focused.get();
846
847 match selectable {
848 Selectable::Single => {
849 if selection.contains(&index) {
850 if min_selected == 0 {
851 selection.clear();
852 focused = None;
853 }
854 } else {
855 selection.clear();
856 selection.insert(index);
857 focused = Some(index);
858 if let Some(on_select) = &self.on_select {
859 on_select(cx, index);
860 }
861 }
862 }
863
864 Selectable::Multi => {
865 focused = Some(index);
867
868 if selection.contains(&index) {
869 if selection.len() > min_selected {
870 selection.remove(&index);
871 if let Some(on_select) = &self.on_select {
872 on_select(cx, index);
873 }
874 }
875 } else if selection.len() < max_selected {
876 selection.insert(index);
877 if let Some(on_select) = &self.on_select {
878 on_select(cx, index);
879 }
880 }
881 }
882
883 Selectable::None => {}
884 }
885
886 self.selection.set(selection);
887 self.set_focused_with_callback(cx, focused);
888
889 meta.consume();
890 }
891
892 ListEvent::SelectFocused => {
893 if let Some(focused) = self.focused.get() {
894 self.focus_visibility.set(true);
895 cx.emit(ListEvent::Select(focused))
896 }
897 meta.consume();
898 }
899
900 ListEvent::Focus(index) => {
901 if index < self.num_items.get() {
902 self.focus_visibility.set(true);
903 self.set_focused_with_callback(cx, Some(index));
904 }
905
906 meta.consume();
907 }
908
909 ListEvent::ClearSelection => {
910 let (min_selected, _) = self.selection_limits();
911 if min_selected == 0 {
912 self.selection.set(BTreeSet::default());
913 }
914 meta.consume();
915 }
916
917 ListEvent::FocusNext => {
918 let mut focused = self.focused.get();
919 let num_items = self.num_items.get();
920 let mut moved_focus = false;
921 if let Some(f) = &mut focused {
922 if *f < num_items.saturating_sub(1) {
923 *f = f.saturating_add(1);
924 moved_focus = true;
925 if self.selection_follows_focus.get() {
926 cx.emit(ListEvent::SelectFocused);
927 }
928 }
929 } else {
930 focused = Some(0);
931 moved_focus = true;
932 if self.selection_follows_focus.get() {
933 cx.emit(ListEvent::SelectFocused);
934 }
935 }
936
937 if moved_focus {
938 self.focus_visibility.set(true);
939 }
940
941 self.set_focused_with_callback(cx, focused);
942
943 meta.consume();
944 }
945
946 ListEvent::FocusPrev => {
947 let mut focused = self.focused.get();
948 let num_items = self.num_items.get();
949 let mut moved_focus = false;
950 if let Some(f) = &mut focused {
951 if *f > 0 {
952 *f = f.saturating_sub(1);
953 moved_focus = true;
954 if self.selection_follows_focus.get() {
955 cx.emit(ListEvent::SelectFocused);
956 }
957 }
958 } else {
959 focused = Some(num_items.saturating_sub(1));
960 moved_focus = true;
961 if self.selection_follows_focus.get() {
962 cx.emit(ListEvent::SelectFocused);
963 }
964 }
965
966 if moved_focus {
967 self.focus_visibility.set(true);
968 }
969
970 self.set_focused_with_callback(cx, focused);
971
972 meta.consume();
973 }
974
975 ListEvent::FocusFirst => {
976 if self.num_items.get() > 0 {
977 self.focus_visibility.set(true);
978 self.set_focused_with_callback(cx, Some(0));
979 if self.selection_follows_focus.get() {
980 cx.emit(ListEvent::SelectFocused);
981 }
982 }
983
984 meta.consume();
985 }
986
987 ListEvent::FocusLast => {
988 let num_items = self.num_items.get();
989 if num_items > 0 {
990 self.focus_visibility.set(true);
991 self.set_focused_with_callback(cx, Some(num_items.saturating_sub(1)));
992 if self.selection_follows_focus.get() {
993 cx.emit(ListEvent::SelectFocused);
994 }
995 }
996
997 meta.consume();
998 }
999
1000 ListEvent::Scroll(x, y) => {
1001 self.scroll_x.set(x);
1002 self.scroll_y.set(y);
1003
1004 self.recalc(cx);
1005
1006 if let Some(callback) = &self.on_scroll {
1007 (callback)(cx, x, y);
1008 }
1009
1010 meta.consume();
1011 }
1012 });
1013
1014 event.map(|window_event, meta| match window_event {
1015 WindowEvent::Press { mouse } => {
1016 self.focus_visibility.set(!*mouse);
1017 }
1018
1019 WindowEvent::FocusIn if meta.target == cx.current() => {
1020 if meta.origin == Entity::root() {
1021 self.focus_visibility.set(true);
1022 }
1023
1024 let next_focused = focus_index_on_focus_in(
1025 &self.selection.get(),
1026 self.num_items.get(),
1027 self.focus_first_item_on_focus_in.get(),
1028 );
1029
1030 if let Some(index) = next_focused {
1031 if self.focused.get() == Some(index) {
1032 self.focused.set(None);
1033 }
1034 self.set_focused_with_callback(cx, Some(index));
1035 }
1036 }
1037
1038 WindowEvent::CharInput(c) => {
1039 if *c == ' ' && meta.target == cx.current() && self.space_selects_focused.get() {
1040 cx.emit(ListEvent::SelectFocused);
1041 meta.consume();
1042 } else if self.try_type_ahead(cx, *c) {
1043 meta.consume();
1044 }
1045 }
1046
1047 WindowEvent::GeometryChanged(geo) => {
1048 if geo.intersects(GeoChanged::WIDTH_CHANGED | GeoChanged::HEIGHT_CHANGED) {
1049 self.recalc(cx);
1050 }
1051 }
1052
1053 _ => {}
1054 });
1055 }
1056}
1057
1058impl Handle<'_, VirtualList> {
1059 pub fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
1061 where
1062 R: Deref<Target = [usize]> + Clone + 'static,
1063 {
1064 let selection = selection.to_signal(self.cx);
1065 self.bind(selection, move |handle| {
1066 selection.with(|selected_indices| {
1067 handle.modify(|list| {
1068 let previous_focused = list.focused.get();
1069 let mut selection = BTreeSet::default();
1070 for idx in selected_indices.deref().iter().copied() {
1071 selection.insert(idx);
1072 }
1073
1074 let focused = previous_focused
1075 .filter(|idx| *idx < list.num_items.get())
1076 .or_else(|| selection.iter().next_back().copied());
1077
1078 list.selection.set(selection);
1079 list.focused.set(focused);
1080 list.normalize_selection_state();
1081 });
1082 });
1083 })
1084 }
1085
1086 pub fn focused_index(self, focused: impl Res<Option<usize>> + 'static) -> Self {
1088 let focused = focused.to_signal(self.cx);
1089 self.bind(focused, move |handle| {
1090 let focused = focused.get();
1091 handle.modify(|list| {
1092 list.focused.set(normalize_focused_index(focused, list.num_items.get()));
1093 });
1094 })
1095 }
1096
1097 pub fn on_select<F>(self, callback: F) -> Self
1099 where
1100 F: 'static + Fn(&mut EventContext, usize),
1101 {
1102 self.modify(|list| list.on_select = Some(Box::new(callback)))
1103 }
1104
1105 pub fn on_focus<F>(self, callback: F) -> Self
1107 where
1108 F: 'static + Fn(&mut EventContext, usize),
1109 {
1110 self.modify(|list| list.on_focus = Some(Box::new(callback)))
1111 }
1112
1113 pub fn selectable<U: Into<Selectable> + Clone + 'static>(
1115 self,
1116 selectable: impl Res<U> + 'static,
1117 ) -> Self {
1118 let selectable = selectable.to_signal(self.cx);
1119 self.bind(selectable, move |handle| {
1120 let selectable = selectable.get();
1121 let s = selectable.into();
1122 handle.modify(|list| {
1123 list.selectable.set(s);
1124 list.normalize_selection_state();
1125 });
1126 })
1127 }
1128
1129 pub fn min_selected(self, min_selected: impl Res<usize> + 'static) -> Self {
1131 let min_selected = min_selected.to_signal(self.cx);
1132 self.bind(min_selected, move |handle| {
1133 let min_selected = min_selected.get();
1134 handle.modify(|list| {
1135 list.min_selected.set(min_selected);
1136 list.normalize_selection_state();
1137 });
1138 })
1139 }
1140
1141 pub fn max_selected(self, max_selected: impl Res<usize> + 'static) -> Self {
1143 let max_selected = max_selected.to_signal(self.cx);
1144 self.bind(max_selected, move |handle| {
1145 let max_selected = max_selected.get();
1146 handle.modify(|list| {
1147 list.max_selected.set(max_selected);
1148 list.normalize_selection_state();
1149 });
1150 })
1151 }
1152
1153 pub fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
1155 self,
1156 flag: impl Res<U> + 'static,
1157 ) -> Self {
1158 let flag = flag.to_signal(self.cx);
1159 self.bind(flag, move |handle| {
1160 let selection_follows_focus = flag.get();
1161 let s = selection_follows_focus.into();
1162 handle.modify(|list| list.selection_follows_focus.set(s));
1163 })
1164 }
1165
1166 pub fn horizontal<U: Into<bool> + Clone + 'static>(
1168 self,
1169 horizontal: impl Res<U> + 'static,
1170 ) -> Self {
1171 let horizontal = horizontal.to_signal(self.cx);
1172 self.bind(horizontal, move |handle| {
1173 let horizontal = horizontal.get();
1174 let horizontal = horizontal.into();
1175 handle.modify(|list| {
1176 list.orientation.set(if horizontal {
1177 Orientation::Horizontal
1178 } else {
1179 Orientation::Vertical
1180 });
1181 });
1182 })
1183 }
1184
1185 pub fn space_selects_focused<U: Into<bool> + Clone + 'static>(
1187 self,
1188 flag: impl Res<U> + 'static,
1189 ) -> Self {
1190 let flag = flag.to_signal(self.cx);
1191 self.bind(flag, move |handle| {
1192 let space_selects_focused = flag.get();
1193 let s = space_selects_focused.into();
1194 handle.modify(|list| list.space_selects_focused.set(s));
1195 })
1196 }
1197
1198 pub fn scroll_to_cursor(self, flag: bool) -> Self {
1200 self.modify(|virtual_list: &mut VirtualList| {
1201 virtual_list.scroll_to_cursor.set(flag);
1202 })
1203 }
1204
1205 pub fn focus_first_item_on_focus_in(self, flag: impl Res<bool> + 'static) -> Self {
1207 let flag = flag.to_signal(self.cx);
1208 self.bind(flag, move |handle| {
1209 let focus_first_item_on_focus_in = flag.get();
1210 handle.modify(|list| {
1211 list.focus_first_item_on_focus_in.set(focus_first_item_on_focus_in);
1212 });
1213 })
1214 }
1215
1216 pub fn on_scroll(
1218 self,
1219 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
1220 ) -> Self {
1221 self.modify(|list| list.on_scroll = Some(Box::new(callback)))
1222 }
1223
1224 pub fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
1226 let scrollx = scrollx.to_signal(self.cx);
1227 self.bind(scrollx, move |handle| {
1228 let sx = scrollx.get();
1229 handle.modify(|list| list.scroll_x.set(sx));
1230 })
1231 }
1232
1233 pub fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self {
1235 let scrollx = scrollx.to_signal(self.cx);
1236 self.bind(scrollx, move |handle| {
1237 let sy = scrollx.get();
1238 handle.modify(|list| list.scroll_y.set(sy));
1239 })
1240 }
1241
1242 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
1244 let flag = flag.to_signal(self.cx);
1245 self.bind(flag, move |handle| {
1246 let s = flag.get();
1247 handle.modify(|list| list.show_horizontal_scrollbar.set(s));
1248 })
1249 }
1250
1251 pub fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
1253 let flag = flag.to_signal(self.cx);
1254 self.bind(flag, move |handle| {
1255 let s = flag.get();
1256 handle.modify(|list| list.show_vertical_scrollbar.set(s));
1257 })
1258 }
1259
1260 pub fn type_ahead_text<F>(self, callback: F) -> Self
1262 where
1263 F: 'static + Fn(&mut EventContext, usize) -> Option<String>,
1264 {
1265 self.modify(|list: &mut VirtualList| list.type_ahead_text = Some(Box::new(callback)))
1266 }
1267}
1268
1269fn normalize_focused_index(focused: Option<usize>, num_items: usize) -> Option<usize> {
1270 focused.filter(|index| *index < num_items)
1271}
1272
1273fn focus_index_on_focus_in(
1274 selection: &BTreeSet<usize>,
1275 num_items: usize,
1276 focus_first_item_on_focus_in: bool,
1277) -> Option<usize> {
1278 selection
1279 .iter()
1280 .copied()
1281 .find(|index| *index < num_items)
1282 .or_else(|| focus_first_item_on_focus_in.then_some(0).filter(|_| num_items > 0))
1283}
1284
1285#[cfg(test)]
1286mod tests {
1287 use super::*;
1288
1289 fn evaluate_indices(range: Range<usize>) -> Vec<usize> {
1290 (0..range.len())
1291 .map(|index| VirtualList::evaluate_index(index, range.start, range.end))
1292 .collect()
1293 }
1294
1295 #[test]
1296 fn test_evaluate_index() {
1297 assert_eq!(evaluate_indices(0..4), [0, 1, 2, 3]);
1299 assert_eq!(evaluate_indices(1..5), [4, 1, 2, 3]);
1301 assert_eq!(evaluate_indices(2..6), [4, 5, 2, 3]);
1303 assert_eq!(evaluate_indices(3..7), [4, 5, 6, 3]);
1305 assert_eq!(evaluate_indices(4..8), [4, 5, 6, 7]);
1307 assert_eq!(evaluate_indices(5..9), [8, 5, 6, 7]);
1309 assert_eq!(evaluate_indices(6..10), [8, 9, 6, 7]);
1311 assert_eq!(evaluate_indices(7..11), [8, 9, 10, 7]);
1313 assert_eq!(evaluate_indices(8..12), [8, 9, 10, 11]);
1315 assert_eq!(evaluate_indices(9..13), [12, 9, 10, 11]);
1317 }
1318
1319 #[test]
1320 fn keeps_focused_index_when_in_range() {
1321 assert_eq!(normalize_focused_index(Some(3), 5), Some(3));
1322 }
1323
1324 #[test]
1325 fn clears_focused_index_when_out_of_range() {
1326 assert_eq!(normalize_focused_index(Some(5), 5), None);
1327 }
1328
1329 #[test]
1330 fn focuses_selected_item_on_focus_in() {
1331 let mut selection = BTreeSet::new();
1332 selection.insert(3);
1333
1334 assert_eq!(focus_index_on_focus_in(&selection, 5, false), Some(3));
1335 }
1336
1337 #[test]
1338 fn can_leave_focus_empty_when_nothing_is_selected() {
1339 let selection = BTreeSet::new();
1340
1341 assert_eq!(focus_index_on_focus_in(&selection, 5, false), None);
1342 }
1343
1344 #[test]
1345 fn falls_back_to_first_item_when_enabled() {
1346 let selection = BTreeSet::new();
1347
1348 assert_eq!(focus_index_on_focus_in(&selection, 5, true), Some(0));
1349 }
1350}