use geometry_core::Rect;
use layout_core::{LayoutError, LayoutStyle};
use renderer_core::RectStyle;
use std::hash::Hash;
use crate::layout_item::LayoutItem;
use crate::reactive_list::ReactiveList;
use crate::scroll_area::ScrollViewport;
use crate::styled_container::StyledContainer;
pub fn visible_window(
offset: f32,
viewport_height: f32,
row_height: f32,
count: usize,
overscan: usize,
) -> (usize, usize) {
if row_height <= 0.0 || viewport_height <= 0.0 || count == 0 {
return (0, count);
}
let first_visible = (offset / row_height).floor().max(0.0) as usize;
let rows_on_screen = (viewport_height / row_height).ceil() as usize + 1;
let first = first_visible.saturating_sub(overscan);
let last = first_visible
.saturating_add(rows_on_screen)
.saturating_add(overscan)
.min(count);
(first.min(count), last)
}
enum Slot<Item> {
Gap {
before: bool,
height: f32,
},
Row(usize, Item),
}
pub struct VirtualList;
impl VirtualList {
pub fn new<Item, Key, S, K, B>(
container_style: LayoutStyle,
viewport: ScrollViewport,
row_height: f32,
overscan: usize,
source: S,
key: K,
build: B,
) -> Result<ReactiveList, LayoutError>
where
Key: Hash + 'static,
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
K: Fn(&Item) -> Key + 'static,
B: Fn(usize, Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
let (_, offset_y) = viewport.offset();
let rect = viewport.rect();
let windowed = move || {
let items = source();
let count = items.len();
let (first, last) = visible_window(
offset_y.get(),
rect.get().height,
row_height,
count,
overscan,
);
let mut slots: Vec<Slot<Item>> = Vec::with_capacity(last - first + 2);
if first > 0 {
slots.push(Slot::Gap {
before: true,
height: first as f32 * row_height,
});
}
slots.extend(
items
.into_iter()
.enumerate()
.skip(first)
.take(last - first)
.map(|(at, item)| Slot::Row(at, item)),
);
if last < count {
slots.push(Slot::Gap {
before: false,
height: (count - last) as f32 * row_height,
});
}
slots
};
let keyer = move |slot: &Slot<Item>| match slot {
Slot::Gap { before, height } => format!("gap:{before}:{height}"),
Slot::Row(at, item) => {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key(item).hash(&mut hasher);
format!("row:{at}:{}", std::hash::Hasher::finish(&hasher))
}
};
ReactiveList::with_style(container_style, windowed, keyer, move |slot| match slot {
Slot::Gap { height, .. } => Ok(Box::new(StyledContainer::new(
LayoutStyle::new().height(height).flex_shrink(0.0),
|_: Rect| RectStyle::default(),
Vec::new(),
)?) as Box<dyn LayoutItem>),
Slot::Row(at, item) => build(at, item),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_window_covers_the_screen_plus_its_overscan() {
let (first, last) = visible_window(0.0, 100.0, 20.0, 1000, 0);
assert_eq!((first, last), (0, 6));
let (first, last) = visible_window(200.0, 100.0, 20.0, 1000, 2);
assert_eq!((first, last), (8, 18));
let (first, _) = visible_window(199.0, 100.0, 20.0, 1000, 0);
assert_eq!(first, 9, "row 9 is still showing its last pixel");
}
#[test]
fn the_window_is_clamped_at_both_ends() {
assert_eq!(visible_window(0.0, 100.0, 20.0, 1000, 5), (0, 11));
assert_eq!(visible_window(19_800.0, 100.0, 20.0, 1000, 5), (985, 1000));
let (first, last) = visible_window(100_000.0, 100.0, 20.0, 10, 0);
assert!(first <= last && last <= 10, "got {first}..{last}");
}
#[test]
fn an_unmeasured_viewport_renders_everything_rather_than_nothing() {
assert_eq!(visible_window(0.0, 0.0, 20.0, 40, 0), (0, 40));
assert_eq!(visible_window(0.0, 100.0, 0.0, 40, 0), (0, 40));
assert_eq!(
visible_window(0.0, 100.0, 20.0, 0, 0),
(0, 0),
"an empty list is empty"
);
}
}