teksilo_widgets/grid_view/layout/
masonry.rs1use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20use teksilo_canvas::EdgeInsets;
21
22use super::columns::{ColumnGeometry, ColumnOptions, geometry_for, with_options};
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: Cell<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 with_columns(self, sizing: GridSizing, options: ColumnOptions) -> Self {
152 self.columns
153 .set(with_options(self.columns.get(), sizing, options));
154 self
155 }
156
157 pub(crate) fn new(
158 sizing: GridSizing,
159 col_gap: f32,
160 row_gap: f32,
161 inset: EdgeInsets,
162 estimated: f32,
163 exact_height: Option<ExactHeightFn>,
164 ) -> Self {
165 let estimated = if estimated > 0.0 {
166 estimated
167 } else {
168 sizing.tile_height().max(1.0)
169 };
170 Self {
171 columns: Cell::new(geometry_for(sizing, col_gap, inset)),
172 exact_height,
173 placement: RefCell::new(Placement::new(
174 estimated,
175 row_gap.max(0.0),
176 inset.top,
177 inset.bottom,
178 )),
179 estimated,
180 }
181 }
182
183 fn reseed_exact(&self) {
184 let Some(ref ef) = self.exact_height else {
185 return;
186 };
187 let mut p = self.placement.borrow_mut();
188 let n = p.len();
189 for i in 0..n {
190 let h = ef(i);
191 p.set_height(i, h);
192 }
193 }
194
195 fn sync(&self, item_count: usize, viewport_width: f32) {
197 let cols = self.columns.get().column_count(viewport_width).max(1);
198 {
199 let mut p = self.placement.borrow_mut();
200 p.set_count(item_count);
201 p.set_cols(cols);
202 }
203 if self.exact_height.is_some() {
204 self.reseed_exact();
205 }
206 self.placement.borrow_mut().rebuild();
207 }
208}
209
210impl GridLayoutStrategy for VirtualizedMasonry {
211 fn set_rtl(&self, rtl: bool) {
212 self.columns.set(self.columns.get().with_rtl(rtl));
213 }
214
215 fn column_count(&self, viewport_width: f32) -> usize {
223 self.columns.get().column_count(viewport_width)
224 }
225
226 fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32) {
227 self.columns.get().column_x(col, viewport_width)
228 }
229
230 fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32 {
231 self.sync(item_count, viewport_width);
232 self.placement.borrow().total_height
233 }
234
235 fn visible_range(
236 &self,
237 scroll_y: f32,
238 viewport_height: f32,
239 viewport_width: f32,
240 item_count: usize,
241 ) -> VisibleTileRange {
242 self.sync(item_count, viewport_width);
243 if item_count == 0 {
244 return VisibleTileRange { start: 0, end: 0 };
245 }
246 let p = self.placement.borrow();
247 let cols = p.cols.max(1);
248 let top = scroll_y;
249 let bot = scroll_y + viewport_height;
250 let mut min_i = None;
254 let mut max_i = None;
255 for i in 0..item_count {
256 let t = p.top_of[i];
257 let b = t + p.heights[i];
258 if b >= top && t <= bot {
259 min_i.get_or_insert(i);
260 max_i = Some(i);
261 }
262 }
263 match (min_i, max_i) {
264 (Some(lo), Some(hi)) => {
265 let buf = BUFFER_ROWS * cols;
266 let start = lo.saturating_sub(buf);
267 let end = (hi + 1 + buf).min(item_count);
268 VisibleTileRange { start, end }
269 }
270 _ => VisibleTileRange { start: 0, end: 0 },
271 }
272 }
273
274 fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect {
275 self.placement.borrow_mut().rebuild();
276 let p = self.placement.borrow();
277 let col = p.col_of.get(index).copied().unwrap_or(0);
278 let (x, width) = self.columns.get().column_x(col, viewport_width);
279 let y = p.top_of.get(index).copied().unwrap_or(0.0);
280 let height = p.heights.get(index).copied().unwrap_or(self.estimated);
281 TileRect {
282 x,
283 y,
284 width,
285 height,
286 }
287 }
288
289 fn estimated_row_height(&self) -> f32 {
290 self.estimated
291 }
292
293 fn measures_tiles(&self) -> bool {
294 self.exact_height.is_none()
295 }
296
297 fn observe_measured(
298 &self,
299 measured: &[(usize, f32)],
300 _scroll_y: f32,
301 viewport_width: f32,
302 ) -> f32 {
303 if self.exact_height.is_some() {
304 return 0.0;
305 }
306 let _ = viewport_width;
309 let mut p = self.placement.borrow_mut();
310 for &(i, h) in measured {
311 p.set_height(i, h);
312 }
313 0.0
314 }
315
316 fn invalidate_rows(&self, item_range: std::ops::Range<usize>) {
317 let end = if item_range.end == usize::MAX {
318 self.placement.borrow().len()
319 } else {
320 item_range.end
321 };
322 self.placement
323 .borrow_mut()
324 .invalidate(item_range.start, end);
325 }
326
327 fn resize(&self, item_count: usize) {
328 self.placement.borrow_mut().set_count(item_count);
329 if self.exact_height.is_some() {
330 self.reseed_exact();
331 }
332 }
333}