use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use teksilo_canvas::{EdgeInsets, Point};
use super::columns::{ColumnGeometry, column_at, geometry_for};
use super::offsets::PrefixSumOffsets;
use super::strategy::{BUFFER_ROWS, GridLayoutStrategy, GridSizing, TileRect, VisibleTileRange};
type ExactHeightFn = Rc<dyn Fn(usize) -> f32>;
pub struct VariableRowGrid {
columns: ColumnGeometry,
row_gap: f32,
estimated: f32,
exact_height: Option<ExactHeightFn>,
offsets: RefCell<PrefixSumOffsets>,
item_count: Cell<usize>,
stored_cols: Cell<usize>,
}
impl std::fmt::Debug for VariableRowGrid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VariableRowGrid")
.field("rows", &self.offsets.borrow().rows())
.field("exact", &self.exact_height.is_some())
.finish()
}
}
impl VariableRowGrid {
pub(crate) fn new(
sizing: GridSizing,
col_gap: f32,
row_gap: f32,
inset: EdgeInsets,
estimated: f32,
exact_height: Option<ExactHeightFn>,
) -> Self {
let estimated = if estimated > 0.0 {
estimated
} else {
sizing.tile_height().max(1.0)
};
Self {
columns: geometry_for(sizing, col_gap, inset),
row_gap: row_gap.max(0.0),
estimated,
exact_height,
offsets: RefCell::new(PrefixSumOffsets::new(
0,
estimated,
row_gap.max(0.0),
inset.top,
inset.bottom,
)),
item_count: Cell::new(0),
stored_cols: Cell::new(0),
}
}
fn reseed_exact(&self, cols: usize) {
let Some(ref ef) = self.exact_height else {
return;
};
let n = self.item_count.get();
let mut off = self.offsets.borrow_mut();
let rows = off.rows();
for r in 0..rows {
let mut h = 0.0_f32;
for i in (r * cols)..((r + 1) * cols).min(n) {
h = h.max(ef(i));
}
off.set_row_height(r, h);
}
}
fn sync(&self, viewport_width: f32) {
let cols = self.columns.column_count(viewport_width).max(1);
let n = self.item_count.get();
let rows = n.div_ceil(cols);
if cols != self.stored_cols.get() {
self.offsets.borrow_mut().reset(rows);
self.stored_cols.set(cols);
self.reseed_exact(cols);
} else if rows != self.offsets.borrow().rows() {
self.offsets.borrow_mut().resize(rows);
self.reseed_exact(cols);
}
}
}
impl GridLayoutStrategy for VariableRowGrid {
fn column_count(&self, viewport_width: f32) -> usize {
self.columns.column_count(viewport_width)
}
fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32) {
self.columns.column_x(col, viewport_width)
}
fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32 {
self.item_count.set(item_count);
self.sync(viewport_width);
self.offsets.borrow_mut().total()
}
fn visible_range(
&self,
scroll_y: f32,
viewport_height: f32,
viewport_width: f32,
item_count: usize,
) -> VisibleTileRange {
self.item_count.set(item_count);
self.sync(viewport_width);
if item_count == 0 {
return VisibleTileRange { start: 0, end: 0 };
}
let cols = self.stored_cols.get().max(1);
let mut off = self.offsets.borrow_mut();
let first_row = off.row_at(scroll_y);
let last_row = off.row_at(scroll_y + viewport_height);
let start_row = first_row.saturating_sub(BUFFER_ROWS);
let end_row = last_row + BUFFER_ROWS;
let start = (start_row * cols).min(item_count);
let end = (end_row.saturating_add(1).saturating_mul(cols)).min(item_count);
VisibleTileRange { start, end }
}
fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect {
self.sync(viewport_width);
let cols = self.stored_cols.get().max(1);
let row = index / cols;
let col = index % cols;
let (x, width) = self.columns.column_x(col, viewport_width);
let mut off = self.offsets.borrow_mut();
let y = off.row_top(row);
let height = off.row_height(row);
TileRect {
x,
y,
width,
height,
}
}
fn estimated_row_height(&self) -> f32 {
self.estimated
}
fn measures_tiles(&self) -> bool {
self.exact_height.is_none()
}
fn observe_measured(
&self,
measured: &[(usize, f32)],
scroll_y: f32,
viewport_width: f32,
) -> f32 {
if self.exact_height.is_some() {
return 0.0;
}
self.sync(viewport_width);
let cols = self.stored_cols.get().max(1);
let mut row_max: HashMap<usize, f32> = HashMap::new();
for &(idx, h) in measured {
let r = idx / cols;
let e = row_max.entry(r).or_insert(0.0);
if h > *e {
*e = h;
}
}
let mut off = self.offsets.borrow_mut();
off.total();
let tops: Vec<(usize, f32, f32)> = row_max
.iter()
.map(|(&r, &h)| (r, off.row_top(r), h))
.collect();
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
}
fn invalidate_rows(&self, item_range: std::ops::Range<usize>) {
let cols = self.stored_cols.get().max(1);
let start_row = item_range.start / cols;
let end_row = if item_range.end == usize::MAX {
self.offsets.borrow().rows()
} else {
item_range.end.div_ceil(cols)
};
self.offsets.borrow_mut().invalidate(start_row, end_row);
}
fn resize(&self, item_count: usize) {
self.item_count.set(item_count);
let cols = self.stored_cols.get().max(1);
let rows = item_count.div_ceil(cols);
self.offsets.borrow_mut().resize(rows);
self.reseed_exact(cols);
}
fn index_at_point(
&self,
content_point: Point,
item_count: usize,
viewport_width: f32,
) -> Option<usize> {
if item_count == 0 {
return None;
}
self.item_count.set(item_count);
self.sync(viewport_width);
let cols = self.stored_cols.get().max(1);
let (row, row_top, row_h) = {
let mut off = self.offsets.borrow_mut();
let row = off.row_at(content_point.y);
(row, off.row_top(row), off.row_height(row))
};
if content_point.y < row_top || content_point.y > row_top + row_h {
return None;
}
let col = column_at(&self.columns, content_point.x, viewport_width)?;
let idx = row * cols + col;
(idx < item_count).then_some(idx)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn grid() -> VariableRowGrid {
VariableRowGrid::new(
GridSizing::Fixed {
width: 100.0,
height: 40.0,
},
10.0,
10.0,
EdgeInsets::ZERO,
40.0,
Some(Rc::new(|_i| 40.0)),
)
}
#[test]
fn index_at_point_closed_form_matches_measured_rows() {
let g = grid();
assert_eq!(g.index_at_point(Point::new(0.0, 0.0), 6, 210.0), Some(0));
assert_eq!(g.index_at_point(Point::new(0.0, 50.0), 6, 210.0), Some(2));
}
#[test]
fn index_at_point_closed_form_returns_none_in_gaps() {
let g = grid();
assert_eq!(g.index_at_point(Point::new(0.0, 45.0), 6, 210.0), None);
assert_eq!(g.index_at_point(Point::new(105.0, 10.0), 6, 210.0), None);
assert_eq!(g.index_at_point(Point::new(0.0, 9000.0), 6, 210.0), None);
}
}