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, ColumnOptions, column_at, geometry_for, with_options};
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: Cell<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 with_columns(self, sizing: GridSizing, options: ColumnOptions) -> Self {
63 self.columns
64 .set(with_options(self.columns.get(), sizing, options));
65 self
66 }
67
68 pub(crate) fn new(
69 sizing: GridSizing,
70 col_gap: f32,
71 row_gap: f32,
72 inset: EdgeInsets,
73 estimated: f32,
74 exact_height: Option<ExactHeightFn>,
75 ) -> Self {
76 let estimated = if estimated > 0.0 {
77 estimated
78 } else {
79 sizing.tile_height().max(1.0)
80 };
81 Self {
82 columns: Cell::new(geometry_for(sizing, col_gap, inset)),
83 row_gap: row_gap.max(0.0),
84 estimated,
85 exact_height,
86 offsets: RefCell::new(PrefixSumOffsets::new(
87 0,
88 estimated,
89 row_gap.max(0.0),
90 inset.top,
91 inset.bottom,
92 )),
93 item_count: Cell::new(0),
94 stored_cols: Cell::new(0),
95 }
96 }
97
98 fn reseed_exact(&self, cols: usize) {
101 let Some(ref ef) = self.exact_height else {
102 return;
103 };
104 let n = self.item_count.get();
105 let mut off = self.offsets.borrow_mut();
106 let rows = off.rows();
107 for r in 0..rows {
108 let mut h = 0.0_f32;
109 for i in (r * cols)..((r + 1) * cols).min(n) {
110 h = h.max(ef(i));
111 }
112 off.set_row_height_exact(r, h);
116 }
117 }
118
119 fn sync(&self, viewport_width: f32) {
124 let cols = self.columns.get().column_count(viewport_width).max(1);
125 let n = self.item_count.get();
126 let rows = n.div_ceil(cols);
127
128 if cols != self.stored_cols.get() {
129 self.offsets.borrow_mut().reset(rows);
130 self.stored_cols.set(cols);
131 self.reseed_exact(cols);
132 } else if rows != self.offsets.borrow().rows() {
133 self.offsets.borrow_mut().resize(rows);
134 self.reseed_exact(cols);
135 }
136 }
137}
138
139impl GridLayoutStrategy for VariableRowGrid {
140 fn set_rtl(&self, rtl: bool) {
141 self.columns.set(self.columns.get().with_rtl(rtl));
142 }
143
144 fn column_count(&self, viewport_width: f32) -> usize {
145 self.columns.get().column_count(viewport_width)
146 }
147
148 fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32) {
149 self.columns.get().column_x(col, viewport_width)
150 }
151
152 fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32 {
153 self.item_count.set(item_count);
154 self.sync(viewport_width);
155 self.offsets.borrow_mut().total()
156 }
157
158 fn visible_range(
159 &self,
160 scroll_y: f32,
161 viewport_height: f32,
162 viewport_width: f32,
163 item_count: usize,
164 ) -> VisibleTileRange {
165 self.item_count.set(item_count);
166 self.sync(viewport_width);
167 if item_count == 0 {
168 return VisibleTileRange { start: 0, end: 0 };
169 }
170 let cols = self.stored_cols.get().max(1);
171 let mut off = self.offsets.borrow_mut();
172 let first_row = off.row_at(scroll_y);
173 let last_row = off.row_at(scroll_y + viewport_height);
174 let start_row = first_row.saturating_sub(BUFFER_ROWS);
175 let end_row = last_row + BUFFER_ROWS;
176 let start = (start_row * cols).min(item_count);
177 let end = (end_row.saturating_add(1).saturating_mul(cols)).min(item_count);
178 VisibleTileRange { start, end }
179 }
180
181 fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect {
182 self.sync(viewport_width);
183 let cols = self.stored_cols.get().max(1);
184 let row = index / cols;
185 let col = index % cols;
186 let (x, width) = self.columns.get().column_x(col, viewport_width);
187 let mut off = self.offsets.borrow_mut();
188 let y = off.row_top(row);
189 let height = off.row_height(row);
190 TileRect {
191 x,
192 y,
193 width,
194 height,
195 }
196 }
197
198 fn estimated_row_height(&self) -> f32 {
199 self.estimated
200 }
201
202 fn measures_tiles(&self) -> bool {
203 self.exact_height.is_none()
206 }
207
208 fn observe_measured(
209 &self,
210 measured: &[(usize, f32)],
211 scroll_y: f32,
212 viewport_width: f32,
213 ) -> f32 {
214 if self.exact_height.is_some() {
215 return 0.0;
216 }
217 self.sync(viewport_width);
218 let cols = self.stored_cols.get().max(1);
219
220 let mut row_max: HashMap<usize, f32> = HashMap::new();
222 for &(idx, h) in measured {
223 let r = idx / cols;
224 let e = row_max.entry(r).or_insert(0.0);
225 if h > *e {
226 *e = h;
227 }
228 }
229
230 let mut off = self.offsets.borrow_mut();
231 off.total();
234 let tops: Vec<(usize, f32, f32)> = row_max
235 .iter()
236 .map(|(&r, &h)| (r, off.row_top(r), h))
237 .collect();
238 let mut anchor_delta = 0.0_f32;
239 for (r, top_before, h) in tops {
240 let delta = off.set_row_height(r, h);
241 if delta.abs() > 0.01 && top_before < scroll_y {
246 anchor_delta += delta;
247 }
248 }
249 anchor_delta
250 }
251
252 fn invalidate_rows(&self, item_range: std::ops::Range<usize>) {
253 let cols = self.stored_cols.get().max(1);
254 let start_row = item_range.start / cols;
255 let end_row = if item_range.end == usize::MAX {
256 self.offsets.borrow().rows()
257 } else {
258 item_range.end.div_ceil(cols)
259 };
260 self.offsets.borrow_mut().invalidate(start_row, end_row);
261 }
262
263 fn resize(&self, item_count: usize) {
264 self.item_count.set(item_count);
265 let cols = self.stored_cols.get().max(1);
266 let rows = item_count.div_ceil(cols);
267 self.offsets.borrow_mut().resize(rows);
268 self.reseed_exact(cols);
269 }
270
271 fn index_at_point(
272 &self,
273 content_point: Point,
274 item_count: usize,
275 viewport_width: f32,
276 ) -> Option<usize> {
277 if item_count == 0 {
278 return None;
279 }
280 self.item_count.set(item_count);
281 self.sync(viewport_width);
282 let cols = self.stored_cols.get().max(1);
283 let (row, row_top, row_h) = {
284 let mut off = self.offsets.borrow_mut();
285 let row = off.row_at(content_point.y);
286 (row, off.row_top(row), off.row_height(row))
287 };
288 if content_point.y < row_top || content_point.y > row_top + row_h {
292 return None;
293 }
294 let col = column_at(&self.columns.get(), content_point.x, viewport_width)?;
295 let idx = row * cols + col;
296 (idx < item_count).then_some(idx)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn grid() -> VariableRowGrid {
305 VariableRowGrid::new(
308 GridSizing::Fixed {
309 width: 100.0,
310 height: 40.0,
311 },
312 10.0,
313 10.0,
314 EdgeInsets::ZERO,
315 40.0,
316 Some(Rc::new(|_i| 40.0)),
317 )
318 }
319
320 #[test]
321 fn index_at_point_closed_form_matches_measured_rows() {
322 let g = grid();
323 assert_eq!(g.index_at_point(Point::new(0.0, 0.0), 6, 210.0), Some(0));
326 assert_eq!(g.index_at_point(Point::new(0.0, 50.0), 6, 210.0), Some(2));
327 }
328
329 #[test]
330 fn index_at_point_closed_form_returns_none_in_gaps() {
331 let g = grid();
332 assert_eq!(g.index_at_point(Point::new(0.0, 45.0), 6, 210.0), None);
334 assert_eq!(g.index_at_point(Point::new(105.0, 10.0), 6, 210.0), None);
336 assert_eq!(g.index_at_point(Point::new(0.0, 9000.0), 6, 210.0), None);
338 }
339}