use std::any::Any;
use std::collections::HashMap;
use std::ops::Range;
use frust_core::{AnyView, BuildCtx, ChangeFlags, ChildPod};
use crate::ChildKey;
pub(crate) struct ItemViews<'a, State: 'static> {
pub(crate) prev: &'a dyn Fn(usize) -> AnyView<State>,
pub(crate) next: &'a dyn Fn(usize) -> AnyView<State>,
}
pub(crate) struct LiveRows<'a> {
pub(crate) children: &'a mut Vec<ChildPod>,
pub(crate) indices: &'a mut Vec<usize>,
}
pub(crate) struct KeyedOutcome {
pub(crate) flags: ChangeFlags,
pub(crate) slot_keys: Vec<ChildKey>,
pub(crate) any_survivor: bool,
pub(crate) departed: Vec<(ChildKey, usize)>,
}
pub(crate) fn reconcile_positional<State: 'static>(
views: ItemViews<'_, State>,
rows: LiveRows<'_>,
range: Range<usize>,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let (start, end) = (range.start, range.end);
let mut flags = ChangeFlags::NONE;
let old_keys = std::mem::take(rows.indices);
let old_children = std::mem::take(rows.children);
let mut old: HashMap<usize, ChildPod> = old_keys.into_iter().zip(old_children).collect();
let mut new_children = Vec::with_capacity(end.saturating_sub(start));
let mut new_keys = Vec::with_capacity(end.saturating_sub(start));
let mut structural = false;
for index in start..end {
if let Some(mut pod) = old.remove(&index) {
let prev_view = (views.prev)(index);
let next_view = (views.next)(index);
flags |= crate::authoring::rebuild_child(&prev_view, &next_view, &mut pod, ctx);
new_children.push(pod);
} else {
new_children.push(crate::authoring::build_child(&(views.next)(index), ctx));
structural = true;
}
new_keys.push(index);
}
for (index, mut pod) in old.drain() {
crate::authoring::teardown_child(&(views.prev)(index), &mut pod, ctx);
structural = true;
}
*rows.children = new_children;
*rows.indices = new_keys;
if structural {
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
flags
}
pub(crate) fn reconcile_keyed<State: 'static>(
views: ItemViews<'_, State>,
rows: LiveRows<'_>,
key_index: &mut HashMap<ChildKey, usize>,
key_of: &dyn Fn(usize) -> ChildKey,
range: Range<usize>,
duplicate_key_msg: &str,
ctx: &mut BuildCtx<'_>,
) -> KeyedOutcome {
reconcile_keyed_retaining(
views,
rows,
key_index,
key_of,
range,
duplicate_key_msg,
None,
ctx,
)
}
pub(crate) struct KeptRow {
pub(crate) pod: ChildPod,
pub(crate) view: Box<dyn Any>,
pub(crate) index: usize,
}
impl KeptRow {
pub(crate) fn teardown<State: 'static>(mut self, ctx: &mut BuildCtx<'_>) {
if let Some(view) = self.view.downcast_ref::<AnyView<State>>() {
crate::authoring::teardown_child(view, &mut self.pod, ctx);
}
}
}
pub(crate) struct Retention<'a> {
pub(crate) kept: &'a mut HashMap<ChildKey, KeptRow>,
pub(crate) wants: &'a dyn Fn(&ChildPod) -> bool,
pub(crate) locate: &'a dyn Fn(ChildKey, usize) -> Option<usize>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn reconcile_keyed_retaining<State: 'static>(
views: ItemViews<'_, State>,
rows: LiveRows<'_>,
key_index: &mut HashMap<ChildKey, usize>,
key_of: &dyn Fn(usize) -> ChildKey,
range: Range<usize>,
duplicate_key_msg: &str,
mut retention: Option<Retention<'_>>,
ctx: &mut BuildCtx<'_>,
) -> KeyedOutcome {
let (start, end) = (range.start, range.end);
let capacity = end.saturating_sub(start);
let mut slot_keys: Vec<ChildKey> = Vec::with_capacity(capacity);
let mut next_index_of: HashMap<ChildKey, usize> = HashMap::with_capacity(capacity);
let mut duplicate = false;
for index in start..end {
let key = key_of(index);
duplicate |= next_index_of.insert(key, index).is_some();
slot_keys.push(key);
}
debug_assert!(!duplicate, "{}", duplicate_key_msg);
let prev_index_of = std::mem::take(key_index);
let old_keys = std::mem::take(rows.indices);
let old_children = std::mem::take(rows.children);
let window_shifted =
old_keys.first().copied() != (capacity > 0).then_some(start) || old_keys.len() != capacity;
let mut old: HashMap<usize, ChildPod> = old_keys.into_iter().zip(old_children).collect();
let mut new_children = Vec::with_capacity(capacity);
let mut new_keys = Vec::with_capacity(capacity);
let mut flags = ChangeFlags::NONE;
let mut structural = window_shifted;
let mut any_survivor = false;
for (index, key) in (start..end).zip(slot_keys.iter().copied()) {
let survivor = prev_index_of
.get(&key)
.copied()
.and_then(|prev_index| old.remove(&prev_index).map(|pod| (prev_index, pod)));
match survivor {
Some((prev_index, mut pod)) => {
any_survivor = true;
let prev_view = (views.prev)(prev_index);
let next_view = (views.next)(index);
flags |= crate::authoring::rebuild_child(&prev_view, &next_view, &mut pod, ctx);
if prev_index != index {
structural = true;
}
new_children.push(pod);
}
None => {
let parked = retention
.as_mut()
.and_then(|retention| retention.kept.remove(&key));
let pod = match parked {
Some(KeptRow { mut pod, view, .. }) => {
let next_view = (views.next)(index);
match view.downcast_ref::<AnyView<State>>() {
Some(prev_view) => {
flags |= crate::authoring::rebuild_child(
prev_view, &next_view, &mut pod, ctx,
);
pod
}
None => crate::authoring::build_child(&next_view, ctx),
}
}
None => crate::authoring::build_child(&(views.next)(index), ctx),
};
new_children.push(pod);
structural = true;
}
}
new_keys.push(index);
}
let mut departed: Vec<(ChildKey, usize)> = Vec::new();
for (key, prev_index) in prev_index_of {
let Some(mut pod) = old.remove(&prev_index) else {
continue;
};
structural = true;
if let Some(retention) = retention.as_mut()
&& (retention.wants)(&pod)
&& let Some(index) = (retention.locate)(key, prev_index)
{
if pod.is_active() {
crate::authoring::cancel_pod(&mut pod);
pod.set_active(false);
}
retention.kept.insert(
key,
KeptRow {
pod,
view: Box::new((views.prev)(prev_index)),
index,
},
);
continue;
}
crate::authoring::teardown_child(&(views.prev)(prev_index), &mut pod, ctx);
departed.push((key, prev_index));
}
for (index, mut pod) in old.drain() {
crate::authoring::teardown_child(&(views.prev)(index), &mut pod, ctx);
structural = true;
}
*rows.children = new_children;
*rows.indices = new_keys;
*key_index = next_index_of;
if structural {
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
KeyedOutcome {
flags,
slot_keys,
any_survivor,
departed,
}
}
pub(crate) const KEY_SCAN_RADIUS: usize = 64;
pub(crate) fn probe_key(
key: ChildKey,
prev_index: usize,
delta: isize,
item_count: usize,
key_of: &dyn Fn(usize) -> ChildKey,
) -> Option<usize> {
if prev_index < item_count && key_of(prev_index) == key {
return Some(prev_index);
}
if delta != 0 {
let candidate = prev_index as isize + delta;
if candidate >= 0 && (candidate as usize) < item_count && key_of(candidate as usize) == key
{
return Some(candidate as usize);
}
}
None
}
pub(crate) fn locate_key(
key: ChildKey,
prev_index: usize,
delta: isize,
item_count: usize,
key_of: &dyn Fn(usize) -> ChildKey,
) -> Option<usize> {
if item_count == 0 {
return None;
}
if let Some(index) = probe_key(key, prev_index, delta, item_count, key_of) {
return Some(index);
}
let centre = (prev_index as isize + delta / 2).clamp(0, item_count as isize - 1);
let matches = |candidate: isize| {
candidate >= 0 && (candidate as usize) < item_count && key_of(candidate as usize) == key
};
for step in 0..=KEY_SCAN_RADIUS as isize {
if matches(centre - step) {
return Some((centre - step) as usize);
}
if step > 0 && matches(centre + step) {
return Some((centre + step) as usize);
}
}
None
}
pub(crate) const MAX_PREFIX_STEP: usize = 512;
#[derive(Clone, Copy, Debug)]
pub(crate) struct Measured {
pub(crate) index: usize,
pub(crate) extent: f64,
}
pub(crate) fn record_measurement(
cache: &mut HashMap<ChildKey, Measured>,
sum: &mut f64,
key: ChildKey,
index: usize,
extent: f64,
) {
match cache.insert(key, Measured { index, extent }) {
Some(previous) => *sum += extent - previous.extent,
None => *sum += extent,
}
}
pub(crate) fn forget_measurement(
cache: &mut HashMap<ChildKey, Measured>,
sum: &mut f64,
key: &ChildKey,
) {
if let Some(previous) = cache.remove(key) {
*sum -= previous.extent;
}
}
pub(crate) fn evict_measurements_past(
cache: &mut HashMap<ChildKey, Measured>,
sum: &mut f64,
item_count: usize,
) {
if cache.is_empty() {
return;
}
let mut dropped = 0.0;
cache.retain(|_, entry| {
let live = entry.index < item_count;
if !live {
dropped += entry.extent;
}
live
});
*sum -= dropped;
}
pub(crate) fn estimated_total(
measured_sum: f64,
measured_count: usize,
item_count: usize,
estimate: f64,
) -> f64 {
let unmeasured = item_count.saturating_sub(measured_count);
measured_sum + estimate * unmeasured as f64
}
pub(crate) fn walk_to_offset(
item_count: usize,
index: usize,
y: f64,
target: f64,
estimate: f64,
extent_at: impl Fn(usize) -> f64,
) -> (usize, f64) {
debug_assert!(item_count > 0, "walk_to_offset needs a non-empty list");
let mut index = index.min(item_count - 1);
let mut y = y;
let gap = target - y;
if estimate > 0.0 && gap.abs() > estimate * MAX_PREFIX_STEP as f64 {
let jump = (gap / estimate).trunc();
let landed = (index as f64 + jump).clamp(0.0, (item_count - 1) as f64);
y += (landed - index as f64) * estimate;
index = landed as usize;
}
let mut steps = 0usize;
while y > target && index > 0 && steps < MAX_PREFIX_STEP {
index -= 1;
y -= extent_at(index);
steps += 1;
}
while index + 1 < item_count && steps < MAX_PREFIX_STEP {
let extent = extent_at(index);
if y + extent <= target {
y += extent;
index += 1;
steps += 1;
} else {
break;
}
}
(index, y)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn locate_key_resolves_both_hypotheses_and_a_two_sided_move() {
let key = ChildKey::new(100u64);
let shifted: Vec<u64> = (1000..1003).chain(0..200).collect();
let calls = Cell::new(0u32);
let key_of_shifted = |i: usize| {
calls.set(calls.get() + 1);
ChildKey::new(shifted[i])
};
assert_eq!(
probe_key(key, 100, 3, shifted.len(), &key_of_shifted),
Some(103)
);
assert_eq!(calls.get(), 2);
let both: Vec<u64> = (1000..1003).chain(0..200).chain(5000..5005).collect();
let key_of = |i: usize| ChildKey::new(both[i]);
assert_eq!(probe_key(key, 100, 8, both.len(), &key_of), None);
assert_eq!(locate_key(key, 100, 8, both.len(), &key_of), Some(103));
assert_eq!(
locate_key(ChildKey::new(9999u64), 100, 8, both.len(), &key_of),
None
);
assert_eq!(locate_key(key, 0, 0, 0, &key_of), None);
}
#[test]
fn the_measured_cache_keeps_its_sum_exact() {
let mut cache = HashMap::new();
let mut sum = 0.0;
record_measurement(&mut cache, &mut sum, ChildKey::new(1u64), 0, 40.0);
record_measurement(&mut cache, &mut sum, ChildKey::new(2u64), 1, 60.0);
record_measurement(&mut cache, &mut sum, ChildKey::new(1u64), 0, 45.0);
assert_eq!(sum, 105.0);
assert_eq!(estimated_total(sum, cache.len(), 10, 50.0), 505.0);
forget_measurement(&mut cache, &mut sum, &ChildKey::new(1u64));
assert_eq!(sum, 60.0);
evict_measurements_past(&mut cache, &mut sum, 1);
assert!(cache.is_empty());
assert_eq!(sum, 0.0);
}
#[test]
fn the_prefix_walk_steps_over_measured_extents_and_jumps_in_closed_form() {
let extent = |i: usize| if i.is_multiple_of(2) { 40.0 } else { 60.0 };
assert_eq!(walk_to_offset(100, 0, 0.0, 130.0, 50.0, extent), (2, 100.0));
assert_eq!(walk_to_offset(100, 4, 200.0, 50.0, 50.0, extent), (1, 40.0));
assert_eq!(walk_to_offset(3, 0, 0.0, 10_000.0, 50.0, extent).0, 2);
assert_eq!(walk_to_offset(3, 2, 70.0, 0.0, 50.0, extent), (0, -30.0));
let (index, y) = walk_to_offset(1_000_000, 0, 0.0, 5_000_000.0, 50.0, |_| 50.0);
assert_eq!((index, y), (100_000, 5_000_000.0));
}
}