teksilo_widgets/grid_view/layout/
masonry.rs1use std::cell::RefCell;
18use std::rc::Rc;
19
20use teksilo_canvas::EdgeInsets;
21
22use super::columns::{ColumnGeometry, geometry_for};
23use super::strategy::{BUFFER_ROWS, GridLayoutStrategy, GridSizing, TileRect, VisibleTileRange};
24
25type ExactHeightFn = Rc<dyn Fn(usize) -> f32>;
26
27#[derive(Debug)]
29struct Placement {
30 heights: Vec<f32>,
31 measured: Vec<bool>,
32 col_of: Vec<usize>,
33 top_of: Vec<f32>,
34 total_height: f32,
35 cols: usize,
36 gap: f32,
37 inset_top: f32,
38 inset_bottom: f32,
39 estimated: f32,
40 dirty: bool,
41}
42
43impl Placement {
44 fn new(estimated: f32, gap: f32, inset_top: f32, inset_bottom: f32) -> Self {
45 Self {
46 heights: Vec::new(),
47 measured: Vec::new(),
48 col_of: Vec::new(),
49 top_of: Vec::new(),
50 total_height: 0.0,
51 cols: 1,
52 gap,
53 inset_top,
54 inset_bottom,
55 estimated,
56 dirty: true,
57 }
58 }
59
60 fn len(&self) -> usize {
61 self.heights.len()
62 }
63
64 fn set_count(&mut self, n: usize) {
65 if n != self.heights.len() {
66 self.heights.resize(n, self.estimated);
67 self.measured.resize(n, false);
68 self.dirty = true;
69 }
70 }
71
72 fn set_cols(&mut self, cols: usize) {
73 if cols != self.cols {
74 self.cols = cols.max(1);
75 self.dirty = true;
76 }
77 }
78
79 fn set_height(&mut self, i: usize, h: f32) {
80 if i < self.heights.len() && (self.heights[i] - h).abs() > 0.01 {
81 self.heights[i] = h;
82 self.measured[i] = true;
83 self.dirty = true;
84 } else if i < self.measured.len() {
85 self.measured[i] = true;
86 }
87 }
88
89 fn invalidate(&mut self, start: usize, end: usize) {
90 let end = end.min(self.heights.len());
91 for i in start..end {
92 self.heights[i] = self.estimated;
93 self.measured[i] = false;
94 }
95 if start < end {
96 self.dirty = true;
97 }
98 }
99
100 fn rebuild(&mut self) {
101 if !self.dirty {
102 return;
103 }
104 let n = self.heights.len();
105 self.col_of.resize(n, 0);
106 self.top_of.resize(n, 0.0);
107 let cols = self.cols.max(1);
108 let mut bottoms = vec![self.inset_top; cols];
109 for i in 0..n {
110 let c = bottoms
112 .iter()
113 .enumerate()
114 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
115 .map(|(i, _)| i)
116 .unwrap_or(0);
117 self.col_of[i] = c;
118 self.top_of[i] = bottoms[c];
119 bottoms[c] += self.heights[i] + self.gap;
120 }
121 let max_bottom = bottoms.iter().cloned().fold(self.inset_top, f32::max);
122 self.total_height = if n == 0 {
123 0.0
124 } else {
125 (max_bottom - self.gap).max(self.inset_top) + self.inset_bottom
126 };
127 self.dirty = false;
128 }
129}
130
131pub struct VirtualizedMasonry {
133 columns: ColumnGeometry,
134 exact_height: Option<ExactHeightFn>,
135 placement: RefCell<Placement>,
136 estimated: f32,
137}
138
139impl std::fmt::Debug for VirtualizedMasonry {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 f.debug_struct("VirtualizedMasonry")
142 .field("items", &self.placement.borrow().len())
143 .field("exact", &self.exact_height.is_some())
144 .finish()
145 }
146}
147
148impl VirtualizedMasonry {
149 pub(crate) fn new(
150 sizing: GridSizing,
151 col_gap: f32,
152 row_gap: f32,
153 inset: EdgeInsets,
154 estimated: f32,
155 exact_height: Option<ExactHeightFn>,
156 ) -> Self {
157 let estimated = if estimated > 0.0 {
158 estimated
159 } else {
160 sizing.tile_height().max(1.0)
161 };
162 Self {
163 columns: geometry_for(sizing, col_gap, inset),
164 exact_height,
165 placement: RefCell::new(Placement::new(
166 estimated,
167 row_gap.max(0.0),
168 inset.top,
169 inset.bottom,
170 )),
171 estimated,
172 }
173 }
174
175 fn reseed_exact(&self) {
176 let Some(ref ef) = self.exact_height else {
177 return;
178 };
179 let mut p = self.placement.borrow_mut();
180 let n = p.len();
181 for i in 0..n {
182 let h = ef(i);
183 p.set_height(i, h);
184 }
185 }
186
187 fn sync(&self, item_count: usize, viewport_width: f32) {
189 let cols = self.columns.column_count(viewport_width).max(1);
190 {
191 let mut p = self.placement.borrow_mut();
192 p.set_count(item_count);
193 p.set_cols(cols);
194 }
195 if self.exact_height.is_some() {
196 self.reseed_exact();
197 }
198 self.placement.borrow_mut().rebuild();
199 }
200}
201
202impl GridLayoutStrategy for VirtualizedMasonry {
203 fn column_count(&self, viewport_width: f32) -> usize {
211 self.columns.column_count(viewport_width)
212 }
213
214 fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32) {
215 self.columns.column_x(col, viewport_width)
216 }
217
218 fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32 {
219 self.sync(item_count, viewport_width);
220 self.placement.borrow().total_height
221 }
222
223 fn visible_range(
224 &self,
225 scroll_y: f32,
226 viewport_height: f32,
227 viewport_width: f32,
228 item_count: usize,
229 ) -> VisibleTileRange {
230 self.sync(item_count, viewport_width);
231 if item_count == 0 {
232 return VisibleTileRange { start: 0, end: 0 };
233 }
234 let p = self.placement.borrow();
235 let cols = p.cols.max(1);
236 let top = scroll_y;
237 let bot = scroll_y + viewport_height;
238 let mut min_i = None;
242 let mut max_i = None;
243 for i in 0..item_count {
244 let t = p.top_of[i];
245 let b = t + p.heights[i];
246 if b >= top && t <= bot {
247 min_i.get_or_insert(i);
248 max_i = Some(i);
249 }
250 }
251 match (min_i, max_i) {
252 (Some(lo), Some(hi)) => {
253 let buf = BUFFER_ROWS * cols;
254 let start = lo.saturating_sub(buf);
255 let end = (hi + 1 + buf).min(item_count);
256 VisibleTileRange { start, end }
257 }
258 _ => VisibleTileRange { start: 0, end: 0 },
259 }
260 }
261
262 fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect {
263 self.placement.borrow_mut().rebuild();
264 let p = self.placement.borrow();
265 let col = p.col_of.get(index).copied().unwrap_or(0);
266 let (x, width) = self.columns.column_x(col, viewport_width);
267 let y = p.top_of.get(index).copied().unwrap_or(0.0);
268 let height = p.heights.get(index).copied().unwrap_or(self.estimated);
269 TileRect {
270 x,
271 y,
272 width,
273 height,
274 }
275 }
276
277 fn estimated_row_height(&self) -> f32 {
278 self.estimated
279 }
280
281 fn measures_tiles(&self) -> bool {
282 self.exact_height.is_none()
283 }
284
285 fn observe_measured(
286 &self,
287 measured: &[(usize, f32)],
288 _scroll_y: f32,
289 viewport_width: f32,
290 ) -> f32 {
291 if self.exact_height.is_some() {
292 return 0.0;
293 }
294 let _ = viewport_width;
297 let mut p = self.placement.borrow_mut();
298 for &(i, h) in measured {
299 p.set_height(i, h);
300 }
301 0.0
302 }
303
304 fn invalidate_rows(&self, item_range: std::ops::Range<usize>) {
305 let end = if item_range.end == usize::MAX {
306 self.placement.borrow().len()
307 } else {
308 item_range.end
309 };
310 self.placement
311 .borrow_mut()
312 .invalidate(item_range.start, end);
313 }
314
315 fn resize(&self, item_count: usize) {
316 self.placement.borrow_mut().set_count(item_count);
317 if self.exact_height.is_some() {
318 self.reseed_exact();
319 }
320 }
321}