use std::{
collections::BTreeSet,
ops::{Deref, Range},
};
use crate::prelude::*;
pub struct VirtualList {
scroll_to_cursor: Signal<bool>,
on_scroll: Option<Box<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
num_items: Signal<usize>,
item_height: f32,
visible_range: Signal<Range<usize>>,
scroll_x: Signal<f32>,
scroll_y: Signal<f32>,
show_horizontal_scrollbar: Signal<bool>,
show_vertical_scrollbar: Signal<bool>,
selection: Signal<BTreeSet<usize>>,
selectable: Signal<Selectable>,
focused: Signal<Option<usize>>,
selection_follows_focus: Signal<bool>,
on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
}
impl VirtualList {
fn evaluate_index(index: usize, start: usize, end: usize) -> usize {
match end - start {
0 => 0,
len => start + (len - (start % len) + index) % len,
}
}
fn recalc(&self, cx: &mut EventContext) {
let num_items = self.num_items.get();
if num_items == 0 {
self.visible_range.set_if_changed(0..0);
return;
}
let current = cx.current();
let current_height = cx.cache.get_height(current);
if current_height == f32::MAX {
return;
}
let item_height = self.item_height;
let total_height = item_height * (num_items as f32);
let visible_height = current_height / cx.scale_factor();
let mut num_visible_items = (visible_height / item_height).ceil();
num_visible_items += 1.0;
let visible_items_height = item_height * num_visible_items;
let empty_height = (total_height - visible_items_height).max(0.0);
let visible_start = empty_height * self.scroll_y.get();
let visible_end = visible_start + visible_items_height;
let mut start_index = (visible_start / item_height).trunc() as usize;
let mut end_index = 1 + (visible_end / item_height).trunc() as usize;
let desired_range_size = (num_visible_items as usize) + 1;
end_index = end_index.min(num_items);
let current_range_size = end_index.saturating_sub(start_index);
if current_range_size < desired_range_size {
match end_index == num_items {
true => {
start_index =
start_index.saturating_sub(desired_range_size - current_range_size);
}
false if end_index < num_items => {
end_index = (start_index + desired_range_size).min(num_items);
}
_ => {}
}
}
self.visible_range.set_if_changed(start_index..end_index);
}
}
impl VirtualList {
pub fn new<V: View, S, L, T>(
cx: &mut Context,
list: S,
item_height: f32,
item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
) -> Handle<Self>
where
S: Res<L> + 'static,
L: Deref<Target = [T]> + Clone + 'static,
T: Clone + PartialEq + 'static,
{
Self::new_generic(
cx,
list,
|list| list.len(),
|list, index| list[index].clone(),
item_height,
item_content,
)
}
pub fn new_generic<V: View, S, L, T>(
cx: &mut Context,
list: S,
list_len: impl 'static + Fn(&L) -> usize,
list_index: impl 'static + Copy + Fn(&L, usize) -> T,
item_height: f32,
item_content: impl 'static + Copy + Fn(&mut Context, usize, Memo<T>) -> Handle<V>,
) -> Handle<Self>
where
S: Res<L> + 'static,
L: Clone + 'static,
T: Clone + PartialEq + 'static,
{
let list = list.to_signal(cx);
let num_items = list.map(list_len).to_signal(cx);
let visible_range = Signal::new(0..0);
let scroll_x = Signal::new(0.0);
let scroll_y = Signal::new(0.0);
let show_horizontal_scrollbar = Signal::new(false);
let show_vertical_scrollbar = Signal::new(true);
let selection = Signal::new(BTreeSet::default());
let selectable = Signal::new(Selectable::None);
let focused = Signal::new(None);
let selection_follows_focus = Signal::new(false);
let scroll_to_cursor = Signal::new(true);
Self {
scroll_to_cursor,
on_scroll: None,
num_items,
item_height,
visible_range,
scroll_x,
scroll_y,
show_horizontal_scrollbar,
show_vertical_scrollbar,
selection,
selectable,
focused,
selection_follows_focus,
on_select: None,
}
.build(cx, |cx| {
Keymap::from(vec![
(
KeyChord::new(Modifiers::empty(), Code::ArrowDown),
KeymapEntry::new("Focus Next", |cx| cx.emit(ListEvent::FocusNext)),
),
(
KeyChord::new(Modifiers::empty(), Code::ArrowUp),
KeymapEntry::new("Focus Previous", |cx| cx.emit(ListEvent::FocusPrev)),
),
(
KeyChord::new(Modifiers::empty(), Code::Space),
KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
),
(
KeyChord::new(Modifiers::empty(), Code::Enter),
KeymapEntry::new("Select Focused", |cx| cx.emit(ListEvent::SelectFocused)),
),
])
.build(cx);
ScrollView::new(cx, move |cx| {
Binding::new(cx, num_items, move |cx| {
let num_items = num_items.get();
cx.emit(ScrollEvent::SetY(0.0));
VStack::new(cx, |cx| {
let num_visible_items = visible_range.map(Range::len);
Binding::new(cx, num_visible_items, move |cx| {
for i in 0..num_visible_items.get().min(num_items) {
let item_index = visible_range.map(move |range| {
Self::evaluate_index(i, range.start, range.end)
});
Binding::new(cx, item_index, move |cx| {
let index = item_index.get();
let item = list.map(move |list| list_index(list, index));
ListItem::new(
cx,
index,
item,
selection,
focused,
move |cx, index, item| {
item_content(cx, index, item).height(Percentage(100.0));
},
)
.min_width(Auto)
.height(Pixels(item_height))
.position_type(PositionType::Absolute)
.bind(
item_index,
move |handle| {
let index = item_index.get();
handle.top(Pixels(index as f32 * item_height));
},
);
});
}
})
})
.height(Pixels(num_items as f32 * item_height));
})
})
.show_horizontal_scrollbar(show_horizontal_scrollbar)
.show_vertical_scrollbar(show_vertical_scrollbar)
.scroll_to_cursor(scroll_to_cursor)
.scroll_x(scroll_x)
.scroll_y(scroll_y)
.on_scroll(|cx, x, y| {
if y.is_finite() && x.is_finite() {
cx.emit(ListEvent::Scroll(x, y));
}
});
})
.toggle_class("selectable", selectable.map(|s| *s != Selectable::None))
.navigable(true)
.role(Role::ListBox)
}
}
impl View for VirtualList {
fn element(&self) -> Option<&'static str> {
Some("virtual-list")
}
fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
event.take(|list_event, meta| match list_event {
ListEvent::Select(index) => {
cx.focus();
let selectable = self.selectable.get();
let mut selection = self.selection.get();
let mut focused = self.focused.get();
match selectable {
Selectable::Single => {
if selection.contains(&index) {
selection.clear();
focused = None;
} else {
selection.clear();
selection.insert(index);
focused = Some(index);
if let Some(on_select) = &self.on_select {
on_select(cx, index);
}
}
}
Selectable::Multi => {
if selection.contains(&index) {
selection.remove(&index);
focused = None;
} else {
selection.insert(index);
focused = Some(index);
if let Some(on_select) = &self.on_select {
on_select(cx, index);
}
}
}
Selectable::None => {}
}
self.selection.set(selection);
self.focused.set(focused);
meta.consume();
}
ListEvent::SelectFocused => {
if let Some(focused) = self.focused.get() {
cx.emit(ListEvent::Select(focused))
}
meta.consume();
}
ListEvent::ClearSelection => {
self.selection.set(BTreeSet::default());
meta.consume();
}
ListEvent::FocusNext => {
let mut focused = self.focused.get();
let num_items = self.num_items.get();
if let Some(f) = &mut focused {
if *f < num_items.saturating_sub(1) {
*f = f.saturating_add(1);
if self.selection_follows_focus.get() {
cx.emit(ListEvent::SelectFocused);
}
}
} else {
focused = Some(0);
if self.selection_follows_focus.get() {
cx.emit(ListEvent::SelectFocused);
}
}
self.focused.set(focused);
meta.consume();
}
ListEvent::FocusPrev => {
let mut focused = self.focused.get();
let num_items = self.num_items.get();
if let Some(f) = &mut focused {
if *f > 0 {
*f = f.saturating_sub(1);
if self.selection_follows_focus.get() {
cx.emit(ListEvent::SelectFocused);
}
}
} else {
focused = Some(num_items.saturating_sub(1));
if self.selection_follows_focus.get() {
cx.emit(ListEvent::SelectFocused);
}
}
self.focused.set(focused);
meta.consume();
}
ListEvent::Scroll(x, y) => {
self.scroll_x.set(x);
self.scroll_y.set(y);
self.recalc(cx);
if let Some(callback) = &self.on_scroll {
(callback)(cx, x, y);
}
meta.consume();
}
});
event.map(|window_event, _| match window_event {
WindowEvent::GeometryChanged(geo) => {
if geo.intersects(GeoChanged::WIDTH_CHANGED | GeoChanged::HEIGHT_CHANGED) {
self.recalc(cx);
}
}
_ => {}
});
}
}
impl Handle<'_, VirtualList> {
pub fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
where
R: Deref<Target = [usize]> + Clone + 'static,
{
let selection = selection.to_signal(self.cx);
self.bind(selection, move |handle| {
selection.with(|selected_indices| {
handle.modify(|list| {
let mut selection = BTreeSet::default();
let mut focused = None;
for idx in selected_indices.deref().iter().copied() {
selection.insert(idx);
focused = Some(idx);
}
list.selection.set(selection);
list.focused.set(focused);
});
});
})
}
pub fn on_select<F>(self, callback: F) -> Self
where
F: 'static + Fn(&mut EventContext, usize),
{
self.modify(|list| list.on_select = Some(Box::new(callback)))
}
pub fn selectable<U: Into<Selectable> + Clone + 'static>(
self,
selectable: impl Res<U> + 'static,
) -> Self {
let selectable = selectable.to_signal(self.cx);
self.bind(selectable, move |handle| {
let selectable = selectable.get();
let s = selectable.into();
handle.modify(|list| list.selectable.set(s));
})
}
pub fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
self,
flag: impl Res<U> + 'static,
) -> Self {
let flag = flag.to_signal(self.cx);
self.bind(flag, move |handle| {
let selection_follows_focus = flag.get();
let s = selection_follows_focus.into();
handle.modify(|list| list.selection_follows_focus.set(s));
})
}
pub fn scroll_to_cursor(self, flag: bool) -> Self {
self.modify(|virtual_list: &mut VirtualList| {
virtual_list.scroll_to_cursor.set(flag);
})
}
pub fn on_scroll(
self,
callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
) -> Self {
self.modify(|list| list.on_scroll = Some(Box::new(callback)))
}
pub fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
let scrollx = scrollx.to_signal(self.cx);
self.bind(scrollx, move |handle| {
let sx = scrollx.get();
handle.modify(|list| list.scroll_x.set(sx));
})
}
pub fn scroll_y(self, scrollx: impl Res<f32> + 'static) -> Self {
let scrollx = scrollx.to_signal(self.cx);
self.bind(scrollx, move |handle| {
let sy = scrollx.get();
handle.modify(|list| list.scroll_y.set(sy));
})
}
pub fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
let flag = flag.to_signal(self.cx);
self.bind(flag, move |handle| {
let s = flag.get();
handle.modify(|list| list.show_horizontal_scrollbar.set(s));
})
}
pub fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
let flag = flag.to_signal(self.cx);
self.bind(flag, move |handle| {
let s = flag.get();
handle.modify(|list| list.show_vertical_scrollbar.set(s));
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn evaluate_indices(range: Range<usize>) -> Vec<usize> {
(0..range.len())
.map(|index| VirtualList::evaluate_index(index, range.start, range.end))
.collect()
}
#[test]
fn test_evaluate_index() {
assert_eq!(evaluate_indices(0..4), [0, 1, 2, 3]);
assert_eq!(evaluate_indices(1..5), [4, 1, 2, 3]);
assert_eq!(evaluate_indices(2..6), [4, 5, 2, 3]);
assert_eq!(evaluate_indices(3..7), [4, 5, 6, 3]);
assert_eq!(evaluate_indices(4..8), [4, 5, 6, 7]);
assert_eq!(evaluate_indices(5..9), [8, 5, 6, 7]);
assert_eq!(evaluate_indices(6..10), [8, 9, 6, 7]);
assert_eq!(evaluate_indices(7..11), [8, 9, 10, 7]);
assert_eq!(evaluate_indices(8..12), [8, 9, 10, 11]);
assert_eq!(evaluate_indices(9..13), [12, 9, 10, 11]);
}
}