use crate::ecs::{Entity, World};
use crate::event::scroll::ScrollOffset;
use crate::types::Fixed;
use crate::widget::dirty::Dirty;
use alloc::vec::Vec;
pub struct LazyList {
pub item_count: u32,
pub item_height: Fixed,
pub pool_size: u8,
pub visible_start: u32,
}
impl LazyList {
pub fn new(item_count: u32, item_height: impl Into<Fixed>, pool_size: u8) -> Self {
Self {
item_count,
item_height: item_height.into(),
pool_size,
visible_start: 0,
}
}
}
pub struct LazyListPool {
pub items: Vec<Entity>,
pub bound_indices: Vec<u32>,
}
impl LazyListPool {
pub fn new(items: Vec<Entity>) -> Self {
let n = items.len();
Self {
items,
bound_indices: alloc::vec![u32::MAX; n],
}
}
}
pub type ItemBinder = fn(&mut crate::ecs::World, Entity, u32);
pub struct LazyListBinder {
pub bind: ItemBinder,
}
struct ListContext {
item_count: u32,
item_height: Fixed,
pool_size: u32,
visible_start: u32,
items: Vec<Entity>,
bound_indices: Vec<u32>,
binder: ItemBinder,
}
fn collect_list_context(world: &World, entity: Entity) -> Option<ListContext> {
let (item_count, item_height, pool_size) = world
.get::<LazyList>(entity)
.map(|l| (l.item_count, l.item_height, l.pool_size as u32))?;
if pool_size == 0 || item_height <= Fixed::ZERO {
return None;
}
let scroll_y = world
.get::<ScrollOffset>(entity)
.map(|s| s.y)
.unwrap_or(Fixed::ZERO);
let raw_start = (scroll_y / item_height).to_int();
let visible_start = (raw_start.max(0) as u32).min(item_count.saturating_sub(pool_size));
let pool = world.get::<LazyListPool>(entity)?;
let binder = world.get::<LazyListBinder>(entity).map(|b| b.bind)?;
Some(ListContext {
item_count,
item_height,
pool_size,
visible_start,
items: pool.items.clone(),
bound_indices: pool.bound_indices.clone(),
binder,
})
}
fn apply_bindings(world: &mut World, entity: Entity, ctx: ListContext) -> bool {
let mut new_bindings = ctx.bound_indices;
let mut any_changed = false;
let pool_size = ctx.pool_size as usize;
for (i, slot) in ctx.items.iter().enumerate().take(pool_size) {
let target = ctx.visible_start + i as u32;
if target >= ctx.item_count {
continue;
}
if new_bindings[i] != target {
(ctx.binder)(world, *slot, target);
new_bindings[i] = target;
any_changed = true;
}
let y = ctx.item_height * Fixed::from_int(target as i32);
crate::widget::set_position(world, *slot, Fixed::ZERO, y);
}
if let Some(pool) = world.get_mut::<LazyListPool>(entity) {
pool.bound_indices = new_bindings;
}
if let Some(list) = world.get_mut::<LazyList>(entity) {
list.visible_start = ctx.visible_start;
}
any_changed
}
pub fn lazy_list_system(world: &mut World) {
let lists: Vec<Entity> = world.query::<LazyList>().collect();
for entity in lists {
let Some(ctx) = collect_list_context(world, entity) else {
continue;
};
if apply_bindings(world, entity, ctx) {
world.insert(entity, Dirty);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::{LayoutStyle, Position};
use crate::types::Dimension;
use crate::widget::{Style, Widget};
#[test]
fn pool_starts_unbound() {
let stub = Entity {
id: 0,
generation: 0,
};
let pool = LazyListPool::new(alloc::vec![stub; 5]);
assert_eq!(pool.items.len(), 5);
assert!(pool.bound_indices.iter().all(|&i| i == u32::MAX));
}
struct BindTrace(alloc::vec::Vec<u32>);
fn recording_binder(world: &mut World, _entity: Entity, index: u32) {
let trace = world.resource_mut::<BindTrace>().expect("trace resource");
trace.0.push(index);
}
fn make_slot(world: &mut World, list: Entity) -> Entity {
let e = world.spawn();
world.insert(e, Widget);
world.insert(
e,
Style {
layout: LayoutStyle {
position: Position::Absolute,
left: Dimension::Px(Fixed::ZERO),
top: Dimension::Px(Fixed::ZERO),
width: Dimension::Px(Fixed::from_int(100)),
height: Dimension::Px(Fixed::from_int(40)),
..Default::default()
},
..Default::default()
},
);
world.insert(e, crate::widget::Parent(list));
e
}
#[test]
fn first_tick_binds_all_slots_top_down() {
let mut world = World::default();
let list = world.spawn();
let pool: Vec<Entity> = (0..5).map(|_| make_slot(&mut world, list)).collect();
world.insert(list, Widget);
world.insert(list, Style::default());
world.insert(list, LazyList::new(100, 40, 5));
world.insert(list, LazyListPool::new(pool.clone()));
world.insert(
list,
LazyListBinder {
bind: recording_binder,
},
);
world.insert_resource(BindTrace(alloc::vec::Vec::new()));
lazy_list_system(&mut world);
let trace = &world.resource::<BindTrace>().unwrap().0;
assert_eq!(trace, &alloc::vec![0u32, 1, 2, 3, 4]);
let bound = &world.get::<LazyListPool>(list).unwrap().bound_indices;
assert_eq!(bound, &alloc::vec![0u32, 1, 2, 3, 4]);
}
#[test]
fn scroll_one_row_rebinds_one_slot() {
let mut world = World::default();
let list = world.spawn();
let pool: Vec<Entity> = (0..5).map(|_| make_slot(&mut world, list)).collect();
world.insert(list, Widget);
world.insert(list, Style::default());
world.insert(list, LazyList::new(100, 40, 5));
world.insert(list, LazyListPool::new(pool.clone()));
world.insert(
list,
LazyListBinder {
bind: recording_binder,
},
);
world.insert_resource(BindTrace(alloc::vec::Vec::new()));
lazy_list_system(&mut world);
world.resource_mut::<BindTrace>().unwrap().0.clear();
world.insert(
list,
ScrollOffset {
x: Fixed::ZERO,
y: Fixed::from_int(40),
},
);
lazy_list_system(&mut world);
let trace = &world.resource::<BindTrace>().unwrap().0;
assert_eq!(trace.len(), 5);
assert_eq!(trace, &alloc::vec![1u32, 2, 3, 4, 5]);
}
}