1use fret_core::Px;
2use std::sync::Arc;
3
4use crate::element::VirtualListMeasureMode;
5use crate::scroll::ScrollStrategy;
6
7#[cfg(test)]
8use std::cell::Cell;
9
10const VIRTUALIZER_PX_SCALE: f32 = 64.0;
11
12fn px_to_units_u32(px: Px) -> u32 {
13 let scaled = (px.0.max(0.0) * VIRTUALIZER_PX_SCALE).round();
14 scaled.clamp(0.0, u32::MAX as f32) as u32
15}
16
17fn px_to_units_u64(px: Px) -> u64 {
18 let scaled = (px.0.max(0.0) * VIRTUALIZER_PX_SCALE).round();
19 scaled.clamp(0.0, u64::MAX as f32) as u64
20}
21
22fn units_u32_to_px(units: u32) -> Px {
23 Px(units as f32 / VIRTUALIZER_PX_SCALE)
24}
25
26fn units_u64_to_px(units: u64) -> Px {
27 Px(units as f32 / VIRTUALIZER_PX_SCALE)
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct VirtualItem {
32 pub key: crate::ItemKey,
33 pub index: usize,
34 pub start: Px,
35 pub end: Px,
36 pub size: Px,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct VirtualRange {
41 pub start_index: usize,
42 pub end_index: usize,
43 pub overscan: usize,
44 pub count: usize,
45}
46
47pub fn default_range_extractor(range: VirtualRange) -> Vec<usize> {
48 if range.count == 0 {
49 return Vec::new();
50 }
51 let start = range.start_index.saturating_sub(range.overscan);
52 let end = (range.end_index + range.overscan).min(range.count.saturating_sub(1));
53
54 (start..=end).collect()
55}
56
57pub(crate) fn shift_virtual_range_minimally(
58 rendered: VirtualRange,
59 visible: VirtualRange,
60) -> VirtualRange {
61 let overscan = rendered.overscan;
62 let count = rendered.count;
63 if count == 0 {
64 return rendered;
65 }
66
67 let inner_len = rendered.end_index.saturating_sub(rendered.start_index);
68 let rendered_outer_start = rendered.start_index.saturating_sub(overscan);
69 let rendered_outer_end = (rendered.end_index + overscan).min(count.saturating_sub(1));
70
71 let mut start = rendered.start_index;
72 let mut end = rendered.end_index;
73
74 if visible.start_index < rendered_outer_start {
75 start = visible.start_index.saturating_add(overscan);
76 end = start.saturating_add(inner_len);
77 } else if visible.end_index > rendered_outer_end {
78 end = visible.end_index.saturating_sub(overscan);
79 start = end.saturating_sub(inner_len);
80 }
81
82 if end >= count {
83 end = count.saturating_sub(1);
84 start = end.saturating_sub(inner_len);
85 }
86 if start >= count {
87 start = count.saturating_sub(1);
88 }
89 if start > end {
90 end = start;
91 }
92
93 VirtualRange {
94 start_index: start,
95 end_index: end,
96 overscan,
97 count,
98 }
99}
100
101pub(crate) fn prefetch_virtual_range_step(
102 rendered: VirtualRange,
103 visible: VirtualRange,
104 prefetch_margin: usize,
105 prefetch_step: usize,
106 prefer_forward: Option<bool>,
107) -> Option<VirtualRange> {
108 let overscan = rendered.overscan;
109 let count = rendered.count;
110 if count == 0 || overscan == 0 || prefetch_step == 0 {
111 return None;
112 }
113
114 let inner_len = rendered.end_index.saturating_sub(rendered.start_index);
115 let rendered_outer_start = rendered.start_index.saturating_sub(overscan);
116 let rendered_outer_end = (rendered.end_index + overscan).min(count.saturating_sub(1));
117
118 if visible.start_index < rendered_outer_start || visible.end_index > rendered_outer_end {
121 return None;
122 }
123
124 let near_start = visible.start_index <= rendered_outer_start.saturating_add(prefetch_margin);
125 let near_end = visible.end_index >= rendered_outer_end.saturating_sub(prefetch_margin);
126 if !near_start && !near_end {
127 return None;
128 }
129
130 let want_forward = if near_end && !near_start {
131 true
132 } else if near_start && !near_end {
133 false
134 } else {
135 prefer_forward?
139 };
140
141 let mut start = rendered.start_index;
142
143 let delta = if want_forward {
144 let max_delta = visible
146 .start_index
147 .saturating_add(overscan)
148 .saturating_sub(rendered.start_index);
149 prefetch_step.min(max_delta)
150 } else {
151 let max_delta = rendered
153 .end_index
154 .saturating_add(overscan)
155 .saturating_sub(visible.end_index);
156 prefetch_step.min(max_delta)
157 };
158
159 if delta == 0 {
160 return None;
161 }
162
163 if want_forward {
164 start = start.saturating_add(delta);
165 } else {
166 start = start.saturating_sub(delta);
167 }
168
169 let mut end = start.saturating_add(inner_len);
171
172 if end >= count {
173 end = count.saturating_sub(1);
174 start = end.saturating_sub(inner_len);
175 }
176 if start >= count {
177 start = count.saturating_sub(1);
178 }
179 if start > end {
180 end = start;
181 }
182
183 let next = VirtualRange {
184 start_index: start,
185 end_index: end,
186 overscan,
187 count,
188 };
189 (next != rendered).then_some(next)
190}
191
192pub(crate) fn visible_item_index_span(items: &[VirtualItem]) -> Option<(usize, usize)> {
193 let first = items.first()?.index;
194 let mut prev = first;
195 for item in items.iter().skip(1) {
196 if item.index <= prev {
197 return None;
198 }
199 prev = item.index;
200 }
201 Some((first, prev))
202}
203
204pub(crate) fn expanded_range_index_span(range: VirtualRange) -> Option<(usize, usize)> {
205 if range.count == 0 {
206 return None;
207 }
208 let start = range.start_index.saturating_sub(range.overscan);
209 let end = (range.end_index + range.overscan).min(range.count.saturating_sub(1));
210 Some((start, end))
211}
212
213pub(crate) fn virtual_list_needs_visible_range_refresh(
214 mounted_items: &[VirtualItem],
215 desired_range: VirtualRange,
216) -> bool {
217 let Some((desired_start, desired_end)) = expanded_range_index_span(desired_range) else {
218 return false;
219 };
220 if mounted_items.is_empty() {
221 return true;
222 }
223 let Some((mounted_start, mounted_end)) = visible_item_index_span(mounted_items) else {
224 return true;
225 };
226 desired_start < mounted_start || desired_end > mounted_end
227}
228
229#[derive(Debug, Clone)]
230pub struct VirtualListMetrics {
231 estimate: Px,
232 gap: Px,
233 scroll_margin: Px,
234 mode: VirtualListMeasureMode,
235 inner: virtualizer::Virtualizer<crate::ItemKey>,
236 keys_signature: (u64, usize),
237 measured_cross_extent_units: u32,
238 fixed: FixedMetrics,
239}
240
241#[derive(Debug, Clone, Copy)]
242struct FixedMetrics {
243 count: usize,
244 estimate_units: u32,
245 gap_units: u32,
246 padding_start_units: u32,
247}
248
249impl Default for VirtualListMetrics {
250 fn default() -> Self {
251 let options = virtualizer::VirtualizerOptions::new(0, |_| 0);
252 Self {
253 estimate: Px(0.0),
254 gap: Px(0.0),
255 scroll_margin: Px(0.0),
256 mode: VirtualListMeasureMode::Measured,
257 inner: virtualizer::Virtualizer::new(options),
258 keys_signature: (0, 0),
259 measured_cross_extent_units: 0,
260 fixed: FixedMetrics {
261 count: 0,
262 estimate_units: 0,
263 gap_units: 0,
264 padding_start_units: 0,
265 },
266 }
267 }
268}
269
270impl VirtualListMetrics {
271 pub fn ensure_with_mode(
272 &mut self,
273 mode: VirtualListMeasureMode,
274 len: usize,
275 estimate: Px,
276 gap: Px,
277 scroll_margin: Px,
278 ) {
279 match mode {
280 VirtualListMeasureMode::Measured => {
281 self.ensure_measured(len, estimate, gap, scroll_margin)
282 }
283 VirtualListMeasureMode::Fixed => self.ensure_fixed(len, estimate, gap, scroll_margin),
284 VirtualListMeasureMode::Known => self.ensure_known(len, estimate, gap, scroll_margin),
285 }
286 }
287
288 pub fn ensure(&mut self, len: usize, estimate: Px, gap: Px, scroll_margin: Px) {
289 self.ensure_measured(len, estimate, gap, scroll_margin);
290 }
291
292 fn ensure_fixed(&mut self, len: usize, estimate: Px, gap: Px, scroll_margin: Px) {
293 let estimate = Px(estimate.0.max(0.0));
294 let gap = Px(gap.0.max(0.0));
295 let scroll_margin = Px(scroll_margin.0.max(0.0));
296
297 if self.mode == VirtualListMeasureMode::Fixed
298 && self.fixed.count == len
299 && self.estimate == estimate
300 && self.gap == gap
301 && self.scroll_margin == scroll_margin
302 {
303 return;
304 }
305
306 self.mode = VirtualListMeasureMode::Fixed;
307 self.estimate = estimate;
308 self.gap = gap;
309 self.scroll_margin = scroll_margin;
310
311 self.fixed = FixedMetrics {
312 count: len,
313 estimate_units: px_to_units_u32(estimate),
314 gap_units: px_to_units_u32(gap),
315 padding_start_units: px_to_units_u32(scroll_margin),
316 };
317 }
318
319 fn ensure_known(&mut self, len: usize, estimate: Px, gap: Px, scroll_margin: Px) {
320 let estimate = Px(estimate.0.max(0.0));
321 let gap = Px(gap.0.max(0.0));
322 let scroll_margin = Px(scroll_margin.0.max(0.0));
323 if self.mode == VirtualListMeasureMode::Known
324 && self.inner.options().count == len
325 && self.estimate == estimate
326 && self.gap == gap
327 && self.scroll_margin == scroll_margin
328 {
329 return;
330 }
331
332 self.mode = VirtualListMeasureMode::Known;
333 self.estimate = estimate;
334 self.gap = gap;
335 self.scroll_margin = scroll_margin;
336
337 let estimate_units = px_to_units_u32(estimate);
338 let gap_units = px_to_units_u32(gap);
339 let padding_start = px_to_units_u32(scroll_margin);
340
341 let mut options = self.inner.options().clone();
342 options.count = len;
343 options.gap = gap_units;
344 options.padding_start = padding_start;
345 options.padding_end = 0;
346 options.scroll_margin = 0;
347 options.estimate_size = Arc::new(move |_| estimate_units);
348 self.inner.set_options(options);
349 }
350
351 fn ensure_measured(&mut self, len: usize, estimate: Px, gap: Px, scroll_margin: Px) {
352 let estimate = Px(estimate.0.max(0.0));
353 let gap = Px(gap.0.max(0.0));
354 let scroll_margin = Px(scroll_margin.0.max(0.0));
355 if self.mode == VirtualListMeasureMode::Measured
356 && self.inner.options().count == len
357 && self.estimate == estimate
358 && self.gap == gap
359 && self.scroll_margin == scroll_margin
360 {
361 return;
362 }
363
364 self.mode = VirtualListMeasureMode::Measured;
365 self.estimate = estimate;
366 self.gap = gap;
367 self.scroll_margin = scroll_margin;
368
369 let estimate_units = px_to_units_u32(estimate);
370 let gap_units = px_to_units_u32(gap);
371 let padding_start = px_to_units_u32(scroll_margin);
372
373 let mut options = self.inner.options().clone();
374 options.count = len;
375 options.gap = gap_units;
376 options.padding_start = padding_start;
377 options.padding_end = 0;
378 options.scroll_margin = 0;
379 options.estimate_size = Arc::new(move |_| estimate_units);
380 self.inner.set_options(options);
381 }
382
383 pub fn sync_keys(&mut self, keys: &[crate::ItemKey], items_revision: u64) {
384 let signature = (items_revision, keys.len());
385 if self.keys_signature == signature {
386 return;
387 }
388
389 if self.mode == VirtualListMeasureMode::Fixed {
390 self.keys_signature = signature;
391 return;
392 }
393
394 let keys = Arc::new(keys.to_vec());
395 let mut options = self.inner.options().clone();
396 options.get_item_key = Arc::new({
397 let keys = Arc::clone(&keys);
398 move |i| keys.get(i).copied().unwrap_or(i as crate::ItemKey)
399 });
400 self.inner.set_options(options);
401 self.keys_signature = signature;
402 }
403
404 pub fn total_height(&self) -> Px {
405 match self.mode {
406 VirtualListMeasureMode::Measured => units_u64_to_px(self.inner.total_size()),
407 VirtualListMeasureMode::Known => units_u64_to_px(self.inner.total_size()),
408 VirtualListMeasureMode::Fixed => {
409 let count = self.fixed.count as u64;
410 if count == 0 {
411 return Px(0.0);
412 }
413
414 let estimate = self.fixed.estimate_units as u64;
415 let gap = self.fixed.gap_units as u64;
416 let padding_start = self.fixed.padding_start_units as u64;
417
418 let gaps = count.saturating_sub(1);
419 let total_units = padding_start
420 .saturating_add(count.saturating_mul(estimate))
421 .saturating_add(gaps.saturating_mul(gap));
422 units_u64_to_px(total_units)
423 }
424 }
425 }
426
427 pub fn virtual_item(&self, index: usize, key: crate::ItemKey) -> VirtualItem {
428 let start = self.offset_for_index(index);
429 let size = self.height_at(index);
430 let end = Px((start.0 + size.0).max(0.0));
431 VirtualItem {
432 key,
433 index,
434 start,
435 end,
436 size,
437 }
438 }
439
440 pub fn estimate(&self) -> Px {
441 self.estimate
442 }
443
444 pub fn gap(&self) -> Px {
445 self.gap
446 }
447
448 pub fn scroll_margin(&self) -> Px {
449 self.scroll_margin
450 }
451
452 pub fn is_measured(&self, index: usize) -> bool {
453 match self.mode {
454 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => {
455 if index >= self.inner.options().count {
456 return false;
457 }
458 self.inner.is_measured(index)
459 }
460 VirtualListMeasureMode::Fixed => index < self.fixed.count,
461 }
462 }
463
464 pub fn reset_measured_cache_if_cross_extent_changed(&mut self, cross_extent: Px) -> bool {
465 if self.mode != VirtualListMeasureMode::Measured {
466 return false;
467 }
468
469 let units = px_to_units_u32(Px(cross_extent.0.max(0.0)));
470 if self.measured_cross_extent_units == units {
471 return false;
472 }
473
474 self.measured_cross_extent_units = units;
475 let options = self.inner.options().clone();
476 self.inner = virtualizer::Virtualizer::new(options);
477 true
478 }
479
480 pub fn height_at(&self, index: usize) -> Px {
481 match self.mode {
482 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => self
483 .inner
484 .item_size(index)
485 .map(units_u32_to_px)
486 .unwrap_or(Px(0.0)),
487 VirtualListMeasureMode::Fixed => {
488 if index >= self.fixed.count {
489 return Px(0.0);
490 }
491 units_u32_to_px(self.fixed.estimate_units)
492 }
493 }
494 }
495
496 pub fn offset_for_index(&self, index: usize) -> Px {
497 match self.mode {
498 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => {
499 if index >= self.inner.options().count {
500 return self.total_height();
501 }
502 self.inner
503 .item_start(index)
504 .map(units_u64_to_px)
505 .unwrap_or(Px(0.0))
506 }
507 VirtualListMeasureMode::Fixed => {
508 if index >= self.fixed.count {
509 return self.total_height();
510 }
511
512 let stride =
513 (self.fixed.estimate_units as u64).saturating_add(self.fixed.gap_units as u64);
514 let start_units = (self.fixed.padding_start_units as u64)
515 .saturating_add((index as u64).saturating_mul(stride));
516 units_u64_to_px(start_units)
517 }
518 }
519 }
520
521 pub fn end_for_index(&self, index: usize) -> Px {
522 match self.mode {
523 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => {
524 if index >= self.inner.options().count {
525 return self.total_height();
526 }
527 self.inner
528 .item_end(index)
529 .map(units_u64_to_px)
530 .unwrap_or(Px(0.0))
531 }
532 VirtualListMeasureMode::Fixed => {
533 if index >= self.fixed.count {
534 return self.total_height();
535 }
536 let start = px_to_units_u64(self.offset_for_index(index));
537 let end = start.saturating_add(self.fixed.estimate_units as u64);
538 units_u64_to_px(end)
539 }
540 }
541 }
542
543 pub fn index_for_offset(&self, offset: Px) -> usize {
544 match self.mode {
545 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => {
546 if self.inner.options().count == 0 {
547 return 0;
548 }
549 if offset.0 >= self.total_height().0 {
550 return self.inner.options().count;
551 }
552 self.inner
553 .index_at_offset(px_to_units_u64(offset))
554 .unwrap_or(0)
555 }
556 VirtualListMeasureMode::Fixed => {
557 let count = self.fixed.count;
558 if count == 0 {
559 return 0;
560 }
561 if offset.0 >= self.total_height().0 {
562 return count;
563 }
564
565 let offset_units = px_to_units_u64(offset);
566 let padding_start = self.fixed.padding_start_units as u64;
567 if offset_units <= padding_start {
568 return 0;
569 }
570 let stride =
571 (self.fixed.estimate_units as u64).saturating_add(self.fixed.gap_units as u64);
572 if stride == 0 {
573 return 0;
574 }
575
576 let adjusted = offset_units.saturating_sub(padding_start);
577 let idx = adjusted / stride;
578 (idx as usize).min(count)
579 }
580 }
581 }
582
583 pub fn end_index_for_offset(&self, offset: Px) -> usize {
584 match self.mode {
585 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => {
586 if self.inner.options().count == 0 {
587 return 0;
588 }
589 let idx = self.index_for_offset(offset);
590 if idx >= self.inner.options().count {
591 return self.inner.options().count;
592 }
593 let start = self.offset_for_index(idx).0;
594 if start < offset.0 {
595 idx.saturating_add(1).min(self.inner.options().count)
596 } else {
597 idx
598 }
599 }
600 VirtualListMeasureMode::Fixed => {
601 let count = self.fixed.count;
602 if count == 0 {
603 return 0;
604 }
605 let idx = self.index_for_offset(offset);
606 if idx >= count {
607 return count;
608 }
609 let start = self.offset_for_index(idx).0;
610 if start < offset.0 {
611 idx.saturating_add(1).min(count)
612 } else {
613 idx
614 }
615 }
616 }
617 }
618
619 pub fn set_measured_height(&mut self, index: usize, height: Px) -> bool {
620 if self.mode == VirtualListMeasureMode::Fixed {
621 return false;
622 }
623
624 let Some(old_units) = self.inner.item_size(index) else {
625 return false;
626 };
627
628 let height = Px(height.0.max(0.0));
629 let height_units = px_to_units_u32(height);
630 let changed = old_units != height_units;
631 if !changed && self.inner.is_measured(index) {
632 return false;
633 }
634
635 self.inner.measure_unadjusted(index, height_units);
636 true
637 }
638
639 pub fn clamp_offset(&self, mut offset_y: Px, viewport_h: Px) -> Px {
640 let viewport_h = Px(viewport_h.0.max(0.0));
641 let total = px_to_units_u64(self.total_height());
642 let max_offset_units = total.saturating_sub(px_to_units_u64(viewport_h));
643 let max_offset = units_u64_to_px(max_offset_units);
644 offset_y = Px(offset_y.0.max(0.0));
645 Px(offset_y.0.min(max_offset.0))
646 }
647
648 pub fn visible_range(
654 &self,
655 offset_y: Px,
656 viewport_h: Px,
657 overscan: usize,
658 ) -> Option<VirtualRange> {
659 let viewport_h = Px(viewport_h.0.max(0.0));
660 let count = match self.mode {
661 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => {
662 self.inner.options().count
663 }
664 VirtualListMeasureMode::Fixed => self.fixed.count,
665 };
666 if viewport_h.0 <= 0.0 || count == 0 {
667 return None;
668 }
669
670 let start = self.index_for_offset(offset_y);
671 if start >= count {
672 return None;
673 }
674 let end_exclusive = self.end_index_for_offset(Px(offset_y.0 + viewport_h.0));
675 let end = end_exclusive.saturating_sub(1).min(count.saturating_sub(1));
676
677 Some(VirtualRange {
678 start_index: start,
679 end_index: end,
680 overscan,
681 count,
682 })
683 }
684
685 pub fn scroll_offset_for_item(
686 &self,
687 index: usize,
688 viewport_h: Px,
689 current_offset_y: Px,
690 strategy: ScrollStrategy,
691 ) -> Px {
692 let viewport_h = Px(viewport_h.0.max(0.0));
693 if viewport_h.0 <= 0.0 {
694 return current_offset_y;
695 }
696
697 let count = match self.mode {
698 VirtualListMeasureMode::Measured | VirtualListMeasureMode::Known => {
699 self.inner.options().count
700 }
701 VirtualListMeasureMode::Fixed => self.fixed.count,
702 };
703 if count == 0 {
704 return current_offset_y;
705 }
706 let index = index.min(count.saturating_sub(1));
707
708 let item_top = self.offset_for_index(index);
709 let item_bottom = self.end_for_index(index);
710
711 let view_top = current_offset_y;
712 let view_bottom = Px(current_offset_y.0 + viewport_h.0);
713
714 match strategy {
715 ScrollStrategy::Start => item_top,
716 ScrollStrategy::End => Px(item_bottom.0 - viewport_h.0),
717 ScrollStrategy::Center => {
718 let item_center = 0.5 * (item_top.0 + item_bottom.0);
719 Px(item_center - 0.5 * viewport_h.0)
720 }
721 ScrollStrategy::Nearest => {
722 if item_top.0 < view_top.0 {
723 item_top
724 } else if item_bottom.0 > view_bottom.0 {
725 Px(item_bottom.0 - viewport_h.0)
726 } else {
727 current_offset_y
728 }
729 }
730 }
731 }
732
733 pub fn rebuild_from_heights(
734 &mut self,
735 heights: Vec<Px>,
736 measured: Vec<bool>,
737 estimate: Px,
738 gap: Px,
739 scroll_margin: Px,
740 ) {
741 let len = heights.len();
742 self.ensure_measured(len, estimate, gap, scroll_margin);
743
744 let mut entries = Vec::new();
745 for (index, height) in heights.into_iter().enumerate() {
746 let is_measured = measured.get(index).copied().unwrap_or(false);
747 if !is_measured {
748 continue;
749 }
750 entries.push((self.inner.key_for(index), px_to_units_u32(height)));
751 }
752 self.inner.import_measurement_cache(entries);
753 }
754
755 pub fn rebuild_from_known_heights(
756 &mut self,
757 heights: Vec<Px>,
758 estimate: Px,
759 gap: Px,
760 scroll_margin: Px,
761 ) {
762 let len = heights.len();
763 self.ensure_known(len, estimate, gap, scroll_margin);
764
765 let mut entries = Vec::with_capacity(len);
766 for (index, height) in heights.into_iter().enumerate() {
767 entries.push((self.inner.key_for(index), px_to_units_u32(height)));
768 }
769 self.inner.import_measurement_cache(entries);
770 }
771}
772
773#[cfg(test)]
774thread_local! {
775 static VIRTUAL_LIST_ITEM_MEASURE_CALLS: Cell<usize> = const { Cell::new(0) };
776}
777
778#[cfg(test)]
779pub(crate) fn debug_record_virtual_list_item_measure() {
780 VIRTUAL_LIST_ITEM_MEASURE_CALLS.with(|calls| {
781 calls.set(calls.get().saturating_add(1));
782 });
783}
784
785#[cfg(test)]
786pub(crate) fn debug_take_virtual_list_item_measures() -> usize {
787 VIRTUAL_LIST_ITEM_MEASURE_CALLS.with(|calls| {
788 let value = calls.get();
789 calls.set(0);
790 value
791 })
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 fn dummy_items(indices: &[usize]) -> Vec<VirtualItem> {
799 indices
800 .iter()
801 .copied()
802 .map(|index| VirtualItem {
803 key: index as crate::ItemKey,
804 index,
805 start: Px(0.0),
806 end: Px(0.0),
807 size: Px(0.0),
808 })
809 .collect()
810 }
811
812 #[test]
813 fn virtual_list_needs_visible_range_refresh_when_span_exceeded() {
814 let mounted = dummy_items(&[0, 1, 2, 3, 4]);
815 let desired = VirtualRange {
816 start_index: 1,
817 end_index: 3,
818 overscan: 0,
819 count: 100,
820 };
821 assert!(!virtual_list_needs_visible_range_refresh(&mounted, desired));
822
823 let desired = VirtualRange {
824 start_index: 5,
825 end_index: 6,
826 overscan: 0,
827 count: 100,
828 };
829 assert!(virtual_list_needs_visible_range_refresh(&mounted, desired));
830
831 let desired = VirtualRange {
832 start_index: 4,
833 end_index: 4,
834 overscan: 2,
835 count: 100,
836 };
837 assert!(virtual_list_needs_visible_range_refresh(&mounted, desired));
839 }
840
841 #[test]
842 fn visible_item_index_span_requires_strictly_increasing_indices() {
843 assert_eq!(
844 visible_item_index_span(&dummy_items(&[0, 1, 2])),
845 Some((0, 2))
846 );
847 assert_eq!(visible_item_index_span(&dummy_items(&[2])), Some((2, 2)));
848 assert_eq!(visible_item_index_span(&dummy_items(&[1, 1])), None);
849 assert_eq!(visible_item_index_span(&dummy_items(&[2, 1])), None);
850 }
851
852 #[test]
853 fn fenwick_sums_match_uniform_heights() {
854 let mut metrics = VirtualListMetrics::default();
855 metrics.ensure(100, Px(10.0), Px(0.0), Px(0.0));
856
857 assert!((metrics.total_height().0 - 1000.0).abs() < 0.01);
858 assert!((metrics.offset_for_index(0).0 - 0.0).abs() < 0.01);
859 assert!((metrics.offset_for_index(6).0 - 60.0).abs() < 0.01);
860 assert!((metrics.offset_for_index(100).0 - 1000.0).abs() < 0.01);
861
862 assert_eq!(metrics.index_for_offset(Px(0.0)), 0);
863 assert_eq!(metrics.index_for_offset(Px(0.1)), 0);
864 assert_eq!(metrics.index_for_offset(Px(9.9)), 0);
865 assert_eq!(metrics.index_for_offset(Px(10.0)), 1);
866 assert_eq!(metrics.index_for_offset(Px(59.9)), 5);
867 assert_eq!(metrics.index_for_offset(Px(60.0)), 6);
868 assert_eq!(metrics.end_index_for_offset(Px(50.0)), 5);
869 assert_eq!(metrics.end_index_for_offset(Px(50.1)), 6);
870 }
871
872 #[test]
873 fn visible_range_is_inclusive_and_clamped() {
874 let mut metrics = VirtualListMetrics::default();
875 metrics.ensure(10, Px(10.0), Px(0.0), Px(0.0));
876
877 let r0 = metrics.visible_range(Px(0.0), Px(25.0), 0).expect("range");
878 assert_eq!(r0.start_index, 0);
879 assert_eq!(r0.end_index, 2);
880 assert_eq!(r0.count, 10);
881
882 let r1 = metrics.visible_range(Px(50.0), Px(20.0), 0).expect("range");
883 assert_eq!(r1.start_index, 5);
884 assert_eq!(r1.end_index, 6);
885
886 assert!(metrics.visible_range(Px(0.0), Px(0.0), 0).is_none());
887
888 let mut empty = VirtualListMetrics::default();
889 empty.ensure(0, Px(10.0), Px(0.0), Px(0.0));
890 assert!(empty.visible_range(Px(0.0), Px(10.0), 0).is_none());
891 }
892
893 #[test]
894 fn prefetch_virtual_range_step_skips_when_both_edges_near_without_direction_hint() {
895 let rendered = VirtualRange {
896 start_index: 10,
897 end_index: 20,
898 overscan: 2,
899 count: 100,
900 };
901 let visible = VirtualRange {
902 start_index: 9,
903 end_index: 21,
904 overscan: 0,
905 count: 100,
906 };
907
908 let prefetch = prefetch_virtual_range_step(rendered, visible, 1, 3, None);
909 assert_eq!(prefetch, None);
910 }
911
912 #[test]
913 fn prefetch_virtual_range_step_avoids_oscillation_by_preferring_direction_hint() {
914 let rendered = VirtualRange {
915 start_index: 10,
916 end_index: 20,
917 overscan: 2,
918 count: 100,
919 };
920 let visible = VirtualRange {
921 start_index: 9,
922 end_index: 21,
923 overscan: 0,
924 count: 100,
925 };
926
927 let forward = prefetch_virtual_range_step(rendered, visible, 1, 3, Some(true));
928 assert_eq!(
929 forward,
930 Some(VirtualRange {
931 start_index: 11,
932 end_index: 21,
933 overscan: 2,
934 count: 100
935 })
936 );
937
938 let backward = prefetch_virtual_range_step(rendered, visible, 1, 3, Some(false));
939 assert_eq!(
940 backward,
941 Some(VirtualRange {
942 start_index: 9,
943 end_index: 19,
944 overscan: 2,
945 count: 100
946 })
947 );
948 }
949
950 #[test]
951 fn scroll_offset_for_item_matches_nearest_semantics() {
952 let mut metrics = VirtualListMetrics::default();
953 metrics.ensure(10, Px(10.0), Px(0.0), Px(0.0));
954
955 assert_eq!(
957 metrics.scroll_offset_for_item(2, Px(50.0), Px(0.0), ScrollStrategy::Nearest),
958 Px(0.0)
959 );
960
961 assert_eq!(
963 metrics.scroll_offset_for_item(0, Px(20.0), Px(50.0), ScrollStrategy::Nearest),
964 Px(0.0)
965 );
966
967 assert_eq!(
969 metrics.scroll_offset_for_item(9, Px(20.0), Px(0.0), ScrollStrategy::Nearest),
970 Px(80.0)
971 );
972 }
973
974 #[test]
975 fn fixed_mode_range_math_matches_uniform_metrics() {
976 let mut metrics = VirtualListMetrics::default();
977 metrics.ensure_with_mode(
978 VirtualListMeasureMode::Fixed,
979 100,
980 Px(10.0),
981 Px(0.0),
982 Px(0.0),
983 );
984
985 assert!((metrics.total_height().0 - 1000.0).abs() < 0.01);
986 assert!((metrics.offset_for_index(0).0 - 0.0).abs() < 0.01);
987 assert!((metrics.offset_for_index(6).0 - 60.0).abs() < 0.01);
988 assert!((metrics.offset_for_index(100).0 - 1000.0).abs() < 0.01);
989
990 assert_eq!(metrics.index_for_offset(Px(0.0)), 0);
991 assert_eq!(metrics.index_for_offset(Px(0.1)), 0);
992 assert_eq!(metrics.index_for_offset(Px(9.9)), 0);
993 assert_eq!(metrics.index_for_offset(Px(10.0)), 1);
994 assert_eq!(metrics.index_for_offset(Px(59.9)), 5);
995 assert_eq!(metrics.index_for_offset(Px(60.0)), 6);
996 assert_eq!(metrics.end_index_for_offset(Px(50.0)), 5);
997 assert_eq!(metrics.end_index_for_offset(Px(50.1)), 6);
998
999 let r0 = metrics.visible_range(Px(0.0), Px(25.0), 0).expect("range");
1000 assert_eq!(r0.start_index, 0);
1001 assert_eq!(r0.end_index, 2);
1002 assert_eq!(r0.count, 100);
1003 }
1004
1005 #[test]
1006 fn known_mode_can_import_fixed_per_index_heights() {
1007 let mut metrics = VirtualListMetrics::default();
1008 metrics.ensure_with_mode(VirtualListMeasureMode::Known, 3, Px(10.0), Px(2.0), Px(4.0));
1009 metrics.rebuild_from_known_heights(
1010 vec![Px(10.0), Px(20.0), Px(30.0)],
1011 Px(10.0),
1012 Px(2.0),
1013 Px(4.0),
1014 );
1015
1016 assert_eq!(metrics.height_at(0), Px(10.0));
1017 assert_eq!(metrics.height_at(1), Px(20.0));
1018 assert_eq!(metrics.height_at(2), Px(30.0));
1019
1020 assert_eq!(metrics.total_height(), Px(68.0));
1022
1023 assert_eq!(metrics.offset_for_index(0), Px(4.0));
1024 assert_eq!(metrics.offset_for_index(1), Px(16.0)); assert_eq!(metrics.offset_for_index(2), Px(38.0)); }
1027}