use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use std::rc::Rc;
use smudge_core::element::{Element, IntoElement, for_each};
use smudge_core::paint::PaintOp;
use smudge_core::reactive::{Memo, RenderEffect, RwSignal, prelude::*};
use smudge_core::style::px;
use smudge_core::widget::{PaintCx, Widget, panel};
use crate::height_index::HeightIndex;
use crate::scroll::{ScrollExt, ScrollView, scroll_with_visible_height};
const OVERSCAN: usize = 4;
const MIN_WINDOW_CAP: usize = 256;
struct Snapshot<T>(Rc<[T]>);
impl<T> PartialEq for Snapshot<T> {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
struct KeyedList<T: 'static, K: Eq + Hash + Clone> {
items: RefCell<Rc<[T]>>,
index: HeightIndex<K>,
measured: HashMap<K, f32>,
anchor: Option<(K, f32)>,
}
pub fn virtual_list_keyed<T: 'static, K: Eq + Hash + Clone + 'static, E: IntoElement>(
items: impl Fn() -> Rc<[T]> + 'static,
key: impl Fn(&T) -> K + 'static,
estimate: f32,
row: impl Fn(&T) -> E + 'static,
) -> Element<ScrollView> {
let estimate = if estimate.is_finite() && estimate > 0.0 {
estimate
} else {
0.0
};
let current_items = Memo::new(move || Snapshot(items()));
let key = Rc::new(key);
let row = Rc::new(row);
let state: Rc<RefCell<KeyedList<T, K>>> = Rc::new(RefCell::new(KeyedList {
items: RefCell::new(Rc::from([])),
index: HeightIndex::new(),
measured: HashMap::new(),
anchor: None,
}));
let offset = RwSignal::new(0.0);
let visible: RwSignal<Option<f32>> = RwSignal::new(None);
let total = RwSignal::new(0.0);
let window: RwSignal<Vec<K>> = RwSignal::new(Vec::new());
let pin: RwSignal<Option<(usize, f32)>> = RwSignal::new(None);
let reported = RwSignal::new(0u64);
let pending: Rc<RefCell<Vec<(K, f32)>>> = Rc::new(RefCell::new(Vec::new()));
{
let state = Rc::clone(&state);
let current_items = current_items.clone();
let key = Rc::clone(&key);
let offset = offset.clone();
let visible = visible.clone();
let total = total.clone();
let window = window.clone();
let pin = pin.clone();
let reported = reported.clone();
let pending = Rc::clone(&pending);
RenderEffect::new(move || {
let snapshot = current_items.with(|snapshot| Rc::clone(&snapshot.0));
let batch = if reported.get() > 0 {
std::mem::take(&mut *pending.borrow_mut())
} else {
Vec::new()
};
let viewport = visible.get();
let mut wanted: f32 = offset.get();
let mut geometry_changed = false;
{
let mut state = state.borrow_mut();
if !state.index.is_empty() {
let at = anchor_row(&state.index, wanted, viewport);
let within = wanted - state.index.offset_of(at);
state.anchor = Some((state.index.key(at).clone(), within));
}
let same_items = Rc::ptr_eq(&*state.items.borrow(), &snapshot);
*state.items.borrow_mut() = snapshot;
if estimate == 0.0 {
state.index = HeightIndex::new();
state.anchor = None;
} else {
if !same_items {
let list = &mut *state;
let items = Rc::clone(&*list.items.borrow());
let old_len = list.index.len();
let appended = items.len() >= old_len
&& items[..old_len]
.iter()
.zip(list.index.keys())
.all(|(item, old)| key(item) == *old);
if appended {
for item in &items[old_len..] {
let item_key = key(item);
let height =
list.measured.get(&item_key).copied().unwrap_or(estimate);
list.index.push(item_key, height);
}
} else {
let keys: Vec<K> = items.iter().map(|item| key(item)).collect();
let live: HashSet<&K> = keys.iter().collect();
list.measured.retain(|item_key, _| live.contains(item_key));
let measured = &list.measured;
list.index = HeightIndex::from_heights(keys, |missing| {
measured.get(missing).copied().unwrap_or(estimate)
});
geometry_changed = true;
}
}
for (item_key, height) in batch {
if state.measured.get(&item_key) != Some(&height) {
state.measured.insert(item_key.clone(), height);
if let Some(at) = state.index.position(&item_key) {
state.index.set_height(at, height);
geometry_changed = true;
}
}
}
if geometry_changed
&& let Some((anchor_key, within)) = state.anchor.take()
&& let Some(at) = state.index.position(&anchor_key)
{
wanted = state.index.offset_of(at) + within;
}
}
}
if let Some(viewport) = viewport {
let max = (state.borrow().index.total() - viewport).max(0.0);
wanted = wanted.clamp(0.0, max);
}
if wanted != offset.read(|value| *value) {
offset.set(wanted);
}
let anchor_at = {
let mut state = state.borrow_mut();
let anchor_at = if state.index.is_empty() {
state.anchor = None;
None
} else {
let at = anchor_row(&state.index, wanted, viewport);
let top = state.index.offset_of(at);
state.anchor = Some((state.index.key(at).clone(), wanted - top));
Some((at, top))
};
let end = state.index.total();
if total.read(|value| *value) != end {
total.set(end);
}
anchor_at
};
let state = state.borrow();
let range = match (viewport, anchor_at) {
(Some(viewport), Some((at, _))) if estimate > 0.0 => {
Some(window_range(&state.index, viewport, wanted, at))
}
_ => None,
};
let next = range.map_or(&[][..], |(start, end)| &state.index.keys()[start..=end]);
let next_pin = anchor_at
.zip(range)
.map(|((at, top), (start, _))| (at - start, top));
if window.read(|value| value.as_slice() != next) {
window.update(|value| {
value.clear();
value.extend_from_slice(next);
});
}
if pin.read(|value| *value != next_pin) {
pin.set(next_pin);
}
});
}
let rows = {
let state = Rc::clone(&state);
let window = window.clone();
let row = Rc::clone(&row);
for_each(
move || window.get(),
|item_key| item_key.clone(),
move |item_key| {
let content = {
let state = state.borrow();
let at = state.index.position(&item_key);
let snapshot = state.items.borrow();
match at.and_then(|at| snapshot.get(at)) {
Some(item) => row(item).into_any(),
None => panel().into_any(),
}
};
let report_state = Rc::clone(&state);
let report_key = item_key.clone();
let report_signal = reported.clone();
let report_cell = Rc::clone(&pending);
let report = move |height: f32| {
let height = height.max(0.0);
if report_state.borrow().measured.get(&report_key) != Some(&height) {
report_cell.borrow_mut().push((report_key.clone(), height));
report_signal.update(|count| *count += 1);
}
};
Element::new(MeasureRow {
report: Rc::new(report),
})
.shrink(0.0)
.child(content)
},
)
.column()
.shrink(0.0)
.pin_child(move || pin.get())
};
let content = panel()
.column()
.height({
let total = total.clone();
move || px(total.get())
})
.child(rows);
scroll_with_visible_height(content, visible, Some(total)).offset(offset)
}
fn window_cap(viewport: f32) -> usize {
MIN_WINDOW_CAP.max((viewport.ceil() as usize).saturating_add(1)) + 2 * OVERSCAN
}
fn anchor_row<K: Eq + Hash + Clone>(
index: &HeightIndex<K>,
offset: f32,
viewport: Option<f32>,
) -> usize {
let at = index.index_at(offset);
if index.offset_of(at) != offset {
return at;
}
let viewport = viewport.unwrap_or(0.0);
let last = index.index_at(offset + viewport);
let spare = window_cap(viewport).saturating_sub(2 * OVERSCAN + (last - at) + 1);
let mut first = at;
while first > 0 && at - first < spare && index.height(first - 1) == 0.0 {
first -= 1;
}
first
}
fn window_range<K: Eq + Hash + Clone>(
index: &HeightIndex<K>,
viewport: f32,
offset: f32,
first: usize,
) -> (usize, usize) {
let last = index.index_at(offset + viewport);
let start = first.saturating_sub(OVERSCAN);
let end = (last + OVERSCAN)
.min(index.len() - 1)
.min(start.saturating_add(window_cap(viewport) - 1));
(start, end)
}
struct MeasureRow {
report: Rc<dyn Fn(f32)>,
}
impl Widget for MeasureRow {
fn paint(&self, cx: &PaintCx<'_>, _out: &mut Vec<PaintOp>) {
(self.report)(cx.rect.height());
}
}