1use std::fmt;
2use std::hash::Hash;
3use std::sync::Arc;
4
5use fret_core::Px;
6
7const VIRTUALIZER_PX_SCALE: f32 = 64.0;
8
9fn px_to_units_u32(px: Px) -> u32 {
10 let scaled = (px.0.max(0.0) * VIRTUALIZER_PX_SCALE).round();
11 scaled.clamp(0.0, u32::MAX as f32) as u32
12}
13
14fn px_to_units_u64(px: Px) -> u64 {
15 let scaled = (px.0.max(0.0) * VIRTUALIZER_PX_SCALE).round();
16 scaled.clamp(0.0, u64::MAX as f32) as u64
17}
18
19fn units_u32_to_px(units: u32) -> Px {
20 Px(units as f32 / VIRTUALIZER_PX_SCALE)
21}
22
23fn units_u64_to_px(units: u64) -> Px {
24 Px(units as f32 / VIRTUALIZER_PX_SCALE)
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum GridAxisMeasureMode {
29 Fixed,
30 Measured,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct GridAxisRange {
35 pub start_index: usize,
36 pub end_index: usize,
37 pub overscan: usize,
38 pub count: usize,
39}
40
41pub fn default_range_extractor(range: GridAxisRange) -> Vec<usize> {
42 if range.count == 0 {
43 return Vec::new();
44 }
45 let start = range.start_index.saturating_sub(range.overscan);
46 let end = (range.end_index + range.overscan).min(range.count.saturating_sub(1));
47 (start..=end).collect()
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct GridAxisItem<K> {
52 pub key: K,
53 pub index: usize,
54 pub start: Px,
55 pub end: Px,
56 pub size: Px,
57}
58
59#[derive(Debug, Clone)]
60struct FixedAxisMetrics {
61 count: usize,
62 estimate_units: u32,
63 gap_units: u32,
64 padding_start_units: u32,
65}
66
67#[derive(Clone)]
82pub struct GridAxisMetrics<K> {
83 mode: GridAxisMeasureMode,
84 estimate: Px,
85 gap: Px,
86 padding_start: Px,
87 keys_signature: (u64, usize),
88 inner: virtualizer::Virtualizer<K>,
89 fixed: FixedAxisMetrics,
90 get_item_key: Arc<dyn Fn(usize) -> K + Send + Sync + 'static>,
91}
92
93impl<K> fmt::Debug for GridAxisMetrics<K> {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 f.debug_struct("GridAxisMetrics")
96 .field("mode", &self.mode)
97 .field("estimate", &self.estimate)
98 .field("gap", &self.gap)
99 .field("padding_start", &self.padding_start)
100 .field("signature", &self.keys_signature)
101 .field("fixed_count", &self.fixed.count)
102 .finish()
103 }
104}
105
106impl<K> Default for GridAxisMetrics<K>
107where
108 K: Hash + Eq + Clone + Send + Sync + 'static,
109{
110 fn default() -> Self {
111 let options: virtualizer::VirtualizerOptions<K> =
112 virtualizer::VirtualizerOptions::new_with_key(
113 0,
114 |_| 0,
115 |_| {
116 panic!(
117 "GridAxisMetrics default key resolver should not be called for empty axes"
118 )
119 },
120 );
121 Self {
122 mode: GridAxisMeasureMode::Measured,
123 estimate: Px(0.0),
124 gap: Px(0.0),
125 padding_start: Px(0.0),
126 keys_signature: (0, 0),
127 inner: virtualizer::Virtualizer::new(options),
128 fixed: FixedAxisMetrics {
129 count: 0,
130 estimate_units: 0,
131 gap_units: 0,
132 padding_start_units: 0,
133 },
134 get_item_key: Arc::new(|_| {
135 panic!("GridAxisMetrics get_item_key should not be called before ensure_*")
136 }),
137 }
138 }
139}
140
141impl<K> GridAxisMetrics<K>
142where
143 K: Hash + Eq + Clone + Send + Sync + 'static,
144{
145 pub fn ensure_with_mode(
146 &mut self,
147 mode: GridAxisMeasureMode,
148 keys: Arc<Vec<K>>,
149 items_revision: u64,
150 estimate: Px,
151 gap: Px,
152 padding_start: Px,
153 ) {
154 match mode {
155 GridAxisMeasureMode::Fixed => {
156 self.ensure_fixed(keys, items_revision, estimate, gap, padding_start);
157 }
158 GridAxisMeasureMode::Measured => {
159 self.ensure_measured(keys, items_revision, estimate, gap, padding_start);
160 }
161 }
162 }
163
164 pub fn ensure_measured(
165 &mut self,
166 keys: Arc<Vec<K>>,
167 items_revision: u64,
168 estimate: Px,
169 gap: Px,
170 padding_start: Px,
171 ) {
172 let count = keys.len();
173 let get_key = move |i: usize| {
174 keys.get(i)
175 .cloned()
176 .unwrap_or_else(|| keys.last().expect("non-empty keys").clone())
177 };
178 self.ensure_measured_with_key(count, items_revision, estimate, gap, padding_start, get_key);
179 }
180
181 pub fn ensure_measured_with_key(
182 &mut self,
183 count: usize,
184 items_revision: u64,
185 estimate: Px,
186 gap: Px,
187 padding_start: Px,
188 get_key: impl Fn(usize) -> K + Send + Sync + 'static,
189 ) {
190 let estimate = Px(estimate.0.max(0.0));
191 let gap = Px(gap.0.max(0.0));
192 let padding_start = Px(padding_start.0.max(0.0));
193
194 let signature = (items_revision, count);
195 if self.mode == GridAxisMeasureMode::Measured
196 && self.keys_signature == signature
197 && self.estimate == estimate
198 && self.gap == gap
199 && self.padding_start == padding_start
200 {
201 return;
202 }
203
204 self.mode = GridAxisMeasureMode::Measured;
205 self.estimate = estimate;
206 self.gap = gap;
207 self.padding_start = padding_start;
208 self.keys_signature = signature;
209
210 let estimate_units = px_to_units_u32(estimate);
211 let gap_units = px_to_units_u32(gap);
212 let padding_start_units = px_to_units_u32(padding_start);
213
214 let mut options = self.inner.options().clone();
215 options.count = count;
216 options.gap = gap_units;
217 options.padding_start = padding_start_units;
218 options.padding_end = 0;
219 options.scroll_margin = 0;
220 options.estimate_size = Arc::new(move |_| estimate_units);
221
222 let key_fn = Arc::new(get_key);
223 let key_fn_clamped: Arc<dyn Fn(usize) -> K + Send + Sync + 'static> =
224 Arc::new(move |i: usize| {
225 if count == 0 {
226 panic!("GridAxisMetrics measured key resolver called with count=0");
227 }
228 (key_fn)(i.min(count.saturating_sub(1)))
229 });
230 self.get_item_key = Arc::clone(&key_fn_clamped);
231 options.get_item_key = key_fn_clamped;
232 self.inner.set_options(options);
233 }
234
235 pub fn ensure_fixed(
236 &mut self,
237 keys: Arc<Vec<K>>,
238 items_revision: u64,
239 estimate: Px,
240 gap: Px,
241 padding_start: Px,
242 ) {
243 let count = keys.len();
244 let get_key = move |i: usize| {
245 keys.get(i)
246 .cloned()
247 .unwrap_or_else(|| keys.last().expect("non-empty keys").clone())
248 };
249 self.ensure_fixed_with_key(count, items_revision, estimate, gap, padding_start, get_key);
250 }
251
252 pub fn ensure_fixed_with_key(
253 &mut self,
254 count: usize,
255 items_revision: u64,
256 estimate: Px,
257 gap: Px,
258 padding_start: Px,
259 get_key: impl Fn(usize) -> K + Send + Sync + 'static,
260 ) {
261 let estimate = Px(estimate.0.max(0.0));
262 let gap = Px(gap.0.max(0.0));
263 let padding_start = Px(padding_start.0.max(0.0));
264
265 let signature = (items_revision, count);
266 if self.mode == GridAxisMeasureMode::Fixed
267 && self.keys_signature == signature
268 && self.estimate == estimate
269 && self.gap == gap
270 && self.padding_start == padding_start
271 {
272 return;
273 }
274
275 self.mode = GridAxisMeasureMode::Fixed;
276 self.estimate = estimate;
277 self.gap = gap;
278 self.padding_start = padding_start;
279 self.keys_signature = signature;
280
281 let key_fn = Arc::new(get_key);
282 let key_fn_clamped: Arc<dyn Fn(usize) -> K + Send + Sync + 'static> =
283 Arc::new(move |i: usize| {
284 if count == 0 {
285 panic!("GridAxisMetrics fixed key resolver called with count=0");
286 }
287 (key_fn)(i.min(count.saturating_sub(1)))
288 });
289 self.get_item_key = key_fn_clamped;
290
291 self.fixed = FixedAxisMetrics {
292 count,
293 estimate_units: px_to_units_u32(estimate),
294 gap_units: px_to_units_u32(gap),
295 padding_start_units: px_to_units_u32(padding_start),
296 };
297 }
298
299 pub fn total_size(&self) -> Px {
300 match self.mode {
301 GridAxisMeasureMode::Measured => units_u64_to_px(self.inner.total_size()),
302 GridAxisMeasureMode::Fixed => {
303 let count = self.fixed.count as u64;
304 if count == 0 {
305 return Px(0.0);
306 }
307 let estimate = self.fixed.estimate_units as u64;
308 let gap = self.fixed.gap_units as u64;
309 let padding_start = self.fixed.padding_start_units as u64;
310 let gaps = count.saturating_sub(1);
311 let total_units = padding_start
312 .saturating_add(count.saturating_mul(estimate))
313 .saturating_add(gaps.saturating_mul(gap));
314 units_u64_to_px(total_units)
315 }
316 }
317 }
318
319 pub fn clamp_scroll_offset(&self, offset: Px, viewport: Px) -> Px {
320 let viewport = Px(viewport.0.max(0.0));
321 let total_units = px_to_units_u64(self.total_size());
322 let max_offset_units = total_units.saturating_sub(px_to_units_u64(viewport));
323 let max_offset = units_u64_to_px(max_offset_units);
324 let offset = Px(offset.0.max(0.0));
325 Px(offset.0.min(max_offset.0))
326 }
327
328 pub fn axis_item(&self, index: usize) -> Option<GridAxisItem<K>> {
329 let count = match self.mode {
330 GridAxisMeasureMode::Measured => self.inner.options().count,
331 GridAxisMeasureMode::Fixed => self.fixed.count,
332 };
333 if index >= count {
334 return None;
335 }
336 let key = (self.get_item_key)(index);
337 let start = self.offset_for_index(index);
338 let size = self.size_at(index);
339 let end = Px((start.0 + size.0).max(0.0));
340 Some(GridAxisItem {
341 key,
342 index,
343 start,
344 end,
345 size,
346 })
347 }
348
349 pub fn size_at(&self, index: usize) -> Px {
350 match self.mode {
351 GridAxisMeasureMode::Measured => self
352 .inner
353 .item_size(index)
354 .map(units_u32_to_px)
355 .unwrap_or(Px(0.0)),
356 GridAxisMeasureMode::Fixed => {
357 if index >= self.fixed.count {
358 return Px(0.0);
359 }
360 units_u32_to_px(self.fixed.estimate_units)
361 }
362 }
363 }
364
365 pub fn offset_for_index(&self, index: usize) -> Px {
366 match self.mode {
367 GridAxisMeasureMode::Measured => {
368 if index >= self.inner.options().count {
369 return self.total_size();
370 }
371 self.inner
372 .item_start(index)
373 .map(units_u64_to_px)
374 .unwrap_or(Px(0.0))
375 }
376 GridAxisMeasureMode::Fixed => {
377 if index >= self.fixed.count {
378 return self.total_size();
379 }
380 let stride =
381 (self.fixed.estimate_units as u64).saturating_add(self.fixed.gap_units as u64);
382 let start_units = (self.fixed.padding_start_units as u64)
383 .saturating_add((index as u64).saturating_mul(stride));
384 units_u64_to_px(start_units)
385 }
386 }
387 }
388
389 pub fn index_for_offset(&self, offset: Px) -> usize {
390 match self.mode {
391 GridAxisMeasureMode::Measured => {
392 if self.inner.options().count == 0 {
393 return 0;
394 }
395 if offset.0 >= self.total_size().0 {
396 return self.inner.options().count;
397 }
398 self.inner
399 .index_at_offset(px_to_units_u64(offset))
400 .unwrap_or(0)
401 }
402 GridAxisMeasureMode::Fixed => {
403 let count = self.fixed.count;
404 if count == 0 {
405 return 0;
406 }
407 if offset.0 >= self.total_size().0 {
408 return count;
409 }
410
411 let offset_units = px_to_units_u64(offset);
412 let padding_start = self.fixed.padding_start_units as u64;
413 if offset_units <= padding_start {
414 return 0;
415 }
416 let stride =
417 (self.fixed.estimate_units as u64).saturating_add(self.fixed.gap_units as u64);
418 if stride == 0 {
419 return 0;
420 }
421
422 let adjusted = offset_units.saturating_sub(padding_start);
423 let idx = adjusted / stride;
424 (idx as usize).min(count.saturating_sub(1))
425 }
426 }
427 }
428
429 pub fn visible_range(
431 &self,
432 offset: Px,
433 viewport: Px,
434 overscan: usize,
435 ) -> Option<GridAxisRange> {
436 let viewport = Px(viewport.0.max(0.0));
437 let count = match self.mode {
438 GridAxisMeasureMode::Measured => self.inner.options().count,
439 GridAxisMeasureMode::Fixed => self.fixed.count,
440 };
441 if viewport.0 <= 0.0 || count == 0 {
442 return None;
443 }
444
445 let (start, end) = match self.mode {
446 GridAxisMeasureMode::Measured => {
447 let range = self
448 .inner
449 .visible_range_for(px_to_units_u64(offset), px_to_units_u32(viewport));
450 if range.is_empty() {
451 return None;
452 }
453 let start = range.start_index;
454 let end = range
455 .end_index
456 .saturating_sub(1)
457 .min(count.saturating_sub(1));
458 (start, end)
459 }
460 GridAxisMeasureMode::Fixed => {
461 let start = self.index_for_offset(offset);
462 if start >= count {
463 return None;
464 }
465
466 let end_exclusive = {
467 let offset = Px(offset.0 + viewport.0);
468 let total = self.total_size();
469 if offset.0 >= total.0 {
470 count
471 } else {
472 let offset_units = px_to_units_u64(offset);
473 let padding_start = self.fixed.padding_start_units as u64;
474 if offset_units <= padding_start {
475 1
476 } else {
477 let stride = (self.fixed.estimate_units as u64)
478 .saturating_add(self.fixed.gap_units as u64);
479 if stride == 0 {
480 1
481 } else {
482 let adjusted = offset_units.saturating_sub(padding_start);
483 let idx = adjusted / stride;
484 (idx as usize).saturating_add(1).min(count)
485 }
486 }
487 }
488 };
489
490 let end = end_exclusive.saturating_sub(1).min(count.saturating_sub(1));
491 (start, end)
492 }
493 };
494
495 Some(GridAxisRange {
496 start_index: start,
497 end_index: end,
498 overscan,
499 count,
500 })
501 }
502
503 pub fn measure(&mut self, index: usize, size: Px) {
504 if self.mode != GridAxisMeasureMode::Measured {
505 return;
506 }
507 let size_units = px_to_units_u32(size);
508 let Some(old_units) = self.inner.item_size(index) else {
509 return;
510 };
511 if old_units == size_units {
512 return;
513 }
514 self.inner.measure_unadjusted(index, size_units);
515 }
516
517 pub fn reset_measurements(&mut self) {
518 if self.mode != GridAxisMeasureMode::Measured {
519 return;
520 }
521 self.inner.reset_measurements();
522 }
523}
524
525#[derive(Debug, Clone, PartialEq)]
526pub struct GridViewport2D {
527 pub row_range: GridAxisRange,
528 pub col_range: GridAxisRange,
529 pub scroll_x: Px,
530 pub scroll_y: Px,
531 pub total_width: Px,
532 pub total_height: Px,
533}
534
535pub fn compute_grid_viewport_2d<KR, KC>(
536 rows: &GridAxisMetrics<KR>,
537 cols: &GridAxisMetrics<KC>,
538 scroll_x: Px,
539 scroll_y: Px,
540 viewport_w: Px,
541 viewport_h: Px,
542 overscan_rows: usize,
543 overscan_cols: usize,
544) -> Option<GridViewport2D>
545where
546 KR: Hash + Eq + Clone + Send + Sync + 'static,
547 KC: Hash + Eq + Clone + Send + Sync + 'static,
548{
549 let scroll_x = cols.clamp_scroll_offset(scroll_x, viewport_w);
550 let scroll_y = rows.clamp_scroll_offset(scroll_y, viewport_h);
551
552 let row_range = rows.visible_range(scroll_y, viewport_h, overscan_rows)?;
553 let col_range = cols.visible_range(scroll_x, viewport_w, overscan_cols)?;
554
555 Some(GridViewport2D {
556 row_range,
557 col_range,
558 scroll_x,
559 scroll_y,
560 total_width: cols.total_size(),
561 total_height: rows.total_size(),
562 })
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn fixed_axis_total_size_includes_padding_and_gaps() {
571 let mut axis: GridAxisMetrics<u64> = GridAxisMetrics::default();
572 axis.ensure_with_mode(
573 GridAxisMeasureMode::Fixed,
574 Arc::new(vec![0, 1, 2]),
575 1,
576 Px(10.0),
577 Px(2.0),
578 Px(5.0),
579 );
580
581 assert_eq!(axis.total_size(), Px(39.0));
583 assert_eq!(axis.offset_for_index(0), Px(5.0));
584 assert_eq!(axis.offset_for_index(1), Px(17.0));
585 assert_eq!(axis.offset_for_index(2), Px(29.0));
586 }
587
588 #[test]
589 fn measured_axis_preserves_sizes_across_reorder_by_key() {
590 let mut axis: GridAxisMetrics<u64> = GridAxisMetrics::default();
591 axis.ensure_measured(Arc::new(vec![10, 20, 30]), 1, Px(10.0), Px(0.0), Px(0.0));
592
593 axis.measure(1, Px(50.0));
595 assert_eq!(axis.size_at(1), Px(50.0));
596
597 axis.ensure_measured(Arc::new(vec![20, 10, 30]), 2, Px(10.0), Px(0.0), Px(0.0));
599 assert_eq!(axis.size_at(0), Px(50.0));
600 }
601
602 #[test]
603 fn grid_viewport_2d_returns_ranges() {
604 let mut rows: GridAxisMetrics<u64> = GridAxisMetrics::default();
605 rows.ensure_with_mode(
606 GridAxisMeasureMode::Fixed,
607 Arc::new((0..100).collect()),
608 1,
609 Px(10.0),
610 Px(0.0),
611 Px(0.0),
612 );
613
614 let mut cols: GridAxisMetrics<u64> = GridAxisMetrics::default();
615 cols.ensure_with_mode(
616 GridAxisMeasureMode::Fixed,
617 Arc::new((0..50).collect()),
618 1,
619 Px(20.0),
620 Px(0.0),
621 Px(0.0),
622 );
623
624 let vp =
625 compute_grid_viewport_2d(&rows, &cols, Px(30.0), Px(25.0), Px(100.0), Px(50.0), 2, 1)
626 .expect("viewport");
627
628 assert_eq!(vp.row_range.start_index, 2);
630 assert_eq!(vp.row_range.end_index, 7);
631
632 assert_eq!(vp.col_range.start_index, 1);
634 assert_eq!(vp.col_range.end_index, 6);
635 }
636}