Skip to main content

cvkg_layout/
progressive.rs

1use cvkg_core::{Alignment, Distribution, LayoutCache, LayoutView, Rect};
2
3#[derive(Debug, Clone)]
4pub struct ProgressiveChild {
5    pub hash: u64,
6    pub laid_out: bool,
7    pub rect: Rect,
8}
9
10/// Opt-in wrapper that breaks a single layout pass into incremental batches.
11///
12/// # Contract
13/// Saves partially computed layouts inside `LayoutCache` so the main UI thread does not stall for deep trees.
14pub struct ProgressiveLayoutContext<'a> {
15    pub children: &'a [&'a dyn LayoutView],
16    pub entries: Vec<ProgressiveChild>,
17    pub spacing: f32,
18    pub alignment: Alignment,
19    pub distribution: Distribution,
20    pub bounds: Rect,
21    pub completed: usize,
22    pub fallback_applied: bool,
23}
24
25impl<'a> ProgressiveLayoutContext<'a> {
26    /// Create a new progressive layout context for the given subviews.
27    pub fn new(
28        bounds: Rect,
29        subviews: &'a [&'a dyn LayoutView],
30        spacing: f32,
31        alignment: Alignment,
32        distribution: Distribution,
33    ) -> Self {
34        let entries = subviews
35            .iter()
36            .map(|v| ProgressiveChild {
37                hash: v.view_hash(),
38                laid_out: false,
39                rect: Rect::zero(),
40            })
41            .collect();
42
43        Self {
44            children: subviews,
45            entries,
46            spacing,
47            alignment,
48            distribution,
49            bounds,
50            completed: 0,
51            fallback_applied: false,
52        }
53    }
54
55    /// Layout up to `batch_size` additional children.
56    pub fn layout_next_batch(&mut self, batch_size: usize) -> bool {
57        self.layout_next_batch_inner(batch_size, None);
58        self.is_complete()
59    }
60
61    /// Variant of `layout_next_batch` that integrates with a persistent cache.
62    pub fn layout_next_batch_with_cache(
63        &mut self,
64        batch_size: usize,
65        cache: &mut LayoutCache,
66    ) -> (bool, Vec<Rect>) {
67        self.layout_next_batch_inner(batch_size, Some(cache));
68        let new_rects: Vec<Rect> = self
69            .entries
70            .iter()
71            .filter(|e| e.laid_out && e.rect != Rect::zero())
72            .map(|e| e.rect)
73            .collect();
74        (self.is_complete(), new_rects)
75    }
76
77    fn layout_next_batch_inner(
78        &mut self,
79        batch_size: usize,
80        mut cache: Option<&mut LayoutCache>,
81    ) {
82        let mut processed = 0;
83        let mut batch_indices = Vec::new();
84        for (i, entry) in self.entries.iter().enumerate() {
85            if entry.laid_out {
86                continue;
87            }
88            if processed >= batch_size {
89                break;
90            }
91            batch_indices.push(i);
92            processed += 1;
93        }
94
95        if batch_indices.is_empty() {
96            return;
97        }
98
99        let batch_subviews: Vec<&dyn LayoutView> = batch_indices
100            .iter()
101            .map(|&i| self.children[i])
102            .collect();
103
104        let rects = match cache {
105            Some(ref mut c) => crate::taffy_engine::HStack::compute_layout_incremental(
106                self.spacing,
107                self.alignment,
108                self.distribution,
109                self.bounds,
110                0,
111                &batch_subviews,
112                *c,
113            ),
114            None => {
115                let mut tmp = LayoutCache::new();
116                crate::taffy_engine::HStack::compute_layout_incremental(
117                    self.spacing,
118                    self.alignment,
119                    self.distribution,
120                    self.bounds,
121                    0,
122                    &batch_subviews,
123                    &mut tmp,
124                )
125            }
126        };
127
128        for (local_idx, &global_idx) in batch_indices.iter().enumerate() {
129            if local_idx < rects.len() {
130                self.entries[global_idx].rect = rects[local_idx];
131                self.entries[global_idx].laid_out = true;
132                self.completed += 1;
133            }
134        }
135
136        if let Some(c) = cache.as_mut() {
137            for (local_idx, &global_idx) in batch_indices.iter().enumerate() {
138                if local_idx < rects.len() {
139                    let hash = self.entries[global_idx].hash;
140                    if hash != 0 {
141                        c.previous_rects.insert(hash, rects[local_idx]);
142                    }
143                }
144            }
145        }
146    }
147
148    /// Returns `true` when every child has been laid out or fallback has been applied.
149    pub fn is_complete(&self) -> bool {
150        self.fallback_applied || self.completed >= self.entries.len()
151    }
152
153    /// Returns `(completed, total)` progress counts.
154    pub fn progress(&self) -> (usize, usize) {
155        (self.completed, self.entries.len())
156    }
157
158    /// Apply fallback positioning to all children that have not yet been laid out.
159    pub fn apply_remaining_fallback(&mut self, cache: &mut LayoutCache) -> Vec<Rect> {
160        let mut fallback_rects = Vec::new();
161        let remaining: Vec<usize> = self
162            .entries
163            .iter()
164            .enumerate()
165            .filter(|(_, e)| !e.laid_out)
166            .map(|(i, _)| i)
167            .collect();
168
169        if remaining.is_empty() {
170            self.fallback_applied = true;
171            return fallback_rects;
172        }
173
174        let cols = (remaining.len() as f32).sqrt().ceil() as usize;
175        let rows = (remaining.len() + cols - 1) / cols;
176        let cell_w = self.bounds.width / cols as f32;
177        let cell_h = self.bounds.height / rows as f32;
178
179        for (offset, &idx) in remaining.iter().enumerate() {
180            let hash = self.entries[idx].hash;
181            let rect = if hash != 0 {
182                cache
183                    .previous_rects
184                    .get(&hash)
185                    .copied()
186                    .unwrap_or_else(|| {
187                        let col = offset % cols;
188                        let row = offset / cols;
189                        Rect {
190                            x: self.bounds.x + col as f32 * cell_w,
191                            y: self.bounds.y + row as f32 * cell_h,
192                            width: cell_w,
193                            height: cell_h,
194                        }
195                    })
196            } else {
197                let col = offset % cols;
198                let row = offset / cols;
199                Rect {
200                    x: self.bounds.x + col as f32 * cell_w,
201                    y: self.bounds.y + row as f32 * cell_h,
202                    width: cell_w,
203                    height: cell_h,
204                }
205            };
206
207            self.entries[idx].rect = rect;
208            self.entries[idx].laid_out = true;
209            self.completed += 1;
210            if hash != 0 {
211                cache.previous_rects.insert(hash, rect);
212            }
213            fallback_rects.push(rect);
214        }
215
216        self.fallback_applied = true;
217        fallback_rects
218    }
219
220    /// Consume the context and return the final `Vec<Rect>` for all children in order.
221    pub fn take_rects(self) -> Vec<Rect> {
222        self.entries.into_iter().map(|e| e.rect).collect()
223    }
224}