teksilo_widgets/grid_view/layout/
variable_row.rs1use std::cell::{Cell, RefCell};
22use std::collections::HashMap;
23use std::rc::Rc;
24
25use teksilo_canvas::{EdgeInsets, Point};
26
27use super::columns::{ColumnGeometry, column_at, geometry_for};
28use super::offsets::PrefixSumOffsets;
29use super::strategy::{BUFFER_ROWS, GridLayoutStrategy, GridSizing, TileRect, VisibleTileRange};
30
31type ExactHeightFn = Rc<dyn Fn(usize) -> f32>;
32
33pub struct VariableRowGrid {
35 columns: ColumnGeometry,
36 row_gap: f32,
37 estimated: f32,
38 exact_height: Option<ExactHeightFn>,
41 offsets: RefCell<PrefixSumOffsets>,
42 item_count: Cell<usize>,
45 stored_cols: Cell<usize>,
48}
49
50impl std::fmt::Debug for VariableRowGrid {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("VariableRowGrid")
53 .field("rows", &self.offsets.borrow().rows())
54 .field("exact", &self.exact_height.is_some())
55 .finish()
56 }
57}
58
59impl VariableRowGrid {
60 pub(crate) fn new(
61 sizing: GridSizing,
62 col_gap: f32,
63 row_gap: f32,
64 inset: EdgeInsets,
65 estimated: f32,
66 exact_height: Option<ExactHeightFn>,
67 ) -> Self {
68 let estimated = if estimated > 0.0 {
69 estimated
70 } else {
71 sizing.tile_height().max(1.0)
72 };
73 Self {
74 columns: geometry_for(sizing, col_gap, inset),
75 row_gap: row_gap.max(0.0),
76 estimated,
77 exact_height,
78 offsets: RefCell::new(PrefixSumOffsets::new(
79 0,
80 estimated,
81 row_gap.max(0.0),
82 inset.top,
83 inset.bottom,
84 )),
85 item_count: Cell::new(0),
86 stored_cols: Cell::new(0),
87 }
88 }
89
90 fn reseed_exact(&self, cols: usize) {
93 let Some(ref ef) = self.exact_height else {
94 return;
95 };
96 let n = self.item_count.get();
97 let mut off = self.offsets.borrow_mut();
98 let rows = off.rows();
99 for r in 0..rows {
100 let mut h = 0.0_f32;
101 for i in (r * cols)..((r + 1) * cols).min(n) {
102 h = h.max(ef(i));
103 }
104 off.set_row_height_exact(r, h);
108 }
109 }
110
111 fn sync(&self, viewport_width: f32) {
116 let cols = self.columns.column_count(viewport_width).max(1);
117 let n = self.item_count.get();
118 let rows = n.div_ceil(cols);
119
120 if cols != self.stored_cols.get() {
121 self.offsets.borrow_mut().reset(rows);
122 self.stored_cols.set(cols);
123 self.reseed_exact(cols);
124 } else if rows != self.offsets.borrow().rows() {
125 self.offsets.borrow_mut().resize(rows);
126 self.reseed_exact(cols);
127 }
128 }
129}
130
131impl GridLayoutStrategy for VariableRowGrid {
132 fn column_count(&self, viewport_width: f32) -> usize {
133 self.columns.column_count(viewport_width)
134 }
135
136 fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32) {
137 self.columns.column_x(col, viewport_width)
138 }
139
140 fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32 {
141 self.item_count.set(item_count);
142 self.sync(viewport_width);
143 self.offsets.borrow_mut().total()
144 }
145
146 fn visible_range(
147 &self,
148 scroll_y: f32,
149 viewport_height: f32,
150 viewport_width: f32,
151 item_count: usize,
152 ) -> VisibleTileRange {
153 self.item_count.set(item_count);
154 self.sync(viewport_width);
155 if item_count == 0 {
156 return VisibleTileRange { start: 0, end: 0 };
157 }
158 let cols = self.stored_cols.get().max(1);
159 let mut off = self.offsets.borrow_mut();
160 let first_row = off.row_at(scroll_y);
161 let last_row = off.row_at(scroll_y + viewport_height);
162 let start_row = first_row.saturating_sub(BUFFER_ROWS);
163 let end_row = last_row + BUFFER_ROWS;
164 let start = (start_row * cols).min(item_count);
165 let end = (end_row.saturating_add(1).saturating_mul(cols)).min(item_count);
166 VisibleTileRange { start, end }
167 }
168
169 fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect {
170 self.sync(viewport_width);
171 let cols = self.stored_cols.get().max(1);
172 let row = index / cols;
173 let col = index % cols;
174 let (x, width) = self.columns.column_x(col, viewport_width);
175 let mut off = self.offsets.borrow_mut();
176 let y = off.row_top(row);
177 let height = off.row_height(row);
178 TileRect {
179 x,
180 y,
181 width,
182 height,
183 }
184 }
185
186 fn estimated_row_height(&self) -> f32 {
187 self.estimated
188 }
189
190 fn measures_tiles(&self) -> bool {
191 self.exact_height.is_none()
194 }
195
196 fn observe_measured(
197 &self,
198 measured: &[(usize, f32)],
199 scroll_y: f32,
200 viewport_width: f32,
201 ) -> f32 {
202 if self.exact_height.is_some() {
203 return 0.0;
204 }
205 self.sync(viewport_width);
206 let cols = self.stored_cols.get().max(1);
207
208 let mut row_max: HashMap<usize, f32> = HashMap::new();
210 for &(idx, h) in measured {
211 let r = idx / cols;
212 let e = row_max.entry(r).or_insert(0.0);
213 if h > *e {
214 *e = h;
215 }
216 }
217
218 let mut off = self.offsets.borrow_mut();
219 off.total();
222 let tops: Vec<(usize, f32, f32)> = row_max
223 .iter()
224 .map(|(&r, &h)| (r, off.row_top(r), h))
225 .collect();
226 let mut anchor_delta = 0.0_f32;
227 for (r, top_before, h) in tops {
228 let delta = off.set_row_height(r, h);
229 if delta.abs() > 0.01 && top_before < scroll_y {
234 anchor_delta += delta;
235 }
236 }
237 anchor_delta
238 }
239
240 fn invalidate_rows(&self, item_range: std::ops::Range<usize>) {
241 let cols = self.stored_cols.get().max(1);
242 let start_row = item_range.start / cols;
243 let end_row = if item_range.end == usize::MAX {
244 self.offsets.borrow().rows()
245 } else {
246 item_range.end.div_ceil(cols)
247 };
248 self.offsets.borrow_mut().invalidate(start_row, end_row);
249 }
250
251 fn resize(&self, item_count: usize) {
252 self.item_count.set(item_count);
253 let cols = self.stored_cols.get().max(1);
254 let rows = item_count.div_ceil(cols);
255 self.offsets.borrow_mut().resize(rows);
256 self.reseed_exact(cols);
257 }
258
259 fn index_at_point(
260 &self,
261 content_point: Point,
262 item_count: usize,
263 viewport_width: f32,
264 ) -> Option<usize> {
265 if item_count == 0 {
266 return None;
267 }
268 self.item_count.set(item_count);
269 self.sync(viewport_width);
270 let cols = self.stored_cols.get().max(1);
271 let (row, row_top, row_h) = {
272 let mut off = self.offsets.borrow_mut();
273 let row = off.row_at(content_point.y);
274 (row, off.row_top(row), off.row_height(row))
275 };
276 if content_point.y < row_top || content_point.y > row_top + row_h {
280 return None;
281 }
282 let col = column_at(&self.columns, content_point.x, viewport_width)?;
283 let idx = row * cols + col;
284 (idx < item_count).then_some(idx)
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn grid() -> VariableRowGrid {
293 VariableRowGrid::new(
296 GridSizing::Fixed {
297 width: 100.0,
298 height: 40.0,
299 },
300 10.0,
301 10.0,
302 EdgeInsets::ZERO,
303 40.0,
304 Some(Rc::new(|_i| 40.0)),
305 )
306 }
307
308 #[test]
309 fn index_at_point_closed_form_matches_measured_rows() {
310 let g = grid();
311 assert_eq!(g.index_at_point(Point::new(0.0, 0.0), 6, 210.0), Some(0));
314 assert_eq!(g.index_at_point(Point::new(0.0, 50.0), 6, 210.0), Some(2));
315 }
316
317 #[test]
318 fn index_at_point_closed_form_returns_none_in_gaps() {
319 let g = grid();
320 assert_eq!(g.index_at_point(Point::new(0.0, 45.0), 6, 210.0), None);
322 assert_eq!(g.index_at_point(Point::new(105.0, 10.0), 6, 210.0), None);
324 assert_eq!(g.index_at_point(Point::new(0.0, 9000.0), 6, 210.0), None);
326 }
327}