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(r, h);
105 }
106 }
107
108 fn sync(&self, viewport_width: f32) {
113 let cols = self.columns.column_count(viewport_width).max(1);
114 let n = self.item_count.get();
115 let rows = n.div_ceil(cols);
116
117 if cols != self.stored_cols.get() {
118 self.offsets.borrow_mut().reset(rows);
119 self.stored_cols.set(cols);
120 self.reseed_exact(cols);
121 } else if rows != self.offsets.borrow().rows() {
122 self.offsets.borrow_mut().resize(rows);
123 self.reseed_exact(cols);
124 }
125 }
126}
127
128impl GridLayoutStrategy for VariableRowGrid {
129 fn column_count(&self, viewport_width: f32) -> usize {
130 self.columns.column_count(viewport_width)
131 }
132
133 fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32) {
134 self.columns.column_x(col, viewport_width)
135 }
136
137 fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32 {
138 self.item_count.set(item_count);
139 self.sync(viewport_width);
140 self.offsets.borrow_mut().total()
141 }
142
143 fn visible_range(
144 &self,
145 scroll_y: f32,
146 viewport_height: f32,
147 viewport_width: f32,
148 item_count: usize,
149 ) -> VisibleTileRange {
150 self.item_count.set(item_count);
151 self.sync(viewport_width);
152 if item_count == 0 {
153 return VisibleTileRange { start: 0, end: 0 };
154 }
155 let cols = self.stored_cols.get().max(1);
156 let mut off = self.offsets.borrow_mut();
157 let first_row = off.row_at(scroll_y);
158 let last_row = off.row_at(scroll_y + viewport_height);
159 let start_row = first_row.saturating_sub(BUFFER_ROWS);
160 let end_row = last_row + BUFFER_ROWS;
161 let start = (start_row * cols).min(item_count);
162 let end = (end_row.saturating_add(1).saturating_mul(cols)).min(item_count);
163 VisibleTileRange { start, end }
164 }
165
166 fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect {
167 self.sync(viewport_width);
168 let cols = self.stored_cols.get().max(1);
169 let row = index / cols;
170 let col = index % cols;
171 let (x, width) = self.columns.column_x(col, viewport_width);
172 let mut off = self.offsets.borrow_mut();
173 let y = off.row_top(row);
174 let height = off.row_height(row);
175 TileRect {
176 x,
177 y,
178 width,
179 height,
180 }
181 }
182
183 fn estimated_row_height(&self) -> f32 {
184 self.estimated
185 }
186
187 fn measures_tiles(&self) -> bool {
188 self.exact_height.is_none()
191 }
192
193 fn observe_measured(
194 &self,
195 measured: &[(usize, f32)],
196 scroll_y: f32,
197 viewport_width: f32,
198 ) -> f32 {
199 if self.exact_height.is_some() {
200 return 0.0;
201 }
202 self.sync(viewport_width);
203 let cols = self.stored_cols.get().max(1);
204
205 let mut row_max: HashMap<usize, f32> = HashMap::new();
207 for &(idx, h) in measured {
208 let r = idx / cols;
209 let e = row_max.entry(r).or_insert(0.0);
210 if h > *e {
211 *e = h;
212 }
213 }
214
215 let mut off = self.offsets.borrow_mut();
216 off.total();
219 let tops: Vec<(usize, f32, f32)> = row_max
220 .iter()
221 .map(|(&r, &h)| (r, off.row_top(r), h))
222 .collect();
223 let mut anchor_delta = 0.0_f32;
224 for (r, top_before, h) in tops {
225 let delta = off.set_row_height(r, h);
226 if delta.abs() > 0.01 && top_before < scroll_y {
231 anchor_delta += delta;
232 }
233 }
234 anchor_delta
235 }
236
237 fn invalidate_rows(&self, item_range: std::ops::Range<usize>) {
238 let cols = self.stored_cols.get().max(1);
239 let start_row = item_range.start / cols;
240 let end_row = if item_range.end == usize::MAX {
241 self.offsets.borrow().rows()
242 } else {
243 item_range.end.div_ceil(cols)
244 };
245 self.offsets.borrow_mut().invalidate(start_row, end_row);
246 }
247
248 fn resize(&self, item_count: usize) {
249 self.item_count.set(item_count);
250 let cols = self.stored_cols.get().max(1);
251 let rows = item_count.div_ceil(cols);
252 self.offsets.borrow_mut().resize(rows);
253 self.reseed_exact(cols);
254 }
255
256 fn index_at_point(
257 &self,
258 content_point: Point,
259 item_count: usize,
260 viewport_width: f32,
261 ) -> Option<usize> {
262 if item_count == 0 {
263 return None;
264 }
265 self.item_count.set(item_count);
266 self.sync(viewport_width);
267 let cols = self.stored_cols.get().max(1);
268 let (row, row_top, row_h) = {
269 let mut off = self.offsets.borrow_mut();
270 let row = off.row_at(content_point.y);
271 (row, off.row_top(row), off.row_height(row))
272 };
273 if content_point.y < row_top || content_point.y > row_top + row_h {
277 return None;
278 }
279 let col = column_at(&self.columns, content_point.x, viewport_width)?;
280 let idx = row * cols + col;
281 (idx < item_count).then_some(idx)
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 fn grid() -> VariableRowGrid {
290 VariableRowGrid::new(
293 GridSizing::Fixed {
294 width: 100.0,
295 height: 40.0,
296 },
297 10.0,
298 10.0,
299 EdgeInsets::ZERO,
300 40.0,
301 Some(Rc::new(|_i| 40.0)),
302 )
303 }
304
305 #[test]
306 fn index_at_point_closed_form_matches_measured_rows() {
307 let g = grid();
308 assert_eq!(g.index_at_point(Point::new(0.0, 0.0), 6, 210.0), Some(0));
311 assert_eq!(g.index_at_point(Point::new(0.0, 50.0), 6, 210.0), Some(2));
312 }
313
314 #[test]
315 fn index_at_point_closed_form_returns_none_in_gaps() {
316 let g = grid();
317 assert_eq!(g.index_at_point(Point::new(0.0, 45.0), 6, 210.0), None);
319 assert_eq!(g.index_at_point(Point::new(105.0, 10.0), 6, 210.0), None);
321 assert_eq!(g.index_at_point(Point::new(0.0, 9000.0), 6, 210.0), None);
323 }
324}