use crate::{Context, Entity, IntoElement, ParentElement, Render, Styled, Window, div, px};
pub struct VirtualScrollState {
pub total_items: usize,
pub item_height: f32,
pub scroll_offset: f32,
pub viewport_height: f32,
pub buffer_size: usize,
}
impl VirtualScrollState {
pub fn new(total_items: usize, item_height: f32) -> Self {
Self {
total_items,
item_height,
scroll_offset: 0.0,
viewport_height: 0.0,
buffer_size: 5,
}
}
pub fn with_buffer_size(mut self, size: usize) -> Self {
self.buffer_size = size;
self
}
pub fn set_scroll_offset(&mut self, offset: f32) {
let max_offset =
(self.total_items as f32 * self.item_height - self.viewport_height).max(0.0);
self.scroll_offset = offset.clamp(0.0, max_offset);
}
pub fn set_viewport_height(&mut self, height: f32) {
self.viewport_height = height;
}
pub fn visible_start_index(&self) -> usize {
let start = (self.scroll_offset / self.item_height) as usize;
start.saturating_sub(self.buffer_size)
}
pub fn visible_end_index(&self) -> usize {
let end = ((self.scroll_offset + self.viewport_height) / self.item_height) as usize;
(end + self.buffer_size).min(self.total_items)
}
pub fn visible_count(&self) -> usize {
self.visible_end_index() - self.visible_start_index()
}
pub fn total_height(&self) -> f32 {
self.total_items as f32 * self.item_height
}
pub fn item_top(&self, index: usize) -> f32 {
index as f32 * self.item_height
}
pub fn item_bottom(&self, index: usize) -> f32 {
(index + 1) as f32 * self.item_height
}
pub fn scroll_to_item(&mut self, index: usize) {
let target_offset = index as f32 * self.item_height;
if target_offset < self.scroll_offset {
self.scroll_offset = target_offset;
} else if target_offset + self.item_height > self.scroll_offset + self.viewport_height {
self.scroll_offset = target_offset + self.item_height - self.viewport_height;
}
}
}
pub struct VirtualScroll {
state: Entity<VirtualScrollState>,
}
impl VirtualScroll {
pub fn new(state: Entity<VirtualScrollState>) -> Self {
Self { state }
}
}
impl Render for VirtualScroll {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let state = self.state.read(cx);
div()
.relative()
.h(px(state.total_height()))
.child(div().absolute().top(px(state.scroll_offset)).w_full())
}
}
pub trait VirtualItemRenderer {
type Item;
fn render_item(&self, item: &Self::Item, index: usize) -> impl IntoElement;
fn item_height(&self, item: &Self::Item) -> f32;
}
pub struct VirtualScrollList<I: Clone> {
state: Entity<VirtualScrollState>,
items: Vec<I>,
renderer: Box<dyn Fn(&I, usize) -> crate::AnyElement>,
}
impl<I: Clone + 'static> VirtualScrollList<I> {
pub fn new(
state: Entity<VirtualScrollState>,
items: Vec<I>,
renderer: impl Fn(&I, usize) -> crate::AnyElement + 'static,
) -> Self {
Self {
state,
items,
renderer: Box::new(renderer),
}
}
pub fn visible_items(&self, cx: &Context<Self>) -> Vec<(usize, &I)> {
let state = self.state.read(cx);
let start = state.visible_start_index();
let end = state.visible_end_index();
self.items[start..end]
.iter()
.enumerate()
.map(|(i, item)| (start + i, item))
.collect()
}
}
impl<I: Clone + 'static> Render for VirtualScrollList<I> {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let visible = self.visible_items(cx);
let state = self.state.read(cx);
div()
.relative()
.h(px(state.total_height()))
.children(visible.into_iter().map(|(index, item)| {
let y = state.item_top(index);
div()
.absolute()
.top(px(y))
.w_full()
.child((self.renderer)(item, index))
}))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VirtualScrollDirection {
Vertical,
Horizontal,
}
#[derive(Debug, Clone)]
pub struct VirtualScrollConfig {
pub direction: VirtualScrollDirection,
pub buffer_size: usize,
pub estimated_item_height: f32,
pub enable_recycling: bool,
}
impl Default for VirtualScrollConfig {
fn default() -> Self {
Self {
direction: VirtualScrollDirection::Vertical,
buffer_size: 5,
estimated_item_height: 30.0,
enable_recycling: true,
}
}
}