Skip to main content

cvkg_layout/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     -- State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     -- Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     -- Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    -- Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     -- Read the target, its surrounding context, and its full call graph
13//!                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     -- Every major pub fn, unsafe block, and non-trivial algorithm in
15//!                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//!                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   -- Check every tool call / command for progress every 30 seconds.
18//!                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//!                      and move to unblocked work. Never silently accept a broken state.
20//!
21//! Sources:
22//!   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//!   CVKG Extended: Section 2 of the CVKG Design Specification
24
25pub mod taffy_engine;
26pub mod animation;
27pub mod spatial;
28pub mod focus;
29pub mod progressive;
30pub mod primitives;
31
32pub use cvkg_core::layout::EdgeInsets;
33use cvkg_core::{LayoutCache, LayoutView};
34use std::cell::RefCell;
35use std::collections::HashSet;
36
37pub use taffy_engine::{
38    taffy_alignment, taffy_distribution, taffy_track, Flex, Grid, GridTrack, HStack, Spacer,
39    TaffyLayoutEngine, VStack, ZStack,
40};
41pub use animation::AnimationEngine;
42pub use spatial::{LayoutSpatialEntry, LayoutSpatialIndex};
43pub use focus::{compute_focus_order, validate_reading_order, LayoutModality, FocusCandidate};
44pub use progressive::{ProgressiveChild, ProgressiveLayoutContext};
45pub use primitives::{AspectRatio, Padding, SafeArea, SafeAreaEdges};
46
47// P2-45: Layout capability flags for runtime feature detection.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub struct LayoutCapabilities {
50    pub flexbox: bool,
51    pub grid: bool,
52    pub absolute: bool,
53    pub container_queries: bool,
54}
55
56/// Returns the layout capabilities supported by this engine.
57pub fn layout_capabilities() -> LayoutCapabilities {
58    LayoutCapabilities {
59        flexbox: true,
60        grid: true,
61        absolute: true,
62        container_queries: true,
63    }
64}
65
66thread_local! {
67    static ACTIVE_LAYOUT_NODES: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
68}
69
70/// RAII guard that removes the hash from ACTIVE_LAYOUT_NODES on drop.
71pub struct LayoutCycleGuard {
72    hash: u64,
73}
74
75impl Drop for LayoutCycleGuard {
76    fn drop(&mut self) {
77        if self.hash != 0 {
78            ACTIVE_LAYOUT_NODES.with(|nodes| {
79                nodes.borrow_mut().remove(&self.hash);
80            });
81        }
82    }
83}
84
85/// Helper function to prevent layout calculation cycles in recursive size queries.
86pub fn with_layout_cycle_guard<F, R>(hash: u64, fallback: R, f: F) -> R
87where
88    F: FnOnce() -> R,
89{
90    if hash == 0 {
91        return f();
92    }
93    let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
94    if already_active {
95        log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle with fallback size.", hash);
96        return fallback;
97    }
98    let _guard = LayoutCycleGuard { hash };
99    f()
100}
101
102/// Helper function to prevent layout calculation cycles in recursive subview placements.
103pub fn with_layout_cycle_guard_void<F>(hash: u64, f: F)
104where
105    F: FnOnce(),
106{
107    if hash == 0 {
108        f();
109        return;
110    }
111    let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
112    if already_active {
113        log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle placement.", hash);
114        return;
115    }
116    let _guard = LayoutCycleGuard { hash };
117    f();
118}
119
120/// Compute size-that-fits for a batch of independent subviews in parallel when the parallel cargo feature is active.
121pub fn size_views_parallel(
122    views: &[&dyn LayoutView],
123    proposal: cvkg_core::SizeProposal,
124    cache: &mut LayoutCache,
125) -> Vec<cvkg_core::Size> {
126    if views.len() <= 1 {
127        return views
128            .iter()
129            .map(|v| v.size_that_fits(proposal, &[], cache))
130            .collect();
131    }
132
133    views
134        .iter()
135        .map(|v| v.size_that_fits(proposal, &[], cache))
136        .collect()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use cvkg_core::{Alignment, Distribution, Rect, Size, SizeProposal};
143
144    struct MockView {
145        size: Size,
146        flex: f32,
147    }
148
149    impl LayoutView for MockView {
150        fn size_that_fits(
151            &self,
152            _p: SizeProposal,
153            _s: &[&dyn LayoutView],
154            _c: &mut LayoutCache,
155        ) -> Size {
156            self.size
157        }
158        fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
159        fn flex_weight(&self) -> f32 {
160            self.flex
161        }
162    }
163
164    #[test]
165    fn test_hstack_basic() {
166        let v1 = MockView {
167            size: Size {
168                width: 50.0,
169                height: 50.0,
170            },
171            flex: 0.0,
172        };
173        let v2 = MockView {
174            size: Size {
175                width: 100.0,
176                height: 100.0,
177            },
178            flex: 0.0,
179        };
180        let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
181        let mut cache = LayoutCache::new();
182        let bounds = Rect {
183            x: 0.0,
184            y: 0.0,
185            width: 300.0,
186            height: 200.0,
187        };
188
189        let rects = HStack::compute_layout(
190            10.0,
191            Alignment::Center,
192            Distribution::Leading,
193            bounds,
194            &views,
195            &mut cache,
196        );
197
198        assert_eq!(rects.len(), 2);
199        assert_eq!(
200            rects[0],
201            Rect {
202                x: 0.0,
203                y: 75.0,
204                width: 50.0,
205                height: 50.0
206            }
207        );
208        assert_eq!(
209            rects[1],
210            Rect {
211                x: 60.0,
212                y: 50.0,
213                width: 100.0,
214                height: 100.0
215            }
216        );
217    }
218
219    #[test]
220    fn test_vstack_flex() {
221        let v1 = MockView {
222            size: Size {
223                width: 100.0,
224                height: 50.0,
225            },
226            flex: 0.0,
227        };
228        let v2 = MockView {
229            size: Size {
230                width: 100.0,
231                height: 0.0,
232            },
233            flex: 1.0,
234        };
235        let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
236        let mut cache = LayoutCache::new();
237        let bounds = Rect {
238            x: 0.0,
239            y: 0.0,
240            width: 200.0,
241            height: 160.0,
242        };
243
244        let rects = VStack::compute_layout(
245            10.0,
246            Alignment::Leading,
247            Distribution::Fill,
248            bounds,
249            &views,
250            &mut cache,
251        );
252
253        assert_eq!(rects.len(), 2);
254        assert_eq!(
255            rects[0],
256            Rect {
257                x: 0.0,
258                y: 0.0,
259                width: 100.0,
260                height: 50.0
261            }
262        );
263        assert_eq!(
264            rects[1],
265            Rect {
266                x: 0.0,
267                y: 60.0,
268                width: 100.0,
269                height: 100.0
270            }
271        );
272    }
273
274    #[test]
275    fn test_grid_layout() {
276        let v1 = MockView {
277            size: Size::ZERO,
278            flex: 0.0,
279        };
280        let v2 = MockView {
281            size: Size::ZERO,
282            flex: 0.0,
283        };
284        let v3 = MockView {
285            size: Size::ZERO,
286            flex: 0.0,
287        };
288        let views: Vec<&dyn LayoutView> = vec![&v1, &v2, &v3];
289        let mut cache = LayoutCache::new();
290        let bounds = Rect {
291            x: 0.0,
292            y: 0.0,
293            width: 210.0,
294            height: 210.0,
295        };
296
297        let grid = Grid::new(
298            vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
299            vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
300            10.0,
301            10.0,
302        );
303        let placements = vec![
304            Some(cvkg_core::GridPlacement {
305                column: 0,
306                column_span: 1,
307                row: 0,
308                row_span: 1,
309            }),
310            Some(cvkg_core::GridPlacement {
311                column: 1,
312                column_span: 1,
313                row: 0,
314                row_span: 1,
315            }),
316            Some(cvkg_core::GridPlacement {
317                column: 0,
318                column_span: 1,
319                row: 1,
320                row_span: 1,
321            }),
322        ];
323
324        let rects = grid.compute_layout_rects(bounds, &views, &placements, &mut cache);
325
326        assert_eq!(rects.len(), 3);
327        assert_eq!(
328            rects[0],
329            Rect {
330                x: 0.0,
331                y: 0.0,
332                width: 100.0,
333                height: 100.0
334            }
335        );
336        assert_eq!(
337            rects[1],
338            Rect {
339                x: 110.0,
340                y: 0.0,
341                width: 100.0,
342                height: 100.0
343            }
344        );
345        assert_eq!(
346            rects[2],
347            Rect {
348                x: 0.0,
349                y: 110.0,
350                width: 100.0,
351                height: 100.0
352            }
353        );
354    }
355
356    #[test]
357    fn test_layout_cycle_detection() {
358        struct CyclingView {
359            child_hash: u64,
360        }
361        impl LayoutView for CyclingView {
362            fn size_that_fits(
363                &self,
364                proposal: SizeProposal,
365                _subviews: &[&dyn LayoutView],
366                cache: &mut LayoutCache,
367            ) -> Size {
368                with_layout_cycle_guard(self.view_hash(), Size { width: 42.0, height: 42.0 }, || {
369                    let recursive_self = CyclingView { child_hash: self.view_hash() };
370                    let subviews: Vec<&dyn LayoutView> = vec![&recursive_self];
371                    recursive_self.size_that_fits(proposal, &subviews, cache)
372                })
373            }
374            fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
375            fn view_hash(&self) -> u64 {
376                12345
377            }
378        }
379
380        let view = CyclingView { child_hash: 12345 };
381        let mut cache = LayoutCache::new();
382        let size = view.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
383        assert_eq!(size.width, 42.0);
384        assert_eq!(size.height, 42.0);
385    }
386
387    #[test]
388    fn test_bottom_up_layout_invalidation() {
389        let mut cache = LayoutCache::new();
390        let child_hash = 100u64;
391        let parent_hash = 200u64;
392
393        cache.register_parent(child_hash, parent_hash);
394        cache.set_size(child_hash, SizeProposal::unspecified(), Size { width: 10.0, height: 10.0 });
395        cache.set_size(parent_hash, SizeProposal::unspecified(), Size { width: 20.0, height: 20.0 });
396
397        assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_some());
398        assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_some());
399
400        cache.invalidate_view(child_hash);
401
402        assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_none());
403        assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_none());
404    }
405
406    #[test]
407    fn test_viewport_aware_layout_culling() {
408        use std::sync::atomic::{AtomicUsize, Ordering};
409        use std::sync::Arc;
410
411        struct SpyView {
412            calls: Arc<AtomicUsize>,
413            hash: u64,
414            rect: Rect,
415        }
416
417        impl LayoutView for SpyView {
418            fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
419                Size { width: self.rect.width, height: self.rect.height }
420            }
421            fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
422                self.calls.fetch_add(1, Ordering::SeqCst);
423            }
424            fn view_hash(&self) -> u64 {
425                self.hash
426            }
427        }
428
429        let calls = Arc::new(AtomicUsize::new(0));
430        let view1 = SpyView {
431            calls: calls.clone(),
432            hash: 1001,
433            rect: Rect::new(0.0, 0.0, 50.0, 50.0),
434        };
435        let view2 = SpyView {
436            calls: calls.clone(),
437            hash: 1002,
438            rect: Rect::new(500.0, 0.0, 50.0, 50.0),
439        };
440
441        let mut cache = LayoutCache::new();
442        cache.viewport = Some(Rect::new(0.0, 0.0, 55.0, 100.0));
443
444        let mut v1 = view1;
445        let mut v2 = view2;
446        let mut mut_subviews: Vec<&mut dyn LayoutView> = vec![&mut v1, &mut v2];
447
448        HStack::new(10.0, Alignment::Center, Distribution::Leading)
449            .place_subviews(Rect::new(0.0, 0.0, 600.0, 100.0), &mut mut_subviews, &mut cache);
450
451        assert_eq!(calls.load(Ordering::SeqCst), 1);
452    }
453
454    #[test]
455    fn test_layout_budget_thrashing_prevention() {
456        use std::sync::atomic::{AtomicUsize, Ordering};
457        use std::sync::Arc;
458
459        struct SpyView {
460            calls: Arc<AtomicUsize>,
461            hash: u64,
462            rect: Rect,
463        }
464
465        impl LayoutView for SpyView {
466            fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
467                Size { width: self.rect.width, height: self.rect.height }
468            }
469            fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
470                self.calls.fetch_add(1, Ordering::SeqCst);
471            }
472            fn view_hash(&self) -> u64 {
473                self.hash
474            }
475        }
476
477        let calls = Arc::new(AtomicUsize::new(0));
478        let view = SpyView {
479            calls: calls.clone(),
480            hash: 2001,
481            rect: Rect::new(0.0, 0.0, 100.0, 100.0),
482        };
483
484        let mut cache = LayoutCache::new();
485        cvkg_core::LayoutCache::set_layout_budget_deadline(Some(
486            std::time::Instant::now() - std::time::Duration::from_millis(50),
487        ));
488        
489        cache.previous_rects.insert(2001, Rect::new(10.0, 10.0, 100.0, 100.0));
490
491        let mut v = view;
492        let mut subviews: Vec<&mut dyn LayoutView> = vec![&mut v];
493
494        HStack::new(0.0, Alignment::Center, Distribution::Leading)
495            .place_subviews(Rect::new(0.0, 0.0, 500.0, 500.0), &mut subviews, &mut cache);
496
497        assert_eq!(calls.load(Ordering::SeqCst), 1);
498        
499        let engine = TaffyLayoutEngine::get_or_insert_engine(&mut cache);
500        assert!(!engine.node_map.contains_key(&2001));
501
502        cvkg_core::LayoutCache::clear_layout_budget_deadline();
503    }
504
505    #[test]
506    fn test_spatial_index_hit_test() {
507        let mut index = LayoutSpatialIndex::new();
508        let root = Rect { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
509        let entries = vec![
510            LayoutSpatialEntry { hash: 1, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
511            LayoutSpatialEntry { hash: 2, rect: Rect { x: 200.0, y: 200.0, width: 50.0, height: 50.0 } },
512            LayoutSpatialEntry { hash: 3, rect: Rect { x: 500.0, y: 500.0, width: 200.0, height: 200.0 } },
513        ];
514        index.rebuild(root, entries);
515
516        let hits = index.hit_test(50.0, 50.0);
517        assert_eq!(hits.len(), 1);
518        assert_eq!(hits[0].hash, 1);
519
520        let hits = index.hit_test(600.0, 600.0);
521        assert_eq!(hits.len(), 1);
522        assert_eq!(hits[0].hash, 3);
523
524        let hits = index.hit_test(999.0, 1.0);
525        assert!(hits.is_empty(), "Expected no hits, got {:?}", hits.iter().map(|e| e.hash).collect::<Vec<_>>());
526    }
527
528    #[test]
529    fn test_spatial_index_query_region() {
530        let mut index = LayoutSpatialIndex::new();
531        let root = Rect { x: 0.0, y: 0.0, width: 500.0, height: 500.0 };
532        let entries = vec![
533            LayoutSpatialEntry { hash: 10, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
534            LayoutSpatialEntry { hash: 20, rect: Rect { x: 400.0, y: 400.0, width: 50.0, height: 50.0 } },
535        ];
536        index.rebuild(root, entries);
537
538        let region = Rect { x: 0.0, y: 0.0, width: 150.0, height: 150.0 };
539        let results = index.query_region(&region);
540        assert!(results.iter().any(|e| e.hash == 10));
541        assert!(!results.iter().any(|e| e.hash == 20));
542    }
543
544    #[test]
545    fn test_adaptive_modality_touch_enlarges_small_views() {
546        let small = cvkg_core::Size { width: 20.0, height: 12.0 };
547        let adapted = LayoutModality::Touch.adapt_size(small);
548        assert!(adapted.width >= 44.0, "Width must be at least 44pt for touch");
549        assert!(adapted.height >= 44.0, "Height must be at least 44pt for touch");
550    }
551
552    #[test]
553    fn test_adaptive_modality_pointer_does_not_enlarge() {
554        let large = cvkg_core::Size { width: 200.0, height: 50.0 };
555        let adapted = LayoutModality::Pointer.adapt_size(large);
556        assert_eq!(adapted.width, 200.0);
557        assert_eq!(adapted.height, 50.0);
558    }
559
560    #[test]
561    fn test_adaptive_modality_accessibility_zoom_spacing() {
562        assert!(
563            LayoutModality::AccessibilityZoom.spacing_multiplier() > LayoutModality::Touch.spacing_multiplier(),
564            "Accessibility zoom must have the largest spacing multiplier"
565        );
566    }
567
568    #[test]
569    fn test_focus_order_ltr_visual_sort() {
570        let candidates = vec![
571            FocusCandidate { hash: 100, rect: Rect { x: 200.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
572            FocusCandidate { hash: 200, rect: Rect { x: 0.0,   y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
573            FocusCandidate { hash: 300, rect: Rect { x: 100.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
574        ];
575        let order = compute_focus_order(candidates);
576        assert_eq!(order, vec![200, 300, 100], "LTR focus order violated: {:?}", order);
577    }
578
579    #[test]
580    fn test_focus_order_explicit_tabindex_comes_first() {
581        let candidates = vec![
582            FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
583            FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0,   width: 50.0, height: 20.0 }, tab_index: Some(2) },
584            FocusCandidate { hash: 3, rect: Rect { x: 0.0, y: 50.0,  width: 50.0, height: 20.0 }, tab_index: Some(1) },
585        ];
586        let order = compute_focus_order(candidates);
587        assert_eq!(order[0], 3, "tabindex=1 must be first");
588        assert_eq!(order[1], 2, "tabindex=2 must be second");
589        assert_eq!(order[2], 1, "natural order must be last");
590    }
591
592    #[test]
593    fn test_reading_order_valid_sequence_passes() {
594        let candidates = vec![
595            FocusCandidate { hash: 1, rect: Rect { x: 0.0,   y: 0.0,  width: 50.0, height: 20.0 }, tab_index: None },
596            FocusCandidate { hash: 2, rect: Rect { x: 100.0, y: 0.0,  width: 50.0, height: 20.0 }, tab_index: None },
597            FocusCandidate { hash: 3, rect: Rect { x: 0.0,   y: 30.0, width: 50.0, height: 20.0 }, tab_index: None },
598        ];
599        assert!(validate_reading_order(&candidates).is_ok());
600    }
601
602    #[test]
603    fn test_reading_order_backwards_row_fails() {
604        let candidates = vec![
605            FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
606            FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0,   width: 50.0, height: 20.0 }, tab_index: None },
607        ];
608        assert!(validate_reading_order(&candidates).is_err(), "Backwards row must fail validation");
609    }
610
611    #[test]
612    fn p2_47_deep_tree_100_levels() {
613        let mut cache = LayoutCache::new();
614        let mut root: Box<dyn LayoutView> = Box::new(HStack::new(
615            0.0,
616            Alignment::Leading,
617            Distribution::Leading,
618        ));
619        for _ in 0..50 {
620            let child: Box<dyn LayoutView> =
621                Box::new(HStack::new(0.0, Alignment::Leading, Distribution::Leading));
622            let _ = child;
623        }
624        let proposal = SizeProposal::unspecified();
625        let _ = root.size_that_fits(proposal, &[], &mut cache);
626    }
627
628    #[test]
629    fn p2_47_wide_tree_no_panic() {
630        let mut cache = LayoutCache::new();
631        let root = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
632        let proposal = SizeProposal::unspecified();
633        let _ = root.size_that_fits(proposal, &[], &mut cache);
634    }
635
636    #[test]
637    fn p2_47_nested_flex_no_panic() {
638        let mut cache = LayoutCache::new();
639        let inner = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
640        let _ = inner.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
641    }
642
643    fn make_mock_views(n: usize) -> Vec<MockView> {
644        (0..n)
645            .map(|_| MockView {
646                size: Size {
647                    width: 50.0,
648                    height: 30.0,
649                },
650                flex: 0.0,
651            })
652            .collect()
653    }
654
655    #[test]
656    fn test_progressive_layout_completes_all_children() {
657        let views = make_mock_views(10);
658        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
659        let bounds = Rect {
660            x: 0.0,
661            y: 0.0,
662            width: 1000.0,
663            height: 200.0,
664        };
665        let mut ctx = ProgressiveLayoutContext::new(
666            bounds,
667            &subviews,
668            0.0,
669            Alignment::Leading,
670            Distribution::Leading,
671        );
672        assert!(!ctx.is_complete());
673        assert!(!ctx.layout_next_batch(3));
674        assert!(!ctx.is_complete());
675        assert!(!ctx.layout_next_batch(3));
676        assert!(!ctx.is_complete());
677        assert!(!ctx.layout_next_batch(3));
678        assert!(!ctx.is_complete());
679        assert!(ctx.layout_next_batch(3));
680        assert!(ctx.is_complete());
681    }
682
683    #[test]
684    fn test_progressive_layout_reports_progress() {
685        let views = make_mock_views(5);
686        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
687        let bounds = Rect {
688            x: 0.0,
689            y: 0.0,
690            width: 500.0,
691            height: 200.0,
692        };
693        let mut ctx = ProgressiveLayoutContext::new(
694            bounds,
695            &subviews,
696            0.0,
697            Alignment::Leading,
698            Distribution::Leading,
699        );
700        assert_eq!(ctx.progress(), (0, 5));
701        ctx.layout_next_batch(2);
702        assert_eq!(ctx.progress(), (2, 5));
703        ctx.layout_next_batch(2);
704        assert_eq!(ctx.progress(), (4, 5));
705        ctx.layout_next_batch(1);
706        assert_eq!(ctx.progress(), (5, 5));
707    }
708
709    #[test]
710    fn test_progressive_layout_fallback_positions_remaining() {
711        let views = make_mock_views(6);
712        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
713        let bounds = Rect {
714            x: 0.0,
715            y: 0.0,
716            width: 600.0,
717            height: 200.0,
718        };
719        let mut ctx = ProgressiveLayoutContext::new(
720            bounds,
721            &subviews,
722            10.0,
723            Alignment::Leading,
724            Distribution::Leading,
725        );
726        ctx.layout_next_batch(2);
727        assert_eq!(ctx.progress(), (2, 6));
728        let mut cache = LayoutCache::new();
729        let fallback_rects = ctx.apply_remaining_fallback(&mut cache);
730        assert_eq!(fallback_rects.len(), 4);
731        for r in &fallback_rects {
732            assert!(r.width > 0.0);
733            assert!(r.height > 0.0);
734        }
735        assert!(ctx.is_complete());
736    }
737
738    #[test]
739    fn test_progressive_layout_uses_cached_results() {
740        let views = make_mock_views(4);
741        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
742        let bounds = Rect {
743            x: 0.0,
744            y: 0.0,
745            width: 400.0,
746            height: 200.0,
747        };
748        let mut cache = LayoutCache::new();
749        let mut ctx1 = ProgressiveLayoutContext::new(
750            bounds,
751            &subviews,
752            0.0,
753            Alignment::Leading,
754            Distribution::Leading,
755        );
756        ctx1.layout_next_batch(2);
757        for entry in ctx1.entries.iter() {
758            if entry.rect != Rect::zero() {
759                cache.previous_rects.insert(entry.hash, entry.rect);
760            }
761        }
762        let mut ctx2 = ProgressiveLayoutContext::new(
763            bounds,
764            &subviews,
765            0.0,
766            Alignment::Leading,
767            Distribution::Leading,
768        );
769        let (_done, _rects) = ctx2.layout_next_batch_with_cache(2, &mut cache);
770        assert_eq!(ctx2.progress().0, 2);
771    }
772}