virtualizer/
virtualizer.rs

1use alloc::sync::Arc;
2use alloc::vec::Vec;
3use core::cell::Cell;
4use core::cmp;
5
6use crate::fenwick::Fenwick;
7use crate::key::{KeyCacheKey, KeySizeMap};
8use crate::{
9    Align, InitialOffset, ItemKey, Range, Rect, ScrollDirection, VirtualItem, VirtualItemKeyed,
10    VirtualRange, VirtualizerOptions,
11};
12use crate::{FrameState, ScrollState, ViewportState};
13
14/// A headless virtualization engine.
15///
16/// This type is intentionally UI-agnostic:
17/// - It does not hold any UI objects.
18/// - Your adapter drives it by providing viewport geometry and scroll offsets.
19/// - Rendering is exposed via zero-allocation iteration APIs (`for_each_virtual_*`).
20///
21/// For smooth scrolling / tweens / anchoring patterns, see the `virtualizer-adapter` crate.
22#[derive(Clone, Debug)]
23pub struct Virtualizer<K = ItemKey> {
24    options: VirtualizerOptions<K>,
25    viewport_size: u32,
26    scroll_offset: u64,
27    scroll_rect: Rect,
28    is_scrolling: bool,
29    scroll_direction: Option<ScrollDirection>,
30    last_scroll_event_ms: Option<u64>,
31
32    sizes: Vec<u32>, // base sizes (no gap)
33    measured: Vec<bool>,
34    sums: Fenwick,
35    key_sizes: KeySizeMap<K>,
36
37    notify_depth: Cell<usize>,
38    notify_pending: Cell<bool>,
39}
40
41impl<K: KeyCacheKey> Virtualizer<K> {
42    /// Creates a new virtualizer from options.
43    ///
44    /// If `options.initial_rect` and/or `options.initial_offset` are set, those values are applied
45    /// immediately.
46    pub fn new(options: VirtualizerOptions<K>) -> Self {
47        let scroll_rect = options.initial_rect.unwrap_or_default();
48        let scroll_offset = options.initial_offset.resolve();
49        vdebug!(
50            count = options.count,
51            enabled = options.enabled,
52            overscan = options.overscan,
53            "Virtualizer::new"
54        );
55        let mut v = Self {
56            viewport_size: scroll_rect.main,
57            scroll_offset,
58            scroll_rect,
59            is_scrolling: false,
60            scroll_direction: None,
61            last_scroll_event_ms: None,
62            sizes: Vec::new(),
63            measured: Vec::new(),
64            sums: Fenwick::new(0),
65            key_sizes: KeySizeMap::<K>::new(),
66            options,
67            notify_depth: Cell::new(0),
68            notify_pending: Cell::new(false),
69        };
70        v.rebuild_estimates();
71        v
72    }
73
74    pub fn options(&self) -> &VirtualizerOptions<K> {
75        &self.options
76    }
77
78    fn reset_to_initial(&mut self) {
79        self.scroll_offset = self.options.initial_offset.resolve();
80        self.scroll_rect = self.options.initial_rect.unwrap_or_default();
81        self.viewport_size = self.scroll_rect.main;
82        self.is_scrolling = false;
83        self.scroll_direction = None;
84        self.last_scroll_event_ms = None;
85    }
86
87    pub fn set_options(&mut self, options: VirtualizerOptions<K>) {
88        let prev_count = self.options.count;
89        let prev_gap = self.options.gap;
90        let was_enabled = self.options.enabled;
91        let estimate_size_unchanged =
92            Arc::ptr_eq(&self.options.estimate_size, &options.estimate_size);
93        let get_item_key_unchanged = Arc::ptr_eq(&self.options.get_item_key, &options.get_item_key);
94        self.options = options;
95        vtrace!(
96            count = self.options.count,
97            enabled = self.options.enabled,
98            overscan = self.options.overscan,
99            "Virtualizer::set_options"
100        );
101
102        if !self.options.enabled {
103            self.viewport_size = 0;
104            self.scroll_offset = self.options.initial_offset.resolve();
105            self.scroll_rect = Rect::default();
106            self.is_scrolling = false;
107            self.scroll_direction = None;
108            self.last_scroll_event_ms = None;
109        } else if !was_enabled {
110            self.reset_to_initial();
111        } else if self.options.count != prev_count {
112            if estimate_size_unchanged && get_item_key_unchanged {
113                self.resize_count(prev_count, self.options.count);
114            } else {
115                self.rebuild_estimates();
116            }
117        } else if !estimate_size_unchanged || !get_item_key_unchanged {
118            self.rebuild_estimates();
119        } else if self.options.gap != prev_gap {
120            self.rebuild_fenwick();
121        }
122
123        self.notify();
124    }
125
126    /// Clones the current options, applies `f`, then delegates to `set_options`.
127    ///
128    /// This is useful when you want to update multiple options at once while letting the
129    /// virtualizer decide what needs to be rebuilt (estimates/fenwick/reset).
130    pub fn update_options(&mut self, f: impl FnOnce(&mut VirtualizerOptions<K>)) {
131        let mut next = self.options.clone();
132        f(&mut next);
133        self.set_options(next);
134    }
135
136    pub fn set_on_change(
137        &mut self,
138        on_change: Option<impl Fn(&Virtualizer<K>, bool) + Send + Sync + 'static>,
139    ) {
140        self.options.on_change = on_change.map(|f| Arc::new(f) as _);
141        self.notify();
142    }
143
144    pub fn set_initial_offset(&mut self, initial_offset: u64) {
145        self.options.initial_offset = InitialOffset::Value(initial_offset);
146        self.notify();
147    }
148
149    pub fn set_initial_offset_provider(
150        &mut self,
151        initial_offset: impl Fn() -> u64 + Send + Sync + 'static,
152    ) {
153        self.options.initial_offset = InitialOffset::Provider(Arc::new(initial_offset));
154        self.notify();
155    }
156
157    pub fn set_use_scrollend_event(&mut self, use_scrollend_event: bool) {
158        self.options.use_scrollend_event = use_scrollend_event;
159        self.notify();
160    }
161
162    pub fn set_is_scrolling_reset_delay_ms(&mut self, delay_ms: u64) {
163        self.options.is_scrolling_reset_delay_ms = delay_ms;
164        self.notify();
165    }
166
167    fn notify_now(&self) {
168        if let Some(cb) = &self.options.on_change {
169            cb(self, self.is_scrolling);
170        }
171    }
172
173    fn notify(&self) {
174        if self.notify_depth.get() > 0 {
175            self.notify_pending.set(true);
176            return;
177        }
178        self.notify_now();
179    }
180
181    /// Batches multiple updates into a single `on_change` notification.
182    ///
183    /// This is recommended for UI adapters: on a typical frame, you might update the scroll
184    /// rect, scroll offset, and `is_scrolling` state together. Without batching, each setter may
185    /// trigger `on_change`, which can be expensive if the callback drives rendering.
186    pub fn batch_update(&mut self, f: impl FnOnce(&mut Self)) {
187        let depth = self.notify_depth.get();
188        self.notify_depth.set(depth.saturating_add(1));
189
190        f(self);
191
192        let depth = self.notify_depth.get();
193        debug_assert!(depth > 0, "notify_depth underflow");
194        let next = depth.saturating_sub(1);
195        self.notify_depth.set(next);
196
197        if next == 0 && self.notify_pending.replace(false) {
198            self.notify_now();
199        }
200    }
201
202    pub fn count(&self) -> usize {
203        self.options.count
204    }
205
206    pub fn enabled(&self) -> bool {
207        self.options.enabled
208    }
209
210    pub fn set_enabled(&mut self, enabled: bool) {
211        if self.options.enabled == enabled {
212            return;
213        }
214        self.options.enabled = enabled;
215        if !enabled {
216            self.viewport_size = 0;
217            self.scroll_offset = self.options.initial_offset.resolve();
218            self.scroll_rect = Rect::default();
219            self.is_scrolling = false;
220            self.scroll_direction = None;
221            self.last_scroll_event_ms = None;
222        } else {
223            self.reset_to_initial();
224        }
225        self.notify();
226    }
227
228    pub fn is_scrolling(&self) -> bool {
229        self.is_scrolling
230    }
231
232    pub fn scroll_direction(&self) -> Option<ScrollDirection> {
233        self.scroll_direction
234    }
235
236    pub fn set_is_scrolling(&mut self, is_scrolling: bool) {
237        if self.is_scrolling == is_scrolling {
238            return;
239        }
240        self.is_scrolling = is_scrolling;
241        if !is_scrolling {
242            self.scroll_direction = None;
243            self.last_scroll_event_ms = None;
244        }
245        self.notify();
246    }
247
248    pub fn notify_scroll_event(&mut self, now_ms: u64) {
249        if !self.options.enabled {
250            return;
251        }
252        self.last_scroll_event_ms = Some(now_ms);
253        self.set_is_scrolling(true);
254    }
255
256    pub fn update_scrolling(&mut self, now_ms: u64) {
257        if !self.options.enabled {
258            return;
259        }
260        if self.options.use_scrollend_event {
261            return;
262        }
263        if !self.is_scrolling {
264            return;
265        }
266        let Some(last) = self.last_scroll_event_ms else {
267            return;
268        };
269        if now_ms.saturating_sub(last) >= self.options.is_scrolling_reset_delay_ms {
270            self.set_is_scrolling(false);
271        }
272    }
273
274    pub fn viewport_size(&self) -> u32 {
275        self.viewport_size
276    }
277
278    pub fn scroll_rect(&self) -> Rect {
279        self.scroll_rect
280    }
281
282    /// Returns a lightweight snapshot of the current viewport state.
283    pub fn viewport_state(&self) -> ViewportState {
284        ViewportState {
285            rect: self.scroll_rect,
286        }
287    }
288
289    /// Returns a lightweight snapshot of the current scroll state.
290    pub fn scroll_state(&self) -> ScrollState {
291        ScrollState {
292            offset: self.scroll_offset,
293            is_scrolling: self.is_scrolling,
294        }
295    }
296
297    /// Returns a combined snapshot of viewport + scroll state.
298    pub fn frame_state(&self) -> FrameState {
299        FrameState {
300            viewport: self.viewport_state(),
301            scroll: self.scroll_state(),
302        }
303    }
304
305    /// Restores viewport geometry from a previously captured snapshot.
306    pub fn restore_viewport_state(&mut self, viewport: ViewportState) {
307        self.set_scroll_rect(viewport.rect);
308    }
309
310    /// Restores scroll state from a previously captured snapshot.
311    ///
312    /// When `scroll.is_scrolling` is `true`, this will update the internal scrolling timers as if
313    /// a scroll event happened at `now_ms`.
314    pub fn restore_scroll_state(&mut self, scroll: ScrollState, now_ms: u64) {
315        if scroll.is_scrolling {
316            self.apply_scroll_offset_event_clamped(scroll.offset, now_ms);
317            return;
318        }
319        self.batch_update(|v| {
320            v.set_scroll_offset_clamped(scroll.offset);
321            v.set_is_scrolling(false);
322        });
323    }
324
325    /// Restores both viewport + scroll state from a previously captured snapshot.
326    ///
327    /// When `frame.scroll.is_scrolling` is `true`, this will update the internal scrolling timers
328    /// as if a scroll event happened at `now_ms`.
329    pub fn restore_frame_state(&mut self, frame: FrameState, now_ms: u64) {
330        if frame.scroll.is_scrolling {
331            self.apply_scroll_frame_clamped(frame.viewport.rect, frame.scroll.offset, now_ms);
332            return;
333        }
334        self.batch_update(|v| {
335            v.set_scroll_rect(frame.viewport.rect);
336            v.set_scroll_offset_clamped(frame.scroll.offset);
337            v.set_is_scrolling(false);
338        });
339    }
340
341    pub fn set_scroll_rect(&mut self, rect: Rect) {
342        if self.scroll_rect == rect {
343            return;
344        }
345        self.scroll_rect = rect;
346        self.viewport_size = rect.main;
347        self.notify();
348    }
349
350    /// Applies a scroll rect update from your UI layer.
351    ///
352    /// Prefer this (or `apply_scroll_frame*`) over calling multiple setters when you have an
353    /// `on_change` callback that may trigger expensive work (like rendering/layout).
354    pub fn apply_scroll_rect_event(&mut self, rect: Rect) {
355        self.batch_update(|v| {
356            v.set_scroll_rect(rect);
357        });
358    }
359
360    pub fn scroll_offset(&self) -> u64 {
361        self.scroll_offset
362    }
363
364    pub fn scroll_offset_in_list(&self) -> u64 {
365        let margin = self.options.scroll_margin as u64;
366        self.scroll_offset.saturating_sub(margin)
367    }
368
369    pub fn set_viewport_size(&mut self, size: u32) {
370        if self.viewport_size == size && self.scroll_rect.main == size {
371            return;
372        }
373        self.viewport_size = size;
374        self.scroll_rect.main = size;
375        self.notify();
376    }
377
378    pub fn set_scroll_offset(&mut self, offset: u64) {
379        if self.scroll_offset == offset {
380            return;
381        }
382        let prev = self.scroll_offset;
383        self.scroll_offset = offset;
384        self.scroll_direction = match offset.cmp(&prev) {
385            cmp::Ordering::Greater => Some(ScrollDirection::Forward),
386            cmp::Ordering::Less => Some(ScrollDirection::Backward),
387            cmp::Ordering::Equal => self.scroll_direction,
388        };
389        self.notify();
390    }
391
392    /// Applies a scroll offset update from your UI layer (e.g. wheel/drag), and marks the
393    /// virtualizer as scrolling.
394    pub fn apply_scroll_offset_event(&mut self, offset: u64, now_ms: u64) {
395        vtrace!(offset, now_ms, "apply_scroll_offset_event");
396        self.batch_update(|v| {
397            v.set_scroll_offset(offset);
398            v.notify_scroll_event(now_ms);
399        });
400    }
401
402    pub fn set_scroll_offset_clamped(&mut self, offset: u64) {
403        let clamped = self.clamp_scroll_offset(offset);
404        self.set_scroll_offset(clamped);
405    }
406
407    /// Same as `apply_scroll_offset_event`, but clamps the offset.
408    pub fn apply_scroll_offset_event_clamped(&mut self, offset: u64, now_ms: u64) {
409        vtrace!(offset, now_ms, "apply_scroll_offset_event_clamped");
410        self.batch_update(|v| {
411            v.set_scroll_offset_clamped(offset);
412            v.notify_scroll_event(now_ms);
413        });
414    }
415
416    pub fn set_viewport_and_scroll(&mut self, viewport_size: u32, scroll_offset: u64) {
417        self.batch_update(|v| {
418            v.set_viewport_size(viewport_size);
419            v.set_scroll_offset(scroll_offset);
420        });
421    }
422
423    pub fn set_viewport_and_scroll_clamped(&mut self, viewport_size: u32, scroll_offset: u64) {
424        self.batch_update(|v| {
425            v.set_viewport_size(viewport_size);
426            v.set_scroll_offset_clamped(scroll_offset);
427        });
428    }
429
430    /// Applies both scroll rect and scroll offset in a single coalesced update.
431    ///
432    /// This is the recommended entry point for UI adapters that receive scroll events along with
433    /// updated viewport/rect information.
434    pub fn apply_scroll_frame(&mut self, rect: Rect, scroll_offset: u64, now_ms: u64) {
435        vtrace!(
436            rect_main = rect.main,
437            rect_cross = rect.cross,
438            scroll_offset,
439            now_ms,
440            "apply_scroll_frame"
441        );
442        self.batch_update(|v| {
443            v.set_scroll_rect(rect);
444            v.set_scroll_offset(scroll_offset);
445            v.notify_scroll_event(now_ms);
446        });
447    }
448
449    /// Same as `apply_scroll_frame`, but clamps the offset.
450    pub fn apply_scroll_frame_clamped(&mut self, rect: Rect, scroll_offset: u64, now_ms: u64) {
451        vtrace!(
452            rect_main = rect.main,
453            rect_cross = rect.cross,
454            scroll_offset,
455            now_ms,
456            "apply_scroll_frame_clamped"
457        );
458        self.batch_update(|v| {
459            v.set_scroll_rect(rect);
460            v.set_scroll_offset_clamped(scroll_offset);
461            v.notify_scroll_event(now_ms);
462        });
463    }
464
465    pub fn set_count(&mut self, count: usize) {
466        if self.options.count == count {
467            return;
468        }
469        let prev = self.options.count;
470        self.options.count = count;
471        self.resize_count(prev, count);
472        self.notify();
473    }
474
475    pub fn set_overscan(&mut self, overscan: usize) {
476        self.options.overscan = overscan;
477        self.notify();
478    }
479
480    pub fn set_padding(&mut self, padding_start: u32, padding_end: u32) {
481        self.options.padding_start = padding_start;
482        self.options.padding_end = padding_end;
483        self.notify();
484    }
485
486    pub fn set_scroll_padding(&mut self, scroll_padding_start: u32, scroll_padding_end: u32) {
487        self.options.scroll_padding_start = scroll_padding_start;
488        self.options.scroll_padding_end = scroll_padding_end;
489        self.notify();
490    }
491
492    pub fn set_scroll_margin(&mut self, scroll_margin: u32) {
493        self.options.scroll_margin = scroll_margin;
494        self.notify();
495    }
496
497    pub fn set_gap(&mut self, gap: u32) {
498        if self.options.gap == gap {
499            return;
500        }
501        self.options.gap = gap;
502        self.rebuild_fenwick();
503        self.notify();
504    }
505
506    pub fn set_get_item_key(&mut self, f: impl Fn(usize) -> K + Send + Sync + 'static) {
507        self.options.get_item_key = Arc::new(f);
508        self.rebuild_estimates();
509        self.notify();
510    }
511
512    pub fn set_should_adjust_scroll_position_on_item_size_change(
513        &mut self,
514        f: Option<impl Fn(&Virtualizer<K>, VirtualItem, i64) -> bool + Send + Sync + 'static>,
515    ) {
516        self.options
517            .should_adjust_scroll_position_on_item_size_change = f.map(|f| Arc::new(f) as _);
518        self.notify();
519    }
520
521    pub fn sync_item_keys(&mut self) {
522        // Rebuild per-index sizes from the key-based cache and current estimates.
523        // Call this after your data set is reordered/changed while `count` stays the same.
524        let count = self.options.count;
525        self.sizes.clear();
526        self.measured.clear();
527        self.sizes.reserve_exact(count);
528        self.measured.reserve_exact(count);
529
530        for i in 0..count {
531            let key = self.key_for(i);
532            if let Some(&measured_size) = self.key_sizes.get(&key) {
533                self.sizes.push(measured_size);
534                self.measured.push(true);
535            } else {
536                self.sizes.push((self.options.estimate_size)(i));
537                self.measured.push(false);
538            }
539        }
540
541        self.rebuild_fenwick();
542        self.notify();
543    }
544
545    pub fn set_range_extractor(
546        &mut self,
547        f: Option<impl Fn(Range, &mut dyn FnMut(usize)) + Send + Sync + 'static>,
548    ) {
549        self.options.range_extractor = f.map(|f| Arc::new(f) as _);
550        self.notify();
551    }
552
553    pub fn set_estimate_size(&mut self, f: impl Fn(usize) -> u32 + Send + Sync + 'static) {
554        self.options.estimate_size = Arc::new(f);
555        self.rebuild_estimates();
556        self.notify();
557    }
558
559    pub fn reset_measurements(&mut self) {
560        self.key_sizes.clear();
561        self.rebuild_estimates();
562        self.notify();
563    }
564
565    /// Returns the number of cached measured sizes (key → size).
566    pub fn measurement_cache_len(&self) -> usize {
567        self.key_sizes.len()
568    }
569
570    /// Iterates over the cached measured sizes (key → size) without allocations.
571    pub fn for_each_cached_size(&self, mut f: impl FnMut(&K, u32)) {
572        for (k, v) in self.key_sizes.iter() {
573            f(k, *v);
574        }
575    }
576
577    /// Exports the cached measured sizes as a `Vec` (useful for persistence).
578    pub fn export_measurement_cache(&self) -> Vec<(K, u32)>
579    where
580        K: Clone,
581    {
582        let mut out = Vec::with_capacity(self.key_sizes.len());
583        self.for_each_cached_size(|k, v| out.push((k.clone(), v)));
584        out
585    }
586
587    /// Replaces the cached measured sizes from an iterator (useful when restoring state).
588    ///
589    /// Note: this rebuilds internal per-index sizes using the current key mapping.
590    pub fn import_measurement_cache(&mut self, entries: impl IntoIterator<Item = (K, u32)>) {
591        self.key_sizes.clear();
592        let mut n = 0usize;
593        for (k, v) in entries {
594            self.key_sizes.insert(k, v);
595            n = n.saturating_add(1);
596        }
597        vdebug!(entries = n, "import_measurement_cache");
598        self.rebuild_estimates();
599        self.notify();
600    }
601
602    /// Marks an item as measured and updates its cached size.
603    ///
604    /// This is aligned with TanStack Virtual's "measure element" behavior: if the measured item is
605    /// before the current scroll offset, the virtualizer may adjust `scroll_offset` to prevent a
606    /// visible "jump".
607    ///
608    /// If you want to update measurements without any scroll adjustment, use
609    /// `measure_unadjusted`.
610    pub fn measure(&mut self, index: usize, size: u32) {
611        if index >= self.options.count {
612            return;
613        }
614        let _ = self.resize_item(index, size);
615    }
616
617    /// Same as [`Self::measure`], but uses a precomputed key to avoid recomputing `get_item_key`.
618    pub fn measure_keyed(&mut self, index: usize, key: K, size: u32) {
619        if index >= self.options.count {
620            return;
621        }
622        let _ = self.resize_item_keyed(index, key, size);
623    }
624
625    /// Marks an item as measured and updates its cached size without adjusting `scroll_offset`.
626    pub fn measure_unadjusted(&mut self, index: usize, size: u32) {
627        if index >= self.options.count {
628            return;
629        }
630        let key = self.key_for(index);
631        self.measure_keyed_unadjusted(index, key, size);
632    }
633
634    /// Same as [`Self::measure_unadjusted`], but uses a precomputed key.
635    pub fn measure_keyed_unadjusted(&mut self, index: usize, key: K, size: u32) {
636        if index >= self.options.count {
637            return;
638        }
639        vtrace!(index, size, "measure_keyed_unadjusted");
640        self.set_item_size_keyed(index, key, size);
641        self.notify();
642    }
643
644    pub fn resize_item(&mut self, index: usize, size: u32) -> i64 {
645        if index >= self.options.count {
646            return 0;
647        }
648        let key = self.key_for(index);
649        self.resize_item_keyed(index, key, size)
650    }
651
652    pub fn resize_item_keyed(&mut self, index: usize, key: K, size: u32) -> i64 {
653        if index >= self.options.count {
654            return 0;
655        }
656        let item = self.item(index);
657        let delta = self.set_item_size_keyed(index, key, size);
658        if delta == 0 {
659            self.notify();
660            return 0;
661        }
662
663        let should_adjust = if let Some(f) = &self
664            .options
665            .should_adjust_scroll_position_on_item_size_change
666        {
667            f(self, item, delta)
668        } else {
669            item.start < self.scroll_offset
670        };
671
672        if should_adjust {
673            if delta > 0 {
674                self.scroll_offset = self.scroll_offset.saturating_add(delta as u64);
675            } else {
676                self.scroll_offset = self.scroll_offset.saturating_sub((-delta) as u64);
677            }
678            self.notify();
679            delta
680        } else {
681            self.notify();
682            0
683        }
684    }
685
686    fn set_item_size_keyed(&mut self, index: usize, key: K, size: u32) -> i64 {
687        let cur = self.sizes[index];
688        if cur == size {
689            self.measured[index] = true;
690            self.key_sizes.insert(key, size);
691            return 0;
692        }
693        self.sizes[index] = size;
694        self.measured[index] = true;
695        self.key_sizes.insert(key, size);
696        let delta = size as i64 - cur as i64;
697        self.sums.add(index, delta);
698        delta
699    }
700
701    /// Measures multiple items in one pass.
702    ///
703    /// Like [`Self::measure`], this may adjust `scroll_offset` to prevent jumps.
704    pub fn measure_many(&mut self, measurements: impl IntoIterator<Item = (usize, u32)>) {
705        let _ = self.resize_item_many(measurements);
706    }
707
708    /// Measures multiple items without adjusting `scroll_offset`.
709    pub fn measure_many_unadjusted(
710        &mut self,
711        measurements: impl IntoIterator<Item = (usize, u32)>,
712    ) {
713        for (index, size) in measurements {
714            if index >= self.options.count {
715                continue;
716            }
717            let key = self.key_for(index);
718            let cur = self.sizes[index];
719            if cur == size {
720                self.measured[index] = true;
721                self.key_sizes.insert(key, size);
722                continue;
723            }
724            self.sizes[index] = size;
725            self.measured[index] = true;
726            self.key_sizes.insert(key, size);
727            self.sums.add(index, size as i64 - cur as i64);
728        }
729        self.notify();
730    }
731
732    pub fn resize_item_many(
733        &mut self,
734        measurements: impl IntoIterator<Item = (usize, u32)>,
735    ) -> i64 {
736        let mut applied = 0i64;
737        self.batch_update(|v| {
738            for (index, size) in measurements {
739                if index >= v.options.count {
740                    continue;
741                }
742                applied += v.resize_item(index, size);
743            }
744        });
745        applied
746    }
747
748    pub fn is_measured(&self, index: usize) -> bool {
749        self.measured.get(index).copied().unwrap_or(false)
750    }
751
752    pub fn total_size(&self) -> u64 {
753        if !self.options.enabled {
754            return 0;
755        }
756        self.options.padding_start as u64 + self.sums.total() + self.options.padding_end as u64
757    }
758
759    pub fn key_for(&self, index: usize) -> K {
760        (self.options.get_item_key)(index)
761    }
762
763    pub fn virtual_range(&self) -> VirtualRange {
764        if !self.options.enabled {
765            return VirtualRange {
766                start_index: 0,
767                end_index: 0,
768            };
769        }
770        self.compute_range(self.scroll_offset, self.viewport_size)
771    }
772
773    pub fn virtual_range_for(&self, scroll_offset: u64, viewport_size: u32) -> VirtualRange {
774        if !self.options.enabled {
775            return VirtualRange {
776                start_index: 0,
777                end_index: 0,
778            };
779        }
780        self.compute_range(scroll_offset, viewport_size)
781    }
782
783    pub fn visible_range(&self) -> VirtualRange {
784        if !self.options.enabled {
785            return VirtualRange {
786                start_index: 0,
787                end_index: 0,
788            };
789        }
790        self.compute_visible_range(self.scroll_offset, self.viewport_size)
791    }
792
793    pub fn visible_range_for(&self, scroll_offset: u64, viewport_size: u32) -> VirtualRange {
794        if !self.options.enabled {
795            return VirtualRange {
796                start_index: 0,
797                end_index: 0,
798            };
799        }
800        self.compute_visible_range(scroll_offset, viewport_size)
801    }
802
803    pub fn for_each_virtual_index(&self, f: impl FnMut(usize)) {
804        self.for_each_virtual_index_for(self.scroll_offset, self.viewport_size, f);
805    }
806
807    pub fn for_each_virtual_index_for(
808        &self,
809        scroll_offset: u64,
810        viewport_size: u32,
811        mut f: impl FnMut(usize),
812    ) {
813        if !self.options.enabled {
814            return;
815        }
816
817        let visible = self.visible_range_for(scroll_offset, viewport_size);
818        if visible.is_empty() {
819            return;
820        }
821
822        let count = self.options.count;
823        let range = Range {
824            start_index: visible.start_index,
825            end_index: visible.end_index,
826            overscan: self.options.overscan,
827            count,
828        };
829
830        if let Some(extract) = &self.options.range_extractor {
831            let mut prev: Option<usize> = None;
832            extract(range, &mut |i| {
833                if i >= count {
834                    debug_assert!(
835                        i < count,
836                        "range_extractor emitted out-of-bounds index (i={i}, count={count})"
837                    );
838                    return;
839                }
840                if let Some(p) = prev {
841                    if i == p {
842                        return;
843                    }
844                    if i < p {
845                        debug_assert!(
846                            i > p,
847                            "range_extractor must emit sorted indexes (prev={p}, next={i})"
848                        );
849                        return;
850                    }
851                    debug_assert!(
852                        i > p,
853                        "range_extractor must emit sorted indexes (prev={p}, next={i})"
854                    );
855                }
856                prev = Some(i);
857                f(i);
858            });
859            return;
860        }
861
862        let overscan = self.options.overscan;
863        let start = visible.start_index.saturating_sub(overscan);
864        let end = cmp::min(count, visible.end_index.saturating_add(overscan));
865        for i in start..end {
866            f(i);
867        }
868    }
869
870    pub fn for_each_virtual_item(&self, f: impl FnMut(VirtualItem)) {
871        self.for_each_virtual_item_for(self.scroll_offset, self.viewport_size, f);
872    }
873
874    pub fn for_each_virtual_item_for(
875        &self,
876        scroll_offset: u64,
877        viewport_size: u32,
878        mut f: impl FnMut(VirtualItem),
879    ) {
880        if !self.options.enabled {
881            return;
882        }
883
884        let visible = self.visible_range_for(scroll_offset, viewport_size);
885        if visible.is_empty() {
886            return;
887        }
888
889        if self.options.range_extractor.is_some() {
890            self.for_each_virtual_index_for(scroll_offset, viewport_size, |i| {
891                f(self.item(i));
892            });
893            return;
894        }
895
896        let count = self.options.count;
897        let overscan = self.options.overscan;
898        let start_index = visible.start_index.saturating_sub(overscan);
899        let end_index = cmp::min(count, visible.end_index.saturating_add(overscan));
900        if start_index >= end_index {
901            return;
902        }
903
904        let margin = self.options.scroll_margin as u64;
905        let mut start = margin.saturating_add(self.start_of(start_index));
906        let gap = self.options.gap as u64;
907
908        for i in start_index..end_index {
909            let size = self.sizes[i];
910            f(VirtualItem {
911                index: i,
912                start,
913                size,
914            });
915
916            start = start.saturating_add(size as u64);
917            if gap > 0 && i + 1 < count {
918                start = start.saturating_add(gap);
919            }
920        }
921    }
922
923    pub fn for_each_virtual_item_keyed(&self, f: impl FnMut(VirtualItemKeyed<K>)) {
924        self.for_each_virtual_item_keyed_for(self.scroll_offset, self.viewport_size, f);
925    }
926
927    pub fn for_each_virtual_item_keyed_for(
928        &self,
929        scroll_offset: u64,
930        viewport_size: u32,
931        mut f: impl FnMut(VirtualItemKeyed<K>),
932    ) {
933        if !self.options.enabled {
934            return;
935        }
936
937        let visible = self.visible_range_for(scroll_offset, viewport_size);
938        if visible.is_empty() {
939            return;
940        }
941
942        if self.options.range_extractor.is_some() {
943            self.for_each_virtual_index_for(scroll_offset, viewport_size, |i| {
944                let item = self.item(i);
945                f(VirtualItemKeyed {
946                    key: self.key_for(i),
947                    index: item.index,
948                    start: item.start,
949                    size: item.size,
950                });
951            });
952            return;
953        }
954
955        let count = self.options.count;
956        let overscan = self.options.overscan;
957        let start_index = visible.start_index.saturating_sub(overscan);
958        let end_index = cmp::min(count, visible.end_index.saturating_add(overscan));
959        if start_index >= end_index {
960            return;
961        }
962
963        let margin = self.options.scroll_margin as u64;
964        let mut start = margin.saturating_add(self.start_of(start_index));
965        let gap = self.options.gap as u64;
966
967        for i in start_index..end_index {
968            let size = self.sizes[i];
969            f(VirtualItemKeyed {
970                key: self.key_for(i),
971                index: i,
972                start,
973                size,
974            });
975
976            start = start.saturating_add(size as u64);
977            if gap > 0 && i + 1 < count {
978                start = start.saturating_add(gap);
979            }
980        }
981    }
982
983    /// Programmatically scrolls to an index (no animation).
984    ///
985    /// This sets the internal `scroll_offset` to the computed (clamped) target and triggers
986    /// `on_change`. It does **not** mark the virtualizer as "scrolling".
987    ///
988    /// If you want "user scrolling" semantics (e.g. to drive `is_scrolling` debouncing), call
989    /// `apply_scroll_offset_event_clamped(scroll_to_index_offset(...), now_ms)` instead.
990    ///
991    /// Returns the applied (clamped) offset.
992    pub fn scroll_to_index(&mut self, index: usize, align: Align) -> u64 {
993        let offset = self.scroll_to_index_offset(index, align);
994        self.set_scroll_offset(offset);
995        offset
996    }
997
998    pub fn scroll_to_index_offset(&self, index: usize, align: Align) -> u64 {
999        if !self.options.enabled {
1000            return self.options.initial_offset.resolve();
1001        }
1002        if self.options.count == 0 {
1003            return 0;
1004        }
1005        let index = index.min(self.options.count - 1);
1006        let item = self.item(index);
1007
1008        let sp_start = self.options.scroll_padding_start as u64;
1009        let sp_end = self.options.scroll_padding_end as u64;
1010        let view = self.viewport_size as u64;
1011
1012        let target = match align {
1013            Align::Start => item.start.saturating_sub(sp_start),
1014            Align::End => item.end().saturating_add(sp_end).saturating_sub(view),
1015            Align::Center => {
1016                let center = item.start.saturating_add(item.size as u64 / 2);
1017                center.saturating_sub(view / 2)
1018            }
1019            Align::Auto => {
1020                let cur = self.scroll_offset;
1021                let cur_end = cur.saturating_add(view);
1022                if item.start >= cur && item.end() <= cur_end {
1023                    cur
1024                } else if item.start < cur {
1025                    item.start.saturating_sub(sp_start)
1026                } else {
1027                    item.end().saturating_add(sp_end).saturating_sub(view)
1028                }
1029            }
1030        };
1031
1032        self.clamp_scroll_offset(target)
1033    }
1034
1035    /// Collects virtual item indexes into `out` (clears `out` first).
1036    ///
1037    /// This is a convenience wrapper around [`Self::for_each_virtual_index`]. For maximum
1038    /// performance, prefer `for_each_virtual_index` and reuse a scratch buffer in your adapter.
1039    pub fn collect_virtual_indexes(&self, out: &mut Vec<usize>) {
1040        self.collect_virtual_indexes_for(self.scroll_offset, self.viewport_size, out);
1041    }
1042
1043    /// Collects virtual item indexes into `out` for a given `scroll_offset`/`viewport_size`.
1044    ///
1045    /// This clears `out` first.
1046    pub fn collect_virtual_indexes_for(
1047        &self,
1048        scroll_offset: u64,
1049        viewport_size: u32,
1050        out: &mut Vec<usize>,
1051    ) {
1052        out.clear();
1053        self.for_each_virtual_index_for(scroll_offset, viewport_size, |i| out.push(i));
1054    }
1055
1056    /// Collects virtual items into `out` (clears `out` first).
1057    ///
1058    /// This is a convenience wrapper around [`Self::for_each_virtual_item`]. For maximum
1059    /// performance, prefer `for_each_virtual_item` and reuse a scratch buffer in your adapter.
1060    pub fn collect_virtual_items(&self, out: &mut Vec<VirtualItem>) {
1061        self.collect_virtual_items_for(self.scroll_offset, self.viewport_size, out);
1062    }
1063
1064    /// Collects virtual items into `out` for a given `scroll_offset`/`viewport_size`.
1065    ///
1066    /// This clears `out` first.
1067    pub fn collect_virtual_items_for(
1068        &self,
1069        scroll_offset: u64,
1070        viewport_size: u32,
1071        out: &mut Vec<VirtualItem>,
1072    ) {
1073        out.clear();
1074        self.for_each_virtual_item_for(scroll_offset, viewport_size, |it| out.push(it));
1075    }
1076
1077    /// Collects keyed virtual items into `out` (clears `out` first).
1078    ///
1079    /// This is a convenience wrapper around [`Self::for_each_virtual_item_keyed`].
1080    pub fn collect_virtual_items_keyed(&self, out: &mut Vec<VirtualItemKeyed<K>>) {
1081        self.collect_virtual_items_keyed_for(self.scroll_offset, self.viewport_size, out);
1082    }
1083
1084    /// Collects keyed virtual items into `out` for a given `scroll_offset`/`viewport_size`.
1085    ///
1086    /// This clears `out` first.
1087    pub fn collect_virtual_items_keyed_for(
1088        &self,
1089        scroll_offset: u64,
1090        viewport_size: u32,
1091        out: &mut Vec<VirtualItemKeyed<K>>,
1092    ) {
1093        out.clear();
1094        self.for_each_virtual_item_keyed_for(scroll_offset, viewport_size, |it| out.push(it));
1095    }
1096
1097    pub fn index_at_offset(&self, offset: u64) -> Option<usize> {
1098        if !self.options.enabled {
1099            return None;
1100        }
1101        self.index_at_offset_inner(offset)
1102            .filter(|&i| i < self.options.count)
1103    }
1104
1105    pub fn item_start(&self, index: usize) -> Option<u64> {
1106        if !self.options.enabled {
1107            return None;
1108        }
1109        (index < self.options.count).then(|| {
1110            let margin = self.options.scroll_margin as u64;
1111            margin.saturating_add(self.start_of(index))
1112        })
1113    }
1114
1115    pub fn item_size(&self, index: usize) -> Option<u32> {
1116        if !self.options.enabled {
1117            return None;
1118        }
1119        self.sizes.get(index).copied()
1120    }
1121
1122    pub fn item_end(&self, index: usize) -> Option<u64> {
1123        let start = self.item_start(index)?;
1124        let size = self.item_size(index)? as u64;
1125        Some(start.saturating_add(size))
1126    }
1127
1128    pub fn virtual_item_for_offset(&self, offset: u64) -> Option<VirtualItem> {
1129        let index = self.index_at_offset(offset)?;
1130        Some(self.item(index))
1131    }
1132
1133    pub fn virtual_item_keyed_for_offset(&self, offset: u64) -> Option<VirtualItemKeyed<K>> {
1134        let index = self.index_at_offset(offset)?;
1135        let item = self.item(index);
1136        Some(VirtualItemKeyed {
1137            key: self.key_for(index),
1138            index: item.index,
1139            start: item.start,
1140            size: item.size,
1141        })
1142    }
1143
1144    fn rebuild_estimates(&mut self) {
1145        vdebug!(
1146            count = self.options.count,
1147            cached = self.key_sizes.len(),
1148            "rebuild_estimates"
1149        );
1150        self.sizes.clear();
1151        self.measured.clear();
1152        self.sizes
1153            .reserve_exact(self.options.count.saturating_sub(self.sizes.len()));
1154        self.measured
1155            .reserve_exact(self.options.count.saturating_sub(self.measured.len()));
1156
1157        for i in 0..self.options.count {
1158            let key = self.key_for(i);
1159            if let Some(&measured_size) = self.key_sizes.get(&key) {
1160                self.sizes.push(measured_size);
1161                self.measured.push(true);
1162            } else {
1163                self.sizes.push((self.options.estimate_size)(i));
1164                self.measured.push(false);
1165            }
1166        }
1167        self.rebuild_fenwick();
1168    }
1169
1170    fn rebuild_fenwick(&mut self) {
1171        self.sums = Fenwick::from_sizes(&self.sizes, self.options.gap);
1172    }
1173
1174    fn resize_count(&mut self, prev_count: usize, new_count: usize) {
1175        if self.sizes.len() != prev_count
1176            || self.measured.len() != prev_count
1177            || self.sums.len() != prev_count
1178        {
1179            // Defensive fallback: if internal invariants don't match the expected previous count,
1180            // rebuild from scratch (preserves correctness at the expense of performance).
1181            self.rebuild_estimates();
1182            return;
1183        }
1184
1185        let gap = self.options.gap as u64;
1186
1187        if new_count > prev_count {
1188            if gap > 0 && prev_count > 0 {
1189                // The previous last item was stored without a trailing gap. It is no longer last.
1190                self.sums.add(prev_count - 1, gap as i64);
1191            }
1192
1193            self.sizes.reserve_exact(new_count - prev_count);
1194            self.measured.reserve_exact(new_count - prev_count);
1195
1196            for i in prev_count..new_count {
1197                let key = self.key_for(i);
1198                let (size, is_measured) = if let Some(&measured_size) = self.key_sizes.get(&key) {
1199                    (measured_size, true)
1200                } else {
1201                    ((self.options.estimate_size)(i), false)
1202                };
1203
1204                self.sizes.push(size);
1205                self.measured.push(is_measured);
1206
1207                let mut value = size as u64;
1208                if gap > 0 && i + 1 < new_count {
1209                    value = value.saturating_add(gap);
1210                }
1211                self.sums.push_value(value);
1212            }
1213            return;
1214        }
1215
1216        // Shrink (or clear).
1217        self.sizes.truncate(new_count);
1218        self.measured.truncate(new_count);
1219        self.sums.truncate(new_count);
1220
1221        if gap > 0 && new_count > 0 && new_count < prev_count {
1222            // The new last item previously had a trailing gap; remove it.
1223            self.sums.add(new_count - 1, -(gap as i64));
1224        }
1225    }
1226
1227    fn item(&self, index: usize) -> VirtualItem {
1228        let margin = self.options.scroll_margin as u64;
1229        let start = margin.saturating_add(self.start_of(index));
1230        VirtualItem {
1231            index,
1232            start,
1233            size: self.sizes[index],
1234        }
1235    }
1236
1237    fn start_of(&self, index: usize) -> u64 {
1238        self.options.padding_start as u64 + self.sums.prefix_sum(index)
1239    }
1240
1241    pub fn max_scroll_offset(&self) -> u64 {
1242        if !self.options.enabled {
1243            return self.options.initial_offset.resolve();
1244        }
1245        let margin = self.options.scroll_margin as u64;
1246        let total = self.total_size();
1247        let view = self.viewport_size as u64;
1248        margin.saturating_add(total.saturating_sub(view))
1249    }
1250
1251    pub fn clamp_scroll_offset(&self, offset: u64) -> u64 {
1252        offset.min(self.max_scroll_offset())
1253    }
1254
1255    fn compute_range(&self, scroll_offset: u64, viewport_size: u32) -> VirtualRange {
1256        let mut range = self.compute_visible_range(scroll_offset, viewport_size);
1257        if range.is_empty() {
1258            return range;
1259        }
1260
1261        let count = self.options.count;
1262        let overscan = self.options.overscan;
1263        range.start_index = range.start_index.saturating_sub(overscan);
1264        range.end_index = cmp::min(count, range.end_index.saturating_add(overscan));
1265        range
1266    }
1267
1268    fn compute_visible_range(&self, scroll_offset: u64, viewport_size: u32) -> VirtualRange {
1269        let count = self.options.count;
1270        if count == 0 || viewport_size == 0 {
1271            return VirtualRange {
1272                start_index: 0,
1273                end_index: 0,
1274            };
1275        }
1276
1277        let margin = self.options.scroll_margin as u64;
1278        let view = viewport_size as u64;
1279
1280        let total = self.total_size();
1281        let max_scroll = margin.saturating_add(total.saturating_sub(view));
1282        let scroll_offset = scroll_offset.min(max_scroll);
1283        let scroll_end = scroll_offset.saturating_add(view);
1284        if scroll_end <= margin {
1285            return VirtualRange {
1286                start_index: 0,
1287                end_index: 0,
1288            };
1289        }
1290
1291        let off = scroll_offset.saturating_sub(margin);
1292        let visible_end_exclusive = scroll_end.saturating_sub(margin);
1293
1294        if off >= total {
1295            return VirtualRange {
1296                start_index: count,
1297                end_index: count,
1298            };
1299        }
1300
1301        let visible_start = off;
1302        let visible_end_inclusive = visible_end_exclusive.saturating_sub(1);
1303
1304        let mut start = self
1305            .index_at_offset_inner_list(visible_start)
1306            .unwrap_or(count);
1307        let mut end = self
1308            .index_at_offset_inner_list(cmp::max(visible_end_inclusive, visible_start))
1309            .map(|i| i + 1)
1310            .unwrap_or(count);
1311
1312        start = start.min(count);
1313        end = end.min(count);
1314
1315        VirtualRange {
1316            start_index: start,
1317            end_index: end,
1318        }
1319    }
1320
1321    fn index_at_offset_inner(&self, offset: u64) -> Option<usize> {
1322        let margin = self.options.scroll_margin as u64;
1323        if offset < margin {
1324            return Some(0);
1325        }
1326        self.index_at_offset_inner_list(offset - margin)
1327    }
1328
1329    fn index_at_offset_inner_list(&self, offset: u64) -> Option<usize> {
1330        let ps = self.options.padding_start as u64;
1331        if offset < ps {
1332            return Some(0);
1333        }
1334
1335        let off_in_items = offset - ps;
1336        let count = self.options.count;
1337        if count == 0 {
1338            return None;
1339        }
1340
1341        // Find the first item whose (effective) end is > off_in_items.
1342        // Fenwick lower_bound returns the number of items whose prefix sum is <= off_in_items.
1343        let consumed = self.sums.lower_bound(off_in_items);
1344        Some(consumed.min(count.saturating_sub(1)))
1345    }
1346}