use std::cell::RefCell;
use std::rc::Rc;
use teksilo_canvas::Rect;
use teksilo_core::widget::EventContext;
use super::row_offsets::PrefixSumOffsets;
pub(crate) type SharedRowMetrics = Rc<RefCell<RowMetrics>>;
pub(crate) fn chase_row_into_outer_view(
ctx: &mut EventContext,
metrics: &SharedRowMetrics,
viewport: Rect,
idx: usize,
scroll_y: f32,
) {
let (top, height) = {
let mut m = metrics.borrow_mut();
(m.row_top(idx), m.row_height(idx))
};
let rect = Rect::new(
viewport.x,
viewport.y + top - scroll_y,
viewport.width,
height,
);
ctx.ensure_visible(rect);
}
pub(crate) enum RowMode {
Uniform { item_height: f32, spacing: f32 },
Exact(Rc<dyn Fn(usize) -> f32>),
AutoMeasure { estimated: f32 },
}
impl std::fmt::Debug for RowMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uniform {
item_height,
spacing,
} => f
.debug_struct("Uniform")
.field("item_height", item_height)
.field("spacing", spacing)
.finish(),
Self::Exact(_) => f.debug_tuple("Exact").field(&"<fn>").finish(),
Self::AutoMeasure { estimated } => f
.debug_struct("AutoMeasure")
.field("estimated", estimated)
.finish(),
}
}
}
#[derive(Default)]
pub(crate) enum HeightSource {
#[default]
Uniform,
Exact(Rc<dyn Fn(usize) -> f32>),
Auto {
estimated: f32,
},
}
impl HeightSource {
pub(crate) fn make_metrics(&self, item_height: f32, spacing: f32) -> RowMetrics {
match self {
Self::Uniform => RowMetrics::uniform(item_height, spacing),
Self::Exact(f) => RowMetrics::exact(f.clone(), spacing),
Self::Auto { estimated } => RowMetrics::auto_measure(*estimated, spacing),
}
}
}
impl std::fmt::Debug for HeightSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uniform => f.write_str("Uniform"),
Self::Exact(_) => f.write_str("Exact(<fn>)"),
Self::Auto { estimated } => write!(f, "Auto {{ estimated: {estimated} }}"),
}
}
}
#[derive(Debug)]
pub(crate) struct RowMetrics {
mode: RowMode,
spacing: f32,
offsets: Option<PrefixSumOffsets>,
count: usize,
}
impl RowMetrics {
pub(crate) fn uniform(item_height: f32, spacing: f32) -> Self {
Self {
mode: RowMode::Uniform {
item_height,
spacing,
},
spacing,
offsets: None,
count: 0,
}
}
pub(crate) fn exact(height_fn: Rc<dyn Fn(usize) -> f32>, spacing: f32) -> Self {
Self {
mode: RowMode::Exact(height_fn),
spacing,
offsets: Some(PrefixSumOffsets::new(0, 1.0, spacing, 0.0, 0.0)),
count: 0,
}
}
pub(crate) fn auto_measure(estimated: f32, spacing: f32) -> Self {
let estimated = estimated.max(1.0);
Self {
mode: RowMode::AutoMeasure { estimated },
spacing,
offsets: Some(PrefixSumOffsets::new(0, estimated, spacing, 0.0, 0.0)),
count: 0,
}
}
pub(crate) fn needs_measure(&self) -> bool {
matches!(self.mode, RowMode::AutoMeasure { .. })
}
#[allow(dead_code)]
pub(crate) fn is_uniform(&self) -> bool {
matches!(self.mode, RowMode::Uniform { .. })
}
fn step(&self) -> f32 {
match &self.mode {
RowMode::Uniform {
item_height,
spacing,
} => item_height + spacing,
_ => 0.0,
}
}
pub(crate) fn resize(&mut self, count: usize) {
let old = self.count;
self.count = count;
if let Some(off) = &mut self.offsets {
off.resize(count);
if count > old
&& let RowMode::Exact(f) = &self.mode
{
for i in old..count {
off.set_row_height(i, f(i));
}
}
}
}
pub(crate) fn reset(&mut self, count: usize) {
self.count = count;
if let Some(off) = &mut self.offsets {
off.reset(count);
if let RowMode::Exact(f) = &self.mode {
for i in 0..count {
off.set_row_height(i, f(i));
}
}
}
}
pub(crate) fn invalidate_from(&mut self, start: usize) {
let count = self.count;
match (&self.mode, &mut self.offsets) {
(RowMode::Exact(f), Some(off)) => {
for i in start..count {
off.set_row_height(i, f(i));
}
}
(RowMode::AutoMeasure { .. }, Some(off)) => {
off.invalidate(start, count);
}
_ => {}
}
}
pub(crate) fn apply_divergence(&mut self, divergence: Option<usize>, new_count: usize) {
match divergence {
Some(d) => {
self.resize(new_count);
self.invalidate_from(d);
}
None => self.reset(new_count),
}
}
pub(crate) fn total_height(&mut self, count: usize) -> f32 {
self.resize(count);
match &self.mode {
RowMode::Uniform {
item_height,
spacing,
} => {
if count == 0 {
0.0
} else {
count as f32 * (item_height + spacing) - spacing
}
}
_ => self.offsets.as_mut().map(|o| o.total()).unwrap_or_default(),
}
}
pub(crate) fn row_top(&mut self, i: usize) -> f32 {
match &self.mode {
RowMode::Uniform { .. } => i as f32 * self.step(),
_ => self
.offsets
.as_mut()
.map(|o| o.row_top(i))
.unwrap_or_default(),
}
}
pub(crate) fn row_height(&mut self, i: usize) -> f32 {
match &self.mode {
RowMode::Uniform { item_height, .. } => *item_height,
_ => self
.offsets
.as_ref()
.map(|o| o.row_height(i))
.unwrap_or_default(),
}
}
pub(crate) fn row_at(&mut self, y: f32) -> usize {
if self.count == 0 {
return 0;
}
match &self.mode {
RowMode::Uniform { .. } => {
let step = self.step();
if step <= 0.0 {
return 0;
}
((y.max(0.0) / step).floor() as usize).min(self.count - 1)
}
_ => self
.offsets
.as_mut()
.map(|o| o.row_at(y))
.unwrap_or_default(),
}
}
pub(crate) fn visible_range(
&mut self,
scroll: f32,
viewport: f32,
count: usize,
buffer: usize,
) -> (usize, usize) {
self.resize(count);
if count == 0 {
return (0, 0);
}
let scroll = scroll.max(0.0);
match &self.mode {
RowMode::Uniform { .. } => {
let step = self.step();
if step <= 0.0 {
return (0, count);
}
let first_visible = (scroll / step).floor() as usize;
let last_visible = ((scroll + viewport) / step).ceil() as usize;
let start = first_visible.saturating_sub(buffer);
let end = (last_visible + buffer).min(count);
(start, end)
}
_ => {
let Some(off) = self.offsets.as_mut() else {
return (0, count);
};
let start = off.row_at(scroll).saturating_sub(buffer);
let bottom = (scroll + viewport - 0.01).max(scroll);
let end = (off.row_at(bottom) + 1 + buffer).min(count);
(start, end)
}
}
}
pub(crate) fn insertion_index(&mut self, y: f32) -> usize {
if self.count == 0 || y < 0.0 {
return 0;
}
let r = self.row_at(y);
let top = self.row_top(r);
let span = self.row_height(r) + self.spacing;
if y - top >= span * 0.5 {
(r + 1).min(self.count)
} else {
r
}
}
pub(crate) fn scroll_for_ensure_visible(
&mut self,
i: usize,
scroll: f32,
viewport: f32,
max_scroll: f32,
) -> f32 {
let top = self.row_top(i);
let bottom = top + self.row_height(i);
if top < scroll {
top.max(0.0)
} else if bottom > scroll + viewport {
(bottom - viewport).clamp(0.0, max_scroll.max(0.0))
} else {
scroll
}
}
pub(crate) fn observe_measured(&mut self, measured: &[(usize, f32)], scroll_y: f32) -> f32 {
if !self.needs_measure() {
return 0.0;
}
let Some(off) = self.offsets.as_mut() else {
return 0.0;
};
let _ = off.total();
let mut tops = Vec::with_capacity(measured.len());
for &(r, h) in measured {
tops.push((r, off.row_top(r), h));
}
let mut anchor_delta = 0.0_f32;
for (r, top_before, h) in tops {
let delta = off.set_row_height(r, h);
if delta.abs() > 0.01 && top_before < scroll_y {
anchor_delta += delta;
}
}
anchor_delta
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uniform(h: f32, sp: f32, count: usize) -> RowMetrics {
let mut m = RowMetrics::uniform(h, sp);
m.resize(count);
m
}
#[test]
fn uniform_total_matches_old_formula() {
let mut m = uniform(32.0, 0.0, 10);
assert_eq!(m.total_height(10), 320.0);
let mut m = uniform(40.0, 8.0, 3);
assert_eq!(m.total_height(3), 136.0);
assert_eq!(m.total_height(0), 0.0);
}
#[test]
fn uniform_visible_range_matches_old_floor_ceil() {
let mut m = uniform(30.0, 0.0, 1000);
assert_eq!(m.visible_range(95.0, 300.0, 1000, 0), (3, 14));
assert_eq!(m.visible_range(95.0, 300.0, 1000, 5), (0, 19));
assert_eq!(m.visible_range(90.0, 300.0, 1000, 0), (3, 13));
}
#[test]
fn uniform_row_lookups() {
let mut m = uniform(40.0, 8.0, 5);
assert_eq!(m.row_top(2), 96.0); assert_eq!(m.row_height(2), 40.0);
assert_eq!(m.row_at(95.0), 1); assert_eq!(m.row_at(96.0), 2);
assert_eq!(m.row_at(-5.0), 0);
assert_eq!(m.row_at(99999.0), 4);
}
#[test]
fn uniform_insertion_index_matches_old_midpoint() {
let mut m = uniform(30.0, 0.0, 10);
assert_eq!(m.insertion_index(0.0), 0);
assert_eq!(m.insertion_index(14.0), 0);
assert_eq!(m.insertion_index(16.0), 1);
assert_eq!(m.insertion_index(290.0), 10); assert_eq!(m.insertion_index(-3.0), 0);
let mut m = uniform(40.0, 8.0, 10);
assert_eq!(m.insertion_index(23.0), 0);
assert_eq!(m.insertion_index(25.0), 1);
}
fn heights_fn(hs: &'static [f32]) -> Rc<dyn Fn(usize) -> f32> {
Rc::new(move |i| hs.get(i).copied().unwrap_or(10.0))
}
#[test]
fn exact_positions_rows_from_callback() {
let mut m = RowMetrics::exact(heights_fn(&[100.0, 20.0, 50.0]), 0.0);
m.resize(3);
assert_eq!(m.row_top(0), 0.0);
assert_eq!(m.row_top(1), 100.0);
assert_eq!(m.row_top(2), 120.0);
assert_eq!(m.total_height(3), 170.0);
assert_eq!(m.row_at(119.0), 1);
assert_eq!(m.row_at(120.0), 2);
}
#[test]
fn exact_with_spacing() {
let mut m = RowMetrics::exact(heights_fn(&[100.0, 20.0, 50.0]), 8.0);
m.resize(3);
assert_eq!(m.row_top(1), 108.0);
assert_eq!(m.row_top(2), 136.0);
assert_eq!(m.total_height(3), 186.0);
}
#[test]
fn exact_resize_seeds_only_appended_rows() {
let mut m = RowMetrics::exact(heights_fn(&[100.0, 20.0, 50.0, 30.0]), 0.0);
m.resize(2);
assert_eq!(m.total_height(2), 120.0);
m.resize(4); assert_eq!(m.row_top(3), 170.0);
assert_eq!(m.total_height(4), 200.0);
}
#[test]
fn exact_invalidate_from_reseeds_from_callback() {
let mut m = RowMetrics::exact(heights_fn(&[100.0, 20.0, 50.0]), 0.0);
m.resize(3);
m.invalidate_from(1);
assert_eq!(m.row_top(2), 120.0);
assert_eq!(m.total_height(3), 170.0);
}
#[test]
fn insertion_index_threshold_form_with_tall_short_tall() {
let mut m = RowMetrics::exact(heights_fn(&[40.0, 10.0, 40.0]), 0.0);
m.resize(3);
assert_eq!(m.insertion_index(35.0), 1);
assert_eq!(m.insertion_index(42.0), 1);
assert_eq!(m.insertion_index(47.0), 2);
assert_eq!(m.insertion_index(85.0), 3);
}
#[test]
fn auto_seeds_at_estimate_and_corrects_on_observe() {
let mut m = RowMetrics::auto_measure(50.0, 0.0);
m.resize(4);
assert_eq!(m.total_height(4), 200.0);
let delta = m.observe_measured(&[(0, 30.0), (1, 30.0)], 0.0);
assert_eq!(delta, 0.0);
assert_eq!(m.row_top(1), 30.0);
assert_eq!(m.row_top(2), 60.0);
assert_eq!(m.total_height(4), 160.0); }
#[test]
fn auto_anchor_delta_only_for_rows_strictly_above_viewport() {
let mut m = RowMetrics::auto_measure(50.0, 0.0);
m.resize(4);
let delta = m.observe_measured(&[(0, 90.0), (2, 60.0)], 100.0);
assert!((delta - 40.0).abs() < 0.01);
}
#[test]
fn auto_invalidate_then_remeasure_does_not_double_count() {
let mut m = RowMetrics::auto_measure(50.0, 0.0);
m.resize(4);
let d1 = m.observe_measured(&[(0, 90.0)], 100.0);
assert!((d1 - 40.0).abs() < 0.01);
m.invalidate_from(0); let d2 = m.observe_measured(&[(0, 90.0)], 100.0);
assert!((d2 - 40.0).abs() < 0.01);
}
#[test]
fn auto_resize_preserves_measured_prefix() {
let mut m = RowMetrics::auto_measure(50.0, 0.0);
m.resize(2);
m.observe_measured(&[(0, 90.0)], 0.0);
m.resize(4);
assert_eq!(m.row_top(1), 90.0); assert_eq!(m.row_top(2), 140.0); }
#[test]
fn auto_sub_epsilon_jitter_is_absorbed() {
let mut m = RowMetrics::auto_measure(50.0, 0.0);
m.resize(2);
m.observe_measured(&[(0, 90.0)], 0.0);
let delta = m.observe_measured(&[(0, 90.005)], 100.0);
assert_eq!(delta, 0.0);
assert_eq!(m.row_top(1), 90.0);
}
#[test]
fn apply_divergence_keeps_measured_prefix() {
let mut m = RowMetrics::auto_measure(50.0, 0.0);
m.resize(3);
m.observe_measured(&[(0, 90.0), (1, 70.0), (2, 60.0)], 0.0);
m.apply_divergence(Some(3), 5);
assert_eq!(m.row_top(1), 90.0);
assert_eq!(m.row_top(2), 160.0);
assert_eq!(m.row_top(3), 220.0); m.apply_divergence(Some(1), 5);
assert_eq!(m.row_top(1), 90.0);
assert_eq!(m.row_top(2), 140.0); }
#[test]
fn apply_divergence_none_resets_everything() {
let mut m = RowMetrics::auto_measure(50.0, 0.0);
m.resize(2);
m.observe_measured(&[(0, 90.0)], 0.0);
m.apply_divergence(None, 2);
assert_eq!(m.row_top(1), 50.0);
}
#[test]
fn scroll_for_ensure_visible_variable_heights() {
let mut m = RowMetrics::exact(heights_fn(&[100.0, 20.0, 50.0, 200.0]), 0.0);
m.resize(4);
assert_eq!(m.scroll_for_ensure_visible(3, 0.0, 100.0, 270.0), 270.0);
assert_eq!(m.scroll_for_ensure_visible(0, 150.0, 100.0, 270.0), 0.0);
assert_eq!(m.scroll_for_ensure_visible(2, 100.0, 100.0, 270.0), 100.0);
}
#[test]
fn offsets_visible_range_boundary_matches_ceil_semantics() {
let mut e = RowMetrics::exact(Rc::new(|_| 30.0), 0.0);
e.resize(1000);
let mut u = uniform(30.0, 0.0, 1000);
assert_eq!(
e.visible_range(90.0, 300.0, 1000, 0),
u.visible_range(90.0, 300.0, 1000, 0)
);
assert_eq!(
e.visible_range(95.0, 300.0, 1000, 0),
u.visible_range(95.0, 300.0, 1000, 0)
);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn arb_height() -> impl Strategy<Value = f32> {
prop_oneof![
4 => Just(0.0_f32),
1 => Just(1.0_f32),
3 => 0.5f32..400.0f32,
]
}
fn arb_heights(max_len: usize) -> impl Strategy<Value = Vec<f32>> {
prop::collection::vec(arb_height(), 0..=max_len)
}
fn arb_gap() -> impl Strategy<Value = f32> {
prop_oneof![3 => Just(0.0_f32), 1 => 0.5f32..20.0f32]
}
fn arb_y() -> impl Strategy<Value = f32> {
prop_oneof![
1 => Just(0.0_f32),
1 => -500.0f32..0.0f32,
1 => 0.0f32..2000.0f32,
1 => Just(100_000.0_f32),
]
}
fn height_fn(heights: Vec<f32>) -> Rc<dyn Fn(usize) -> f32> {
Rc::new(move |i| heights.get(i).copied().unwrap_or(0.0))
}
fn expected_total(heights: &[f32], gap: f32) -> f32 {
let rows = heights.len();
if rows == 0 {
return 0.0;
}
let sum_heights: f32 = heights.iter().sum();
sum_heights + (rows as f32 - 1.0) * gap
}
proptest! {
#[test]
fn insertion_index_is_always_in_0_equals_n(
heights in arb_heights(40),
gap in arb_gap(),
y in arb_y(),
) {
let n = heights.len();
let mut m = RowMetrics::exact(height_fn(heights.clone()), gap);
m.resize(n);
let idx = m.insertion_index(y);
prop_assert!(
idx <= n,
"insertion_index({}) = {} exceeds row count {} (heights={:?}, gap={})",
y, idx, n, heights, gap,
);
}
}
proptest! {
#[test]
fn insertion_index_is_monotone_non_decreasing_in_y(
heights in arb_heights(40),
gap in arb_gap(),
y1 in arb_y(),
y2 in arb_y(),
) {
let (lo, hi) = if y1 <= y2 { (y1, y2) } else { (y2, y1) };
let n = heights.len();
let mut m = RowMetrics::exact(height_fn(heights.clone()), gap);
m.resize(n);
let idx_lo = m.insertion_index(lo);
let idx_hi = m.insertion_index(hi);
prop_assert!(
idx_lo <= idx_hi,
"insertion_index({}) = {} > insertion_index({}) = {}, not monotone \
(heights={:?}, gap={})",
lo, idx_lo, hi, idx_hi, heights, gap,
);
}
}
proptest! {
#[test]
fn uniform_and_exact_constant_height_modes_agree_on_every_query(
item_height in arb_height(),
spacing in arb_gap(),
count in 0usize..=40,
y in arb_y(),
) {
let mut u = RowMetrics::uniform(item_height, spacing);
u.resize(count);
let e_fn: Rc<dyn Fn(usize) -> f32> = Rc::new(move |_| item_height);
let mut e = RowMetrics::exact(e_fn, spacing);
e.resize(count);
let total_u = u.total_height(count);
let total_e = e.total_height(count);
prop_assert!(
(total_u - total_e).abs() < 0.05,
"total_height disagrees: uniform={} exact={} (item_height={}, spacing={}, count={})",
total_u, total_e, item_height, spacing, count,
);
if count > 0 {
for i in 0..count {
let top_u = u.row_top(i);
let top_e = e.row_top(i);
prop_assert!(
(top_u - top_e).abs() < 0.05,
"row_top({}) disagrees: uniform={} exact={} \
(item_height={}, spacing={}, count={})",
i, top_u, top_e, item_height, spacing, count,
);
}
let row_u = u.row_at(y);
let row_e = e.row_at(y);
prop_assert_eq!(
row_u, row_e,
"row_at({}) disagrees: uniform={} exact={} (item_height={}, spacing={}, \
count={}) — expected to diverge when item_height==0.0 && spacing==0.0: \
Uniform's `step <= 0.0 => return 0` fallback vs the offset table's \
tie-break-to-the-last-tied-row",
y, row_u, row_e, item_height, spacing, count,
);
let ins_u = u.insertion_index(y);
let ins_e = e.insertion_index(y);
prop_assert_eq!(
ins_u, ins_e,
"insertion_index({}) disagrees: uniform={} exact={} \
(item_height={}, spacing={}, count={})",
y, ins_u, ins_e, item_height, spacing, count,
);
}
}
}
proptest! {
#[test]
fn invalidate_from_preserves_the_measured_prefix_exactly(
estimated in 1.0f32..200.0f32,
spacing in arb_gap(),
(measured_heights, k) in arb_heights(30).prop_flat_map(|heights| {
let len = heights.len();
(Just(heights), 0..=len)
}),
) {
let count = measured_heights.len();
let mut m = RowMetrics::auto_measure(estimated, spacing);
m.resize(count);
let observations: Vec<(usize, f32)> = measured_heights
.iter()
.copied()
.enumerate()
.collect();
m.observe_measured(&observations, 0.0);
let before: Vec<f32> = (0..k).map(|i| m.row_top(i)).collect();
m.invalidate_from(k);
let after: Vec<f32> = (0..k).map(|i| m.row_top(i)).collect();
prop_assert_eq!(
&before, &after,
"invalidate_from({}) disturbed the measured prefix [0,{}): before={:?} after={:?} \
(estimated={}, spacing={}, count={})",
k, k, before, after, estimated, spacing, count,
);
}
}
proptest! {
#[test]
fn exact_mode_total_height_conserves_the_callback_driven_sum(
heights in arb_heights(40),
gap in arb_gap(),
) {
let n = heights.len();
let mut m = RowMetrics::exact(height_fn(heights.clone()), gap);
let actual = m.total_height(n);
let expected = expected_total(&heights, gap);
prop_assert!(
(actual - expected).abs() < 0.05,
"total_height({})={} but the direct sum of the callback's heights plus gaps \
is {} (heights={:?}, gap={})",
n, actual, expected, heights, gap,
);
}
}
}