cranpose_foundation/lazy/
prefetch.rs

1//! Prefetch scheduler for lazy layouts.
2//!
3//! Pre-composes items before they become visible to reduce jank during scrolling.
4//! Inspired by Jetpack Compose's `LazyListPrefetchStrategy`.
5
6use std::collections::VecDeque;
7
8/// Strategy for prefetching items in a lazy list.
9#[derive(Clone, Debug)]
10pub struct PrefetchStrategy {
11    /// Number of items to prefetch beyond the visible area.
12    /// Default is 2, matching JC's default.
13    pub prefetch_count: usize,
14
15    /// Whether prefetching is enabled.
16    pub enabled: bool,
17}
18
19impl Default for PrefetchStrategy {
20    fn default() -> Self {
21        Self {
22            prefetch_count: 2,
23            enabled: true,
24        }
25    }
26}
27
28impl PrefetchStrategy {
29    /// Creates a new prefetch strategy with the specified count.
30    pub fn new(prefetch_count: usize) -> Self {
31        Self {
32            prefetch_count,
33            enabled: true,
34        }
35    }
36
37    /// Disables prefetching.
38    pub fn disabled() -> Self {
39        Self {
40            prefetch_count: 0,
41            enabled: false,
42        }
43    }
44}
45
46/// Scheduler that tracks which items should be prefetched.
47///
48/// Based on scroll direction and velocity, determines which items
49/// to pre-compose before they become visible.
50#[derive(Debug, Default)]
51pub struct PrefetchScheduler {
52    /// Queue of indices to prefetch, ordered by priority.
53    prefetch_queue: VecDeque<usize>,
54}
55
56impl PrefetchScheduler {
57    /// Creates a new prefetch scheduler.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Updates the prefetch queue based on current scroll state.
63    ///
64    /// # Arguments
65    /// * `first_visible_index` - Index of the first visible item
66    /// * `last_visible_index` - Index of the last visible item  
67    /// * `total_items` - Total number of items in the list
68    /// * `scroll_direction` - Current scroll direction (positive = forward)
69    /// * `strategy` - Prefetch strategy to use
70    pub fn update(
71        &mut self,
72        first_visible_index: usize,
73        last_visible_index: usize,
74        total_items: usize,
75        scroll_direction: f32,
76        strategy: &PrefetchStrategy,
77    ) {
78        if !strategy.enabled {
79            self.prefetch_queue.clear();
80            return;
81        }
82
83        self.prefetch_queue.clear();
84
85        let prefetch_count = strategy.prefetch_count;
86
87        if scroll_direction >= 0.0 {
88            // Scrolling forward - prefetch items after visible area
89            for i in 1..=prefetch_count {
90                let index = last_visible_index.saturating_add(i);
91                if index < total_items {
92                    self.prefetch_queue.push_back(index);
93                }
94            }
95        } else {
96            // Scrolling backward - prefetch items before visible area
97            for i in 1..=prefetch_count {
98                if first_visible_index >= i {
99                    let index = first_visible_index - i;
100                    self.prefetch_queue.push_back(index);
101                }
102            }
103        }
104    }
105
106    /// Returns the next item index to prefetch, if any.
107    pub fn next_prefetch(&mut self) -> Option<usize> {
108        self.prefetch_queue.pop_front()
109    }
110
111    /// Returns all pending prefetch indices.
112    pub fn pending_prefetches(&self) -> &VecDeque<usize> {
113        &self.prefetch_queue
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_prefetch_forward_scroll() {
123        let mut scheduler = PrefetchScheduler::new();
124        let strategy = PrefetchStrategy::new(2);
125
126        scheduler.update(5, 10, 100, 1.0, &strategy);
127
128        assert_eq!(scheduler.next_prefetch(), Some(11));
129        assert_eq!(scheduler.next_prefetch(), Some(12));
130        assert_eq!(scheduler.next_prefetch(), None);
131    }
132
133    #[test]
134    fn test_prefetch_backward_scroll() {
135        let mut scheduler = PrefetchScheduler::new();
136        let strategy = PrefetchStrategy::new(2);
137
138        scheduler.update(5, 10, 100, -1.0, &strategy);
139
140        assert_eq!(scheduler.next_prefetch(), Some(4));
141        assert_eq!(scheduler.next_prefetch(), Some(3));
142        assert_eq!(scheduler.next_prefetch(), None);
143    }
144
145    #[test]
146    fn test_prefetch_at_end() {
147        let mut scheduler = PrefetchScheduler::new();
148        let strategy = PrefetchStrategy::new(2);
149
150        // At end of list
151        scheduler.update(95, 99, 100, 1.0, &strategy);
152
153        // Should not prefetch beyond list bounds
154        assert_eq!(scheduler.next_prefetch(), None);
155    }
156
157    #[test]
158    fn test_prefetch_disabled() {
159        let mut scheduler = PrefetchScheduler::new();
160        let strategy = PrefetchStrategy::disabled();
161
162        scheduler.update(5, 10, 100, 1.0, &strategy);
163
164        assert_eq!(scheduler.next_prefetch(), None);
165    }
166}