use std::collections::HashMap;
use yog_api::ui::{Action, StateFlags, Units, Widget, widget};
use crate::Update;
pub const SELECT: u16 = Action::SELECT;
const OVERSCAN: u32 = 2;
const FIRST_WINDOW: u32 = 32;
pub struct ItemsHost<T> {
owner: u16,
path: &'static [u16],
item_height: Option<f32>,
scroller: Option<u16>,
selectable: bool,
insts: Vec<(u64, T)>,
empty: Option<Widget>,
showing_empty: bool,
}
impl<T> ItemsHost<T> {
pub fn new(
owner: u16,
path: &'static [u16],
item_height: Option<f32>,
scroller: Option<u16>,
selectable: bool,
) -> Self {
Self {
owner,
path,
item_height,
scroller,
selectable,
insts: Vec::new(),
empty: None,
showing_empty: false,
}
}
pub fn begin<'a>(&'a mut self, root: &'a mut Widget, u: &'a mut Update) -> ItemsCursor<'a, T> {
let container = root.at_path_mut(self.path);
let mut old_children = std::mem::take(&mut container.children);
if self.showing_empty {
self.empty = old_children.pop();
self.showing_empty = false;
}
let window = self.item_height.map(|ih| {
let stride = ih + container.style.gap;
let vp = self.scroller.map(|s| u.viewport(s)).unwrap_or_default();
if vp.height <= 0.0 || stride <= 0.0 {
return (0, FIRST_WINDOW);
}
let top = (vp.offset - container.style.pad[0]).max(0.0);
let first = (top / stride) as u32;
let rows = (vp.height / stride).ceil() as u32 + 1;
(first.saturating_sub(OVERSCAN), first + rows + OVERSCAN)
});
if window.is_some() {
if !old_children.is_empty() {
old_children.remove(0);
old_children.pop();
}
}
let mut old = HashMap::with_capacity(self.insts.len());
for ((key, inst), w) in self.insts.drain(..).zip(old_children) {
old.entry(key).or_insert((inst, w));
}
ItemsCursor {
owner: self.owner,
selectable: self.selectable,
item_height: self.item_height,
window,
host_insts: &mut self.insts,
host_empty: &mut self.empty,
showing_empty: &mut self.showing_empty,
container,
u,
old,
widgets: Vec::new(),
index: 0,
first_built: None,
last_built: 0,
selected: None,
}
}
}
pub struct Item<'c, T> {
pub inst: &'c mut T,
pub w: &'c mut Widget,
pub u: &'c mut Update,
}
pub struct ItemsCursor<'a, T> {
owner: u16,
selectable: bool,
item_height: Option<f32>,
window: Option<(u32, u32)>,
host_insts: &'a mut Vec<(u64, T)>,
host_empty: &'a mut Option<Widget>,
showing_empty: &'a mut bool,
container: &'a mut Widget,
u: &'a mut Update,
old: HashMap<u64, (T, Widget)>,
widgets: Vec<Widget>,
index: u32,
first_built: Option<u32>,
last_built: u32,
selected: Option<u32>,
}
fn stamp(w: &mut Widget, owner: u16, index: u32) {
if let Some(a) = &mut w.action
&& a.owner == owner
{
a.index = index;
}
for c in &mut w.children {
stamp(c, owner, index);
}
}
impl<'a, T> ItemsCursor<'a, T> {
pub fn next(
&mut self,
key: u64,
selected: bool,
build: impl FnOnce() -> (T, Widget),
) -> Option<Item<'_, T>> {
let index = self.index;
self.index += 1;
if selected {
self.selected = Some(index);
}
if let Some((first, last)) = self.window
&& (index < first || index >= last)
{
return None;
}
let (inst, mut w) = match self.old.remove(&key) {
Some(x) => x,
None => {
let (inst, mut w) = build();
if self.selectable && w.action.is_none() {
w.action = Some(Action {
owner: self.owner,
slot: SELECT,
index,
});
}
self.u.relayout();
(inst, w)
}
};
stamp(&mut w, self.owner, index);
if w.state.contains(StateFlags::SELECTED) != selected {
w.set_state_inherited(StateFlags::SELECTED, selected);
self.u.repaint();
}
self.first_built.get_or_insert(index);
self.last_built = index + 1;
self.host_insts.push((key, inst));
self.widgets.push(w);
let (_, inst) = self.host_insts.last_mut().expect("just pushed");
let w = self.widgets.last_mut().expect("just pushed");
Some(Item { inst, w, u: self.u })
}
pub fn finish(self, empty: Option<fn() -> Widget>) -> Option<(&'a mut Widget, &'a mut Update)> {
let n = self.index;
if !self.old.is_empty() {
self.u.relayout();
}
self.u.list_info(self.owner, n, self.selected);
let mut children = self.widgets;
if let (Some(ih), Some(_)) = (self.item_height, self.window) {
let stride = ih + self.container.style.gap;
let gap = self.container.style.gap;
let first = self
.first_built
.unwrap_or(n.min(self.window.map_or(0, |w| w.0)));
let after = n.saturating_sub(self.last_built.max(first));
let spacer = |rows: u32| {
widget::spacer()
.w(Units::Px(0.0))
.h(Units::Px((rows as f32 * stride - gap).max(0.0)))
.visible(rows > 0)
};
children.insert(0, spacer(first));
children.push(spacer(after));
}
if n == 0
&& let Some(build) = empty
{
let w = self.host_empty.take().unwrap_or_else(build);
children.push(w);
*self.showing_empty = true;
self.container.children = children;
let w = self.container.children.last_mut().expect("just pushed");
return Some((w, self.u));
}
self.container.children = children;
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::Viewport;
use yog_api::ui::FlexDir;
struct Row;
fn row() -> (Row, Widget) {
(Row, widget::label(""))
}
fn root() -> Widget {
widget::panel(FlexDir::Column).child(widget::scroll_view().gap(2.0))
}
fn run(
host: &mut ItemsHost<Row>,
root: &mut Widget,
u: &mut Update,
keys: &[u64],
sel: u64,
) -> u32 {
let mut built = 0;
let mut it = host.begin(root, u);
for &k in keys {
if let Some(item) = it.next(k, k == sel, row) {
let Row = item.inst;
item.w.set_text(k.to_string());
built += 1;
}
}
it.finish(Some(|| widget::label("empty")));
built
}
#[test]
fn reconciles_by_key_and_stamps_indices() {
let mut host = ItemsHost::new(1, &[0], None, None, true);
let mut r = root();
let mut u = Update::new();
run(&mut host, &mut r, &mut u, &[10, 20, 30], 20);
let list = &r.children[0];
assert_eq!(list.children.len(), 3);
assert_eq!(list.children[1].action.unwrap().index, 1);
assert!(list.children[1].state.contains(StateFlags::SELECTED));
assert_eq!(u.lists, vec![(1, 3, Some(1))]);
let mut u = Update::new();
run(&mut host, &mut r, &mut u, &[30, 10], 0);
let list = &r.children[0];
assert_eq!(list.children.len(), 2);
assert_eq!(list.children[0].action.unwrap().index, 0);
assert!(!list.children[1].state.contains(StateFlags::SELECTED));
assert!(u.needs_relayout(), "an item was removed");
let mut u = Update::new();
run(&mut host, &mut r, &mut u, &[], 0);
let list = &r.children[0];
assert_eq!(list.children.len(), 1, "empty state shown");
let mut u = Update::new();
run(&mut host, &mut r, &mut u, &[5], 0);
assert_eq!(r.children[0].children.len(), 1);
assert!(host.empty.is_some(), "empty widget kept for later");
}
#[test]
fn virtualizes_with_item_height() {
let mut host = ItemsHost::new(1, &[0], Some(10.0), Some(0), true);
let mut r = root();
let keys: Vec<u64> = (0..1000).collect();
let mut u = Update {
viewports: vec![Viewport {
offset: 1200.0,
height: 60.0,
}],
..Update::new()
};
let built = run(&mut host, &mut r, &mut u, &keys, 0);
assert_eq!(built, 10);
let list = &r.children[0];
assert_eq!(list.children.len(), built as usize + 2);
let top = &list.children[0];
assert_eq!(top.style.h, Units::Px(98.0 * 12.0 - 2.0));
assert_eq!(list.children[1].action.unwrap().index, 98);
let bottom = list.children.last().unwrap();
assert_eq!(bottom.style.h, Units::Px((1000.0 - 108.0) * 12.0 - 2.0));
assert_eq!(u.lists, vec![(1, 1000, Some(0))]);
}
}