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 use cvkg_core::layout::EdgeInsets;
26use cvkg_core::{Alignment, Distribution, LayoutCache, LayoutView, Rect, Size, SizeProposal};
27use std::collections::HashMap;
28use std::cell::RefCell;
29use std::collections::HashSet;
30
31// P2-45: Layout capability flags for runtime feature detection.
32// Applications can query these to determine which layout modes are supported.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub struct LayoutCapabilities {
35    pub flexbox: bool,
36    pub grid: bool,
37    pub absolute: bool,
38    pub container_queries: bool,
39}
40
41/// Returns the layout capabilities supported by this engine.
42pub fn layout_capabilities() -> LayoutCapabilities {
43    LayoutCapabilities {
44        flexbox: true,
45        grid: true,
46        absolute: true,
47        container_queries: true,
48    }
49}
50
51thread_local! {
52    static ACTIVE_LAYOUT_NODES: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
53}
54
55/// Helper function to prevent layout calculation cycles in recursive size queries.
56/// If a view is already being traversed on the current thread, returns the fallback size.
57fn with_layout_cycle_guard<F, R>(hash: u64, fallback: R, f: F) -> R
58where
59    F: FnOnce() -> R,
60{
61    if hash == 0 {
62        return f();
63    }
64    let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
65    if already_active {
66        log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle with fallback size.", hash);
67        return fallback;
68    }
69    let res = f();
70    ACTIVE_LAYOUT_NODES.with(|nodes| {
71        nodes.borrow_mut().remove(&hash);
72    });
73    res
74}
75
76/// Helper function to prevent layout calculation cycles in recursive subview placements.
77fn with_layout_cycle_guard_void<F>(hash: u64, f: F)
78where
79    F: FnOnce(),
80{
81    if hash == 0 {
82        f();
83        return;
84    }
85    let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
86    if already_active {
87        log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle placement.", hash);
88        return;
89    }
90    f();
91    ACTIVE_LAYOUT_NODES.with(|nodes| {
92        nodes.borrow_mut().remove(&hash);
93    });
94}
95
96/// The central Taffy engine that computes flexbox and grid layouts.
97/// Stored opaquely inside `cvkg_core::LayoutCache::engine`.
98pub struct TaffyLayoutEngine {
99    pub tree: taffy::TaffyTree,
100    pub node_map: HashMap<u64, taffy::NodeId>,
101}
102
103impl Default for TaffyLayoutEngine {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl TaffyLayoutEngine {
110    pub fn new() -> Self {
111        Self {
112            tree: taffy::TaffyTree::new(),
113            node_map: HashMap::new(),
114        }
115    }
116
117    pub fn get_or_insert_engine(cache: &mut LayoutCache) -> &mut Self {
118        if cache.engine.is_none() {
119            cache.engine = Some(Box::new(TaffyLayoutEngine::new()));
120        }
121        cache
122            .engine
123            .as_mut()
124            .unwrap()
125            .downcast_mut::<TaffyLayoutEngine>()
126            .unwrap()
127    }
128}
129
130/// Manages active physics transitions for layout bounding boxes.
131pub struct AnimationEngine {
132    pub active_transitions: HashMap<u64, cvkg_anim::physics::ViscousSpring>,
133    /// Generation counter for transition eviction.
134    pub eviction_generation: u64,
135    /// Tracks which generation each transition was last touched in.
136    pub transition_generation: HashMap<u64, u64>,
137    /// Number of generations a transition can go untouched before eviction.
138    pub eviction_threshold: u64,
139}
140
141impl Default for AnimationEngine {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147impl AnimationEngine {
148    pub fn new() -> Self {
149        Self {
150            active_transitions: HashMap::new(),
151            eviction_generation: 0,
152            transition_generation: HashMap::new(),
153            eviction_threshold: 300,
154        }
155    }
156
157    pub fn get_or_insert_engine(cache: &mut LayoutCache) -> &mut Self {
158        if cache.animators.is_none() {
159            cache.animators = Some(Box::new(AnimationEngine::new()));
160        }
161        cache
162            .animators
163            .as_mut()
164            .unwrap()
165            .downcast_mut::<AnimationEngine>()
166            .unwrap()
167    }
168
169    /// Evict settled transitions that haven't been touched for N generations.
170    pub fn evict_stale_transitions(&mut self) {
171        self.eviction_generation += 1;
172        let threshold = self.eviction_threshold;
173        let current_gen = self.eviction_generation;
174        self.active_transitions.retain(|hash, spring| {
175            let recent = self
176                .transition_generation
177                .get(hash)
178                .map_or(false, |g| current_gen - *g < threshold);
179            let unsettled = spring.velocity_a.length_sq() > 0.0001 || spring.velocity_b.length_sq() > 0.0001;
180            recent || unsettled
181        });
182        self.transition_generation
183            .retain(|hash, _| self.active_transitions.contains_key(hash));
184    }
185}
186
187use taffy::prelude::*;
188
189fn taffy_alignment(alignment: cvkg_core::Alignment) -> Option<taffy::AlignItems> {
190    match alignment {
191        cvkg_core::Alignment::Leading => Some(taffy::AlignItems::Start),
192        cvkg_core::Alignment::Center => Some(taffy::AlignItems::Center),
193        cvkg_core::Alignment::Trailing => Some(taffy::AlignItems::End),
194        cvkg_core::Alignment::Top => Some(taffy::AlignItems::Start),
195        cvkg_core::Alignment::Bottom => Some(taffy::AlignItems::End),
196    }
197}
198
199fn taffy_distribution(dist: cvkg_core::Distribution) -> Option<taffy::JustifyContent> {
200    match dist {
201        cvkg_core::Distribution::Leading => Some(taffy::JustifyContent::Start),
202        cvkg_core::Distribution::Center => Some(taffy::JustifyContent::Center),
203        cvkg_core::Distribution::Trailing => Some(taffy::JustifyContent::End),
204        cvkg_core::Distribution::SpaceBetween => Some(taffy::JustifyContent::SpaceBetween),
205        cvkg_core::Distribution::Fill => Some(taffy::JustifyContent::Stretch),
206        _ => None,
207    }
208}
209
210/// Taffy flex layout parameters.
211#[derive(Clone, Copy)]
212struct FlexParams {
213    dir: taffy::FlexDirection,
214    spacing: f32,
215    alignment: cvkg_core::Alignment,
216    distribution: cvkg_core::Distribution,
217    bounds: Rect,
218    container_hash: u64,
219}
220
221/// Collect intrinsic sizes for all children without running the Taffy solver.
222/// This is the expensive part (recursively calling size_that_fits on each child),
223/// so we do it once and reuse the results for both measure and placement.
224fn collect_child_sizes(
225    subviews: &[&dyn LayoutView],
226    bounds: Rect,
227    cache: &mut LayoutCache,
228) -> (Vec<u64>, Vec<f32>, Vec<Size>) {
229    let mut sizes = Vec::with_capacity(subviews.len());
230    let mut hashes = Vec::with_capacity(subviews.len());
231    let mut flex_weights = Vec::with_capacity(subviews.len());
232
233    for child in subviews {
234        let hash = child.view_hash();
235        hashes.push(hash);
236        flex_weights.push(child.flex_weight());
237
238        let proposal = SizeProposal::new(Some(bounds.width), Some(bounds.height));
239        let cached_size = if hash != 0 {
240            cache.get_size(hash, proposal)
241        } else {
242            None
243        };
244
245        let size = match cached_size {
246            Some(sz) => sz,
247            None => {
248                let sz = with_layout_cycle_guard(hash, Size::ZERO, || {
249                    child.size_that_fits(proposal, &[], cache)
250                });
251                if hash != 0 {
252                    cache.set_size(hash, proposal, sz);
253                }
254                sz
255            }
256        };
257        if hash != 0 {
258            cache.register_parent(hash, 0); // parent registered by caller if needed
259        }
260        sizes.push(size);
261    }
262
263    (hashes, flex_weights, sizes)
264}
265
266/// Compute the natural (intrinsic) size of a flex container from child sizes,
267/// without running the Taffy solver. Used by size_that_fits to avoid a full layout.
268fn intrinsic_flex_size(dir: taffy::FlexDirection, spacing: f32, sizes: &[Size]) -> Size {
269    if sizes.is_empty() {
270        return Size::ZERO;
271    }
272    let n = sizes.len();
273    match dir {
274        taffy::FlexDirection::Row | taffy::FlexDirection::RowReverse => {
275            let total_width: f32 = sizes.iter().map(|s| s.width).sum();
276            let max_height: f32 = sizes.iter().map(|s| s.height).fold(0.0, f32::max);
277            Size {
278                width: total_width + spacing * (n.saturating_sub(1) as f32),
279                height: max_height,
280            }
281        }
282        taffy::FlexDirection::Column | taffy::FlexDirection::ColumnReverse => {
283            let max_width: f32 = sizes.iter().map(|s| s.width).fold(0.0, f32::max);
284            let total_height: f32 = sizes.iter().map(|s| s.height).sum();
285            Size {
286                width: max_width,
287                height: total_height + spacing * (n.saturating_sub(1) as f32),
288            }
289        }
290    }
291}
292
293fn compute_taffy_flex(
294    params: &FlexParams,
295    subviews: &[&dyn LayoutView],
296    cache: &mut LayoutCache,
297) -> Vec<Rect> {
298    if cache.is_over_budget() {
299        let mut rects = Vec::with_capacity(subviews.len());
300        for child in subviews {
301            let hash = child.view_hash();
302            let r = if hash != 0 {
303                cache.previous_rects.get(&hash).copied().unwrap_or(Rect::zero())
304            } else {
305                Rect::zero()
306            };
307            rects.push(r);
308        }
309        return rects;
310    }
311
312    // Collect child sizes (the expensive part -- done once, reused by place_subviews).
313    let (hashes, flex_weights, sizes) = collect_child_sizes(subviews, params.bounds, cache);
314
315    // Register parent relationships for invalidation propagation.
316    for &hash in &hashes {
317        if hash != 0 && params.container_hash != 0 {
318            cache.register_parent(hash, params.container_hash);
319        }
320    }
321
322    let engine = TaffyLayoutEngine::get_or_insert_engine(cache);
323    let mut child_nodes = Vec::with_capacity(subviews.len());
324
325    for ((&hash, &flex_weight), &size) in hashes.iter().zip(&flex_weights).zip(&sizes) {
326        let style = if flex_weight > 0.0 {
327            taffy::Style {
328                size: taffy::Size {
329                    width: if params.dir == taffy::FlexDirection::Row {
330                        taffy::Dimension::Auto
331                    } else {
332                        taffy::Dimension::Length(size.width)
333                    },
334                    height: if params.dir == taffy::FlexDirection::Column {
335                        taffy::Dimension::Auto
336                    } else {
337                        taffy::Dimension::Length(size.height)
338                    },
339                },
340                flex_grow: flex_weight,
341                flex_basis: taffy::Dimension::Percent(0.0),
342                ..Default::default()
343            }
344        } else {
345            taffy::Style {
346                size: taffy::Size {
347                    width: taffy::Dimension::Length(size.width),
348                    height: taffy::Dimension::Length(size.height),
349                },
350                ..Default::default()
351            }
352        };
353
354        let node = if hash != 0 {
355            if let Some(&existing) = engine.node_map.get(&hash) {
356                let _ = engine.tree.set_style(existing, style);
357                existing
358            } else {
359                let new_node = engine.tree.new_leaf(style).unwrap();
360                engine.node_map.insert(hash, new_node);
361                new_node
362            }
363        } else {
364            engine.tree.new_leaf(style).unwrap()
365        };
366        child_nodes.push(node);
367    }
368
369    let gap_val = taffy::LengthPercentage::Length(params.spacing);
370    let container_style = taffy::Style {
371        display: taffy::Display::Flex,
372        flex_direction: params.dir,
373        gap: taffy::Size {
374            width: if params.dir == taffy::FlexDirection::Row {
375                gap_val
376            } else {
377                taffy::LengthPercentage::Length(0.0)
378            },
379            height: if params.dir == taffy::FlexDirection::Column {
380                gap_val
381            } else {
382                taffy::LengthPercentage::Length(0.0)
383            },
384        },
385        align_items: taffy_alignment(params.alignment),
386        justify_content: taffy_distribution(params.distribution),
387        size: taffy::Size {
388            width: taffy::Dimension::Length(params.bounds.width),
389            height: taffy::Dimension::Length(params.bounds.height),
390        },
391        ..Default::default()
392    };
393
394    let root_node = if params.container_hash != 0 {
395        if let Some(&existing) = engine.node_map.get(&params.container_hash) {
396            let _ = engine.tree.set_style(existing, container_style);
397            let _ = engine.tree.set_children(existing, &child_nodes);
398            existing
399        } else {
400            let new_node = engine
401                .tree
402                .new_with_children(container_style, &child_nodes)
403                .unwrap();
404            engine.node_map.insert(params.container_hash, new_node);
405            new_node
406        }
407    } else {
408        engine
409            .tree
410            .new_with_children(container_style, &child_nodes)
411            .unwrap()
412    };
413
414    engine
415        .tree
416        .compute_layout(root_node, taffy::Size::MAX_CONTENT)
417        .unwrap();
418
419    let mut rects = Vec::with_capacity(subviews.len());
420    for &node in &child_nodes {
421        let layout = engine.tree.layout(node).unwrap();
422        rects.push(Rect {
423            x: params.bounds.x + layout.location.x,
424            y: params.bounds.y + layout.location.y,
425            width: layout.size.width,
426            height: layout.size.height,
427        });
428    }
429
430    if params.container_hash == 0 {
431        let _ = engine.tree.remove(root_node);
432    }
433
434    rects
435}
436
437/// Applies view transitions to calculated layout rects.
438fn apply_layout_animations(
439    rects: Vec<Rect>,
440    subviews: &mut [&mut dyn LayoutView],
441    cache: &mut LayoutCache,
442) {
443    let mut transitions_to_update = Vec::new();
444
445    for (child, target_rect) in subviews.iter().zip(&rects) {
446        let hash = child.view_hash();
447        if hash != 0 {
448            if let Some(prev) = cache.previous_rects.get(&hash) {
449                let dx = (prev.x - target_rect.x).abs();
450                let dy = (prev.y - target_rect.y).abs();
451                let dw = (prev.width - target_rect.width).abs();
452                let dh = (prev.height - target_rect.height).abs();
453                let epsilon = 1e-3;
454                if dx > epsilon || dy > epsilon || dw > epsilon || dh > epsilon {
455                    transitions_to_update.push((hash, *prev, *target_rect));
456                }
457            }
458            cache.previous_rects.insert(hash, *target_rect);
459            cache.previous_rects_generation.insert(hash, cache.eviction_generation);
460        }
461    }
462
463    let mut interpolated_rects = HashMap::new();
464    let delta = cache.delta_time;
465    let scale = cache.scale_factor;
466    let anim_engine = AnimationEngine::get_or_insert_engine(cache);
467
468    for (hash, prev, target_rect) in transitions_to_update {
469        let mut spring = if let Some(mut existing) = anim_engine.active_transitions.remove(&hash) {
470            existing.position_b =
471                cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width);
472            existing
473        } else {
474            cvkg_anim::physics::ViscousSpring::new(
475                cvkg_anim::physics::Vec3::new(prev.x, prev.y, prev.width),
476                cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width),
477                0.9,
478                1000.0,
479            )
480        };
481        spring.step(delta);
482
483        // Temporal layout snapping: snap layout coordinates to integer pixels
484        // only when the spring has nearly settled to prevent jitter during motion.
485        let speed = (spring.velocity_a.length_sq() + spring.velocity_b.length_sq()).sqrt();
486        let snap = |v: f32| (v * scale).round() / scale;
487
488        let (rx, ry, rw) = if speed < 0.05 {
489            (
490                snap(spring.position_a.x),
491                snap(spring.position_a.y),
492                snap(spring.position_a.z),
493            )
494        } else {
495            (
496                spring.position_a.x,
497                spring.position_a.y,
498                spring.position_a.z,
499            )
500        };
501
502        interpolated_rects.insert(
503            hash,
504            Rect {
505                x: rx,
506                y: ry,
507                width: rw,
508                height: target_rect.height,
509            },
510        );
511        anim_engine.active_transitions.insert(hash, spring);
512        anim_engine.transition_generation.insert(hash, anim_engine.eviction_generation);
513    }
514    // Drop anim_engine before evicting cache entries (borrow conflict)
515    // anim_engine dropped implicitly (borrow conflict resolved by scope)
516
517    // Evict stale entries to prevent unbounded growth over long sessions.
518    cache.evict_stale_entries();
519
520    // Re-borrow anim_engine for its own eviction
521    let anim_engine = AnimationEngine::get_or_insert_engine(cache);
522    anim_engine.evict_stale_transitions();
523
524    for (child, mut target_rect) in subviews.iter_mut().zip(rects) {
525        let hash = child.view_hash();
526        if let Some(interp) = interpolated_rects.get(&hash) {
527            target_rect = *interp;
528        }
529        let is_visible = if let Some(viewport) = cache.viewport {
530            target_rect.intersects(&viewport)
531        } else {
532            true
533        };
534        if is_visible {
535            with_layout_cycle_guard_void(hash, || {
536                child.place_subviews(target_rect, &mut [], cache);
537            });
538        }
539    }
540}
541
542/// HStack - lays out children horizontally
543pub struct HStack {
544    spacing: f32,
545    alignment: Alignment,
546    distribution: Distribution,
547}
548
549impl HStack {
550    /// Create a new HStack with the given spacing, alignment, and distribution
551    pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
552        Self {
553            spacing,
554            alignment,
555            distribution,
556        }
557    }
558
559    /// Compute the layout rects for children without placing them.
560    pub fn compute_layout(
561        spacing: f32,
562        alignment: Alignment,
563        distribution: Distribution,
564        bounds: Rect,
565        subviews: &[&dyn LayoutView],
566        cache: &mut LayoutCache,
567    ) -> Vec<Rect> {
568        Self::compute_layout_incremental(
569            spacing,
570            alignment,
571            distribution,
572            bounds,
573            0,
574            subviews,
575            cache,
576        )
577    }
578
579    pub fn compute_layout_incremental(
580        spacing: f32,
581        alignment: Alignment,
582        distribution: Distribution,
583        bounds: Rect,
584        container_hash: u64,
585        subviews: &[&dyn LayoutView],
586        cache: &mut LayoutCache,
587    ) -> Vec<Rect> {
588        compute_taffy_flex(
589            &FlexParams {
590                dir: taffy::FlexDirection::Row,
591                spacing,
592                alignment,
593                distribution,
594                bounds,
595                container_hash,
596            },
597            subviews,
598            cache,
599        )
600    }
601}
602
603impl LayoutView for HStack {
604    fn size_that_fits(
605        &self,
606        proposal: SizeProposal,
607        subviews: &[&dyn LayoutView],
608        cache: &mut LayoutCache,
609    ) -> Size {
610        let bounds = Rect {
611            x: 0.0,
612            y: 0.0,
613            width: proposal.width.unwrap_or(10000.0),
614            height: proposal.height.unwrap_or(10000.0),
615        };
616        // Fast path: collect child sizes and compute intrinsic size without Taffy solve.
617        // The full Taffy solve happens only in place_subviews.
618        let (_, _, sizes) = collect_child_sizes(subviews, bounds, cache);
619        intrinsic_flex_size(taffy::FlexDirection::Row, self.spacing, &sizes)
620    }
621
622    fn place_subviews(
623        &self,
624        bounds: Rect,
625        subviews: &mut [&mut dyn LayoutView],
626        cache: &mut LayoutCache,
627    ) {
628        let views: Vec<&dyn LayoutView> =
629            subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
630        let rects = Self::compute_layout_incremental(
631            self.spacing,
632            self.alignment,
633            self.distribution,
634            bounds,
635            self.view_hash(),
636            &views,
637            cache,
638        );
639        apply_layout_animations(rects, subviews, cache);
640    }
641}
642
643/// VStack - lays out children vertically
644pub struct VStack {
645    spacing: f32,
646    alignment: Alignment,
647    distribution: Distribution,
648}
649
650impl VStack {
651    /// Create a new VStack with the given spacing, alignment, and distribution
652    pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
653        Self {
654            spacing,
655            alignment,
656            distribution,
657        }
658    }
659
660    /// Compute the layout rects for children without placing them.
661    pub fn compute_layout(
662        spacing: f32,
663        alignment: Alignment,
664        distribution: Distribution,
665        bounds: Rect,
666        subviews: &[&dyn LayoutView],
667        cache: &mut LayoutCache,
668    ) -> Vec<Rect> {
669        Self::compute_layout_incremental(
670            spacing,
671            alignment,
672            distribution,
673            bounds,
674            0,
675            subviews,
676            cache,
677        )
678    }
679
680    pub fn compute_layout_incremental(
681        spacing: f32,
682        alignment: Alignment,
683        distribution: Distribution,
684        bounds: Rect,
685        container_hash: u64,
686        subviews: &[&dyn LayoutView],
687        cache: &mut LayoutCache,
688    ) -> Vec<Rect> {
689        compute_taffy_flex(
690            &FlexParams {
691                dir: taffy::FlexDirection::Column,
692                spacing,
693                alignment,
694                distribution,
695                bounds,
696                container_hash,
697            },
698            subviews,
699            cache,
700        )
701    }
702}
703
704impl LayoutView for VStack {
705    fn size_that_fits(
706        &self,
707        proposal: SizeProposal,
708        subviews: &[&dyn LayoutView],
709        cache: &mut LayoutCache,
710    ) -> Size {
711        let bounds = Rect {
712            x: 0.0,
713            y: 0.0,
714            width: proposal.width.unwrap_or(10000.0),
715            height: proposal.height.unwrap_or(10000.0),
716        };
717        // Fast path: collect child sizes and compute intrinsic size without Taffy solve.
718        let (_, _, sizes) = collect_child_sizes(subviews, bounds, cache);
719        intrinsic_flex_size(taffy::FlexDirection::Column, self.spacing, &sizes)
720    }
721
722    fn place_subviews(
723        &self,
724        bounds: Rect,
725        subviews: &mut [&mut dyn LayoutView],
726        cache: &mut LayoutCache,
727    ) {
728        let views: Vec<&dyn LayoutView> =
729            subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
730        let rects = Self::compute_layout_incremental(
731            self.spacing,
732            self.alignment,
733            self.distribution,
734            bounds,
735            self.view_hash(),
736            &views,
737            cache,
738        );
739        apply_layout_animations(rects, subviews, cache);
740    }
741}
742
743/// ZStack - lays out children on top of each other
744pub struct ZStack {}
745
746impl Default for ZStack {
747    fn default() -> Self {
748        Self::new()
749    }
750}
751
752impl ZStack {
753    /// Create a new ZStack
754    pub fn new() -> Self {
755        Self {}
756    }
757}
758
759impl LayoutView for ZStack {
760    fn size_that_fits(
761        &self,
762        proposal: SizeProposal,
763        subviews: &[&dyn LayoutView],
764        cache: &mut LayoutCache,
765    ) -> Size {
766        let mut width = 0.0f32;
767        let mut height = 0.0f32;
768        let self_hash = self.view_hash();
769
770        for child in subviews.iter() {
771            let child_hash = child.view_hash();
772            if self_hash != 0 && child_hash != 0 {
773                cache.register_parent(child_hash, self_hash);
774            }
775            let child_size = with_layout_cycle_guard(child_hash, Size::ZERO, || {
776                child.size_that_fits(proposal, &[], cache)
777            });
778            width = width.max(child_size.width);
779            height = height.max(child_size.height);
780        }
781
782        Size { width, height }
783    }
784
785    fn place_subviews(
786        &self,
787        bounds: Rect,
788        subviews: &mut [&mut dyn LayoutView],
789        cache: &mut LayoutCache,
790    ) {
791        let self_hash = self.view_hash();
792        for child in subviews.iter_mut() {
793            let child_hash = child.view_hash();
794            if self_hash != 0 && child_hash != 0 {
795                cache.register_parent(child_hash, self_hash);
796            }
797            let is_visible = if let Some(viewport) = cache.viewport {
798                bounds.intersects(&viewport)
799            } else {
800                true
801            };
802            if is_visible {
803                with_layout_cycle_guard_void(child_hash, || {
804                    child.place_subviews(bounds, &mut [], cache);
805                });
806            }
807        }
808    }
809}
810
811/// Spacer - a layout view that expands to fill available space
812pub struct Spacer;
813
814impl LayoutView for Spacer {
815    fn size_that_fits(
816        &self,
817        proposal: SizeProposal,
818        _subviews: &[&dyn LayoutView],
819        _cache: &mut LayoutCache,
820    ) -> Size {
821        Size {
822            width: proposal.width.unwrap_or(0.0),
823            height: proposal.height.unwrap_or(0.0),
824        }
825    }
826
827    fn place_subviews(
828        &self,
829        _bounds: Rect,
830        _subviews: &mut [&mut dyn LayoutView],
831        _cache: &mut LayoutCache,
832    ) {
833    }
834}
835
836/// Flex - a container that distributes space among its children flexibly
837pub struct Flex {
838    pub orientation: cvkg_core::Orientation,
839    pub spacing: f32,
840}
841
842impl Flex {
843    pub fn new(orientation: cvkg_core::Orientation, spacing: f32) -> Self {
844        Self {
845            orientation,
846            spacing,
847        }
848    }
849}
850
851impl LayoutView for Flex {
852    fn size_that_fits(
853        &self,
854        proposal: SizeProposal,
855        _subviews: &[&dyn LayoutView],
856        _cache: &mut LayoutCache,
857    ) -> Size {
858        Size {
859            width: proposal.width.unwrap_or(100.0),
860            height: proposal.height.unwrap_or(100.0),
861        }
862    }
863
864    fn place_subviews(
865        &self,
866        bounds: Rect,
867        subviews: &mut [&mut dyn LayoutView],
868        cache: &mut LayoutCache,
869    ) {
870        if subviews.is_empty() {
871            return;
872        }
873
874        let self_hash = self.view_hash();
875        let n = subviews.len() as f32;
876        match self.orientation {
877            cvkg_core::Orientation::Horizontal => {
878                let total_spacing = self.spacing * (n - 1.0);
879                let item_width = (bounds.width - total_spacing) / n;
880                for (i, child) in subviews.iter_mut().enumerate() {
881                    let child_rect = Rect {
882                        x: bounds.x + i as f32 * (item_width + self.spacing),
883                        y: bounds.y,
884                        width: item_width,
885                        height: bounds.height,
886                    };
887                    let child_hash = child.view_hash();
888                    if self_hash != 0 && child_hash != 0 {
889                        cache.register_parent(child_hash, self_hash);
890                    }
891                    let is_visible = if let Some(viewport) = cache.viewport {
892                        child_rect.intersects(&viewport)
893                    } else {
894                        true
895                    };
896                    if is_visible {
897                        with_layout_cycle_guard_void(child_hash, || {
898                            child.place_subviews(child_rect, &mut [], cache);
899                        });
900                    }
901                }
902            }
903            cvkg_core::Orientation::Vertical => {
904                let total_spacing = self.spacing * (n - 1.0);
905                let item_height = (bounds.height - total_spacing) / n;
906                for (i, child) in subviews.iter_mut().enumerate() {
907                    let child_rect = Rect {
908                        x: bounds.x,
909                        y: bounds.y + i as f32 * (item_height + self.spacing),
910                        width: bounds.width,
911                        height: item_height,
912                    };
913                    let child_hash = child.view_hash();
914                    if self_hash != 0 && child_hash != 0 {
915                        cache.register_parent(child_hash, self_hash);
916                    }
917                    let is_visible = if let Some(viewport) = cache.viewport {
918                        child_rect.intersects(&viewport)
919                    } else {
920                        true
921                    };
922                    if is_visible {
923                        with_layout_cycle_guard_void(child_hash, || {
924                            child.place_subviews(child_rect, &mut [], cache);
925                        });
926                    }
927                }
928            }
929        }
930    }
931}
932
933/// Track sizing strategy for a single grid track (row or column).
934#[derive(Debug, Clone, Copy, PartialEq)]
935pub enum GridTrack {
936    /// Exact pixel size.
937    Fixed(f32),
938    /// Proportional weight compared to other flex tracks.
939    Flex(f32),
940    /// Size based on the intrinsic size of the grid item.
941    Auto,
942    /// Size clamped between minimum and maximum bounds.
943    MinMax(f32, f32),
944}
945
946fn taffy_track(track: GridTrack) -> taffy::TrackSizingFunction {
947    match track {
948        GridTrack::Fixed(v) => taffy::prelude::length(v),
949        GridTrack::Flex(v) => taffy::prelude::fr(v),
950        GridTrack::Auto => taffy::prelude::auto(),
951        GridTrack::MinMax(min, max) => {
952            taffy::prelude::minmax(taffy::prelude::length(min), taffy::prelude::length(max))
953        }
954    }
955}
956
957/// A layout engine that computes coordinates for children positioned in a 2D grid.
958pub struct Grid {
959    /// Column track sizing rules.
960    pub columns: Vec<GridTrack>,
961    /// Row track sizing rules.
962    pub rows: Vec<GridTrack>,
963    /// Empty space between columns.
964    pub column_gap: f32,
965    /// Empty space between rows.
966    pub row_gap: f32,
967}
968
969impl Grid {
970    /// Creates a new Grid layout engine.
971    pub fn new(
972        columns: Vec<GridTrack>,
973        rows: Vec<GridTrack>,
974        column_gap: f32,
975        row_gap: f32,
976    ) -> Self {
977        Self {
978            columns,
979            rows,
980            column_gap,
981            row_gap,
982        }
983    }
984
985    /// Computes the rects for children based on track sizing and grid placements.
986    pub fn compute_layout_rects(
987        &self,
988        bounds: Rect,
989        subviews: &[&dyn LayoutView],
990        placements: &[Option<cvkg_core::GridPlacement>],
991        cache: &mut LayoutCache,
992    ) -> Vec<Rect> {
993        self.compute_layout_rects_incremental(bounds, 0, subviews, placements, cache)
994    }
995
996    pub fn compute_layout_rects_incremental(
997        &self,
998        bounds: Rect,
999        container_hash: u64,
1000        subviews: &[&dyn LayoutView],
1001        placements: &[Option<cvkg_core::GridPlacement>],
1002        cache: &mut LayoutCache,
1003    ) -> Vec<Rect> {
1004        if cache.is_over_budget() {
1005            let mut rects = Vec::with_capacity(subviews.len());
1006            for child in subviews {
1007                let hash = child.view_hash();
1008                let r = if hash != 0 {
1009                    cache.previous_rects.get(&hash).copied().unwrap_or(Rect::zero())
1010                } else {
1011                    Rect::zero()
1012                };
1013                rects.push(r);
1014            }
1015            return rects;
1016        }
1017
1018        let mut hashes = Vec::with_capacity(subviews.len());
1019        for child in subviews {
1020            let hash = child.view_hash();
1021            hashes.push(hash);
1022            if container_hash != 0 && hash != 0 {
1023                cache.register_parent(hash, container_hash);
1024            }
1025        }
1026
1027        let engine = TaffyLayoutEngine::get_or_insert_engine(cache);
1028        let mut child_nodes = Vec::with_capacity(subviews.len());
1029
1030        for (hash, placement) in hashes.iter().zip(placements.iter()) {
1031            let style = if let Some(p) = placement.as_ref() {
1032                taffy::Style {
1033                    size: taffy::Size {
1034                        width: taffy::Dimension::Auto,
1035                        height: taffy::Dimension::Auto,
1036                    },
1037                    grid_column: taffy::Line {
1038                        start: taffy::prelude::line((p.column + 1) as i16),
1039                        end: taffy::prelude::span(p.column_span as u16),
1040                    },
1041                    grid_row: taffy::Line {
1042                        start: taffy::prelude::line((p.row + 1) as i16),
1043                        end: taffy::prelude::span(p.row_span as u16),
1044                    },
1045                    ..Default::default()
1046                }
1047            } else {
1048                taffy::Style {
1049                    size: taffy::Size {
1050                        width: taffy::Dimension::Auto,
1051                        height: taffy::Dimension::Auto,
1052                    },
1053                    ..Default::default()
1054                }
1055            };
1056
1057            let node = if *hash != 0 {
1058                if let Some(&existing) = engine.node_map.get(hash) {
1059                    let _ = engine.tree.set_style(existing, style);
1060                    existing
1061                } else {
1062                    let new_node = engine.tree.new_leaf(style).unwrap();
1063                    engine.node_map.insert(*hash, new_node);
1064                    new_node
1065                }
1066            } else {
1067                engine.tree.new_leaf(style).unwrap()
1068            };
1069            child_nodes.push(node);
1070        }
1071
1072        let container_style = taffy::Style {
1073            display: taffy::Display::Grid,
1074            grid_template_columns: self.columns.iter().copied().map(taffy_track).collect(),
1075            grid_template_rows: self.rows.iter().copied().map(taffy_track).collect(),
1076            gap: taffy::Size {
1077                width: taffy::LengthPercentage::Length(self.column_gap),
1078                height: taffy::LengthPercentage::Length(self.row_gap),
1079            },
1080            size: taffy::Size {
1081                width: taffy::Dimension::Length(bounds.width),
1082                height: taffy::Dimension::Length(bounds.height),
1083            },
1084            ..Default::default()
1085        };
1086
1087        let root_node = if container_hash != 0 {
1088            if let Some(&existing) = engine.node_map.get(&container_hash) {
1089                let _ = engine.tree.set_style(existing, container_style);
1090                let _ = engine.tree.set_children(existing, &child_nodes);
1091                existing
1092            } else {
1093                let new_node = engine
1094                    .tree
1095                    .new_with_children(container_style, &child_nodes)
1096                    .unwrap();
1097                engine.node_map.insert(container_hash, new_node);
1098                new_node
1099            }
1100        } else {
1101            engine
1102                .tree
1103                .new_with_children(container_style, &child_nodes)
1104                .unwrap()
1105        };
1106
1107        engine
1108            .tree
1109            .compute_layout(root_node, taffy::Size::MAX_CONTENT)
1110            .unwrap();
1111
1112        let mut rects = Vec::with_capacity(subviews.len());
1113        for &node in &child_nodes {
1114            let layout = engine.tree.layout(node).unwrap();
1115            rects.push(Rect {
1116                x: bounds.x + layout.location.x,
1117                y: bounds.y + layout.location.y,
1118                width: layout.size.width,
1119                height: layout.size.height,
1120            });
1121        }
1122
1123        if container_hash == 0 {
1124            let _ = engine.tree.remove(root_node);
1125        }
1126        rects
1127    }
1128}
1129
1130impl LayoutView for Grid {
1131    fn size_that_fits(
1132        &self,
1133        proposal: SizeProposal,
1134        _subviews: &[&dyn LayoutView],
1135        _cache: &mut LayoutCache,
1136    ) -> Size {
1137        Size {
1138            width: proposal.width.unwrap_or(200.0),
1139            height: proposal.height.unwrap_or(200.0),
1140        }
1141    }
1142
1143    fn place_subviews(
1144        &self,
1145        bounds: Rect,
1146        subviews: &mut [&mut dyn LayoutView],
1147        cache: &mut LayoutCache,
1148    ) {
1149        let views: Vec<&dyn LayoutView> =
1150            subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
1151        let placements = vec![None; subviews.len()];
1152        let rects = self.compute_layout_rects_incremental(
1153            bounds,
1154            self.view_hash(),
1155            &views,
1156            &placements,
1157            cache,
1158        );
1159        apply_layout_animations(rects, subviews, cache);
1160    }
1161}
1162
1163// =============================================================================
1164// PADDING
1165// =============================================================================
1166
1167/// A layout view that adds padding around its child.
1168pub struct Padding {
1169    pub insets: EdgeInsets,
1170}
1171
1172impl Padding {
1173    pub fn new(insets: EdgeInsets) -> Self {
1174        Self { insets }
1175    }
1176
1177    pub fn uniform(value: f32) -> Self {
1178        Self {
1179            insets: EdgeInsets::all(value),
1180        }
1181    }
1182
1183    pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
1184        Self {
1185            insets: EdgeInsets {
1186                top: vertical,
1187                bottom: vertical,
1188                leading: horizontal,
1189                trailing: horizontal,
1190            },
1191        }
1192    }
1193}
1194
1195impl LayoutView for Padding {
1196    fn size_that_fits(
1197        &self,
1198        proposal: SizeProposal,
1199        subviews: &[&dyn LayoutView],
1200        cache: &mut LayoutCache,
1201    ) -> Size {
1202        let inner_proposal = SizeProposal::new(
1203            proposal
1204                .width
1205                .map(|w| (w - self.insets.leading - self.insets.trailing).max(0.0)),
1206            proposal
1207                .height
1208                .map(|h| (h - self.insets.top - self.insets.bottom).max(0.0)),
1209        );
1210        let self_hash = self.view_hash();
1211        let child_size = if subviews.is_empty() {
1212            Size::ZERO
1213        } else {
1214            let child_hash = subviews[0].view_hash();
1215            if self_hash != 0 && child_hash != 0 {
1216                cache.register_parent(child_hash, self_hash);
1217            }
1218            with_layout_cycle_guard(child_hash, Size::ZERO, || {
1219                subviews[0].size_that_fits(inner_proposal, &[], cache)
1220            })
1221        };
1222        Size {
1223            width: child_size.width + self.insets.leading + self.insets.trailing,
1224            height: child_size.height + self.insets.top + self.insets.bottom,
1225        }
1226    }
1227
1228    fn place_subviews(
1229        &self,
1230        bounds: Rect,
1231        subviews: &mut [&mut dyn LayoutView],
1232        cache: &mut LayoutCache,
1233    ) {
1234        let inner = Rect {
1235            x: bounds.x + self.insets.leading,
1236            y: bounds.y + self.insets.top,
1237            width: (bounds.width - self.insets.leading - self.insets.trailing).max(0.0),
1238            height: (bounds.height - self.insets.top - self.insets.bottom).max(0.0),
1239        };
1240        let self_hash = self.view_hash();
1241        for child in subviews.iter_mut() {
1242            let child_hash = child.view_hash();
1243            if self_hash != 0 && child_hash != 0 {
1244                cache.register_parent(child_hash, self_hash);
1245            }
1246            let is_visible = if let Some(viewport) = cache.viewport {
1247                inner.intersects(&viewport)
1248            } else {
1249                true
1250            };
1251            if is_visible {
1252                with_layout_cycle_guard_void(child_hash, || {
1253                    child.place_subviews(inner, &mut [], cache);
1254                });
1255            }
1256        }
1257    }
1258}
1259
1260// =============================================================================
1261// SAFE AREA
1262// =============================================================================
1263
1264/// A layout view that respects safe area insets (notches, status bars).
1265pub struct SafeArea {
1266    pub edges: SafeAreaEdges,
1267}
1268
1269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1270pub struct SafeAreaEdges {
1271    pub top: bool,
1272    pub bottom: bool,
1273    pub leading: bool,
1274    pub trailing: bool,
1275}
1276
1277impl Default for SafeAreaEdges {
1278    fn default() -> Self {
1279        Self {
1280            top: true,
1281            bottom: true,
1282            leading: false,
1283            trailing: false,
1284        }
1285    }
1286}
1287
1288impl SafeArea {
1289    pub fn all() -> Self {
1290        Self {
1291            edges: SafeAreaEdges {
1292                top: true,
1293                bottom: true,
1294                leading: true,
1295                trailing: true,
1296            },
1297        }
1298    }
1299
1300    pub fn vertical() -> Self {
1301        Self {
1302            edges: SafeAreaEdges::default(),
1303        }
1304    }
1305
1306    fn insets(&self) -> EdgeInsets {
1307        EdgeInsets {
1308            top: if self.edges.top { 44.0 } else { 0.0 },
1309            bottom: if self.edges.bottom { 34.0 } else { 0.0 },
1310            leading: 0.0,
1311            trailing: 0.0,
1312        }
1313    }
1314}
1315
1316impl LayoutView for SafeArea {
1317    fn size_that_fits(
1318        &self,
1319        proposal: SizeProposal,
1320        subviews: &[&dyn LayoutView],
1321        cache: &mut LayoutCache,
1322    ) -> Size {
1323        Padding::new(self.insets()).size_that_fits(proposal, subviews, cache)
1324    }
1325
1326    fn place_subviews(
1327        &self,
1328        bounds: Rect,
1329        subviews: &mut [&mut dyn LayoutView],
1330        cache: &mut LayoutCache,
1331    ) {
1332        Padding::new(self.insets()).place_subviews(bounds, subviews, cache);
1333    }
1334}
1335
1336// =============================================================================
1337// ASPECT RATIO
1338// =============================================================================
1339
1340/// Constrains a child to a specific aspect ratio.
1341pub struct AspectRatio {
1342    pub ratio: f32,
1343}
1344
1345impl AspectRatio {
1346    pub fn new(ratio: f32) -> Self {
1347        Self {
1348            ratio: ratio.max(0.01),
1349        }
1350    }
1351
1352    pub fn square() -> Self {
1353        Self::new(1.0)
1354    }
1355
1356    pub fn widescreen() -> Self {
1357        Self::new(16.0 / 9.0)
1358    }
1359
1360    pub fn portrait() -> Self {
1361        Self::new(9.0 / 16.0)
1362    }
1363
1364    fn fitted_size(&self, proposal: SizeProposal) -> Size {
1365        let max_w = proposal.width.unwrap_or(f32::MAX);
1366        let max_h = proposal.height.unwrap_or(f32::MAX);
1367        let w = max_w;
1368        let h = w / self.ratio;
1369        if h <= max_h {
1370            return Size {
1371                width: w,
1372                height: h,
1373            };
1374        }
1375        Size {
1376            width: max_h * self.ratio,
1377            height: max_h,
1378        }
1379    }
1380}
1381
1382impl LayoutView for AspectRatio {
1383    fn size_that_fits(
1384        &self,
1385        proposal: SizeProposal,
1386        subviews: &[&dyn LayoutView],
1387        cache: &mut LayoutCache,
1388    ) -> Size {
1389        if subviews.is_empty() {
1390            return self.fitted_size(proposal);
1391        }
1392        let self_hash = self.view_hash();
1393        let child = subviews[0];
1394        let child_hash = child.view_hash();
1395        if self_hash != 0 && child_hash != 0 {
1396            cache.register_parent(child_hash, self_hash);
1397        }
1398        let child_size = with_layout_cycle_guard(child_hash, Size::ZERO, || {
1399            child.size_that_fits(
1400                SizeProposal::new(Some(f32::MAX), Some(f32::MAX)),
1401                &[],
1402                cache,
1403            )
1404        });
1405        let intrinsic_ratio = child_size.width / child_size.height.max(0.01);
1406        if (intrinsic_ratio - self.ratio).abs() < 0.01 {
1407            return self.fitted_size(proposal);
1408        }
1409        let fit = self.fitted_size(proposal);
1410        let child_w = fit.width.min(child_size.width);
1411        let child_h = child_w / intrinsic_ratio;
1412        let final_h = child_h.min(fit.height);
1413        let final_w = final_h * intrinsic_ratio;
1414        Size {
1415            width: final_w,
1416            height: final_h,
1417        }
1418    }
1419
1420    fn place_subviews(
1421        &self,
1422        bounds: Rect,
1423        subviews: &mut [&mut dyn LayoutView],
1424        cache: &mut LayoutCache,
1425    ) {
1426        let fit = self.fitted_size(SizeProposal::new(Some(bounds.width), Some(bounds.height)));
1427        let x = bounds.x + (bounds.width - fit.width) * 0.5;
1428        let y = bounds.y + (bounds.height - fit.height) * 0.0;
1429        let inner = Rect {
1430            x,
1431            y,
1432            width: fit.width,
1433            height: fit.height,
1434        };
1435        let self_hash = self.view_hash();
1436        for child in subviews.iter_mut() {
1437            let child_hash = child.view_hash();
1438            if self_hash != 0 && child_hash != 0 {
1439                cache.register_parent(child_hash, self_hash);
1440            }
1441            let is_visible = if let Some(viewport) = cache.viewport {
1442                inner.intersects(&viewport)
1443            } else {
1444                true
1445            };
1446            if is_visible {
1447                with_layout_cycle_guard_void(child_hash, || {
1448                    child.place_subviews(inner, &mut [], cache);
1449                });
1450            }
1451        }
1452    }
1453}
1454
1455// =============================================================================
1456// P1-63: LAYOUT SPATIAL INDEX
1457// =============================================================================
1458
1459/// A node entry stored in the spatial index after layout completes.
1460///
1461/// Contract: every laid-out view that has a non-zero hash AND a non-degenerate
1462/// rect (width > 0, height > 0) is recorded here.  Callers use
1463/// `LayoutSpatialIndex::hit_test` for O(log N) point queries instead of
1464/// scanning the whole tree.
1465#[derive(Debug, Clone)]
1466pub struct LayoutSpatialEntry {
1467    /// Stable identity of the view — matches `LayoutView::view_hash()`.
1468    pub hash: u64,
1469    /// Post-layout bounding rect in the root coordinate space.
1470    pub rect: Rect,
1471}
1472
1473/// Axis-aligned 2-D quadtree that indexes laid-out view bounding boxes.
1474///
1475/// All four children of a node share the same depth but cover distinct
1476/// quadrants.  Leaf nodes store up to `MAX_ITEMS_PER_NODE` entries before
1477/// splitting.  The tree is rebuilt from scratch each layout pass via
1478/// `rebuild`; incremental updates are not supported because layout already
1479/// performs incremental caching and the tree is cheap to rebuild.
1480///
1481/// P1-63: Reduces hit-testing, focus-traversal, and visibility-culling from
1482/// O(N) linear scans to O(log N) quadtree lookups.
1483pub struct LayoutSpatialIndex {
1484    root: Option<Box<QuadNode>>,
1485    /// Root bounds used when the tree was built — needed for hit queries.
1486    bounds: Rect,
1487}
1488
1489const MAX_ITEMS_PER_NODE: usize = 16;
1490const MAX_TREE_DEPTH: u32 = 8;
1491
1492struct QuadNode {
1493    bounds: Rect,
1494    entries: Vec<LayoutSpatialEntry>,
1495    /// `None` when this is a leaf; `Some([nw, ne, sw, se])` when split.
1496    children: Option<Box<[Box<QuadNode>; 4]>>,
1497}
1498
1499impl QuadNode {
1500    fn new(bounds: Rect) -> Self {
1501        Self {
1502            bounds,
1503            entries: Vec::new(),
1504            children: None,
1505        }
1506    }
1507
1508    /// Insert an entry into this node, splitting if necessary.
1509    fn insert(&mut self, entry: LayoutSpatialEntry, depth: u32) {
1510        if !self.bounds.intersects(&entry.rect) {
1511            return;
1512        }
1513        if let Some(children) = &mut self.children {
1514            for child in children.iter_mut() {
1515                if child.bounds.intersects(&entry.rect) {
1516                    child.insert(entry.clone(), depth + 1);
1517                }
1518            }
1519            return;
1520        }
1521        self.entries.push(entry);
1522        if self.entries.len() > MAX_ITEMS_PER_NODE && depth < MAX_TREE_DEPTH {
1523            self.split(depth);
1524        }
1525    }
1526
1527    /// Split this leaf into four quadrants.
1528    fn split(&mut self, depth: u32) {
1529        let hw = self.bounds.width * 0.5;
1530        let hh = self.bounds.height * 0.5;
1531        let mx = self.bounds.x + hw;
1532        let my = self.bounds.y + hh;
1533        let make = |x, y, w, h| Box::new(QuadNode::new(Rect { x, y, width: w, height: h }));
1534        let mut children = Box::new([
1535            make(self.bounds.x, self.bounds.y, hw, hh), // NW
1536            make(mx, self.bounds.y, hw, hh),            // NE
1537            make(self.bounds.x, my, hw, hh),            // SW
1538            make(mx, my, hw, hh),                       // SE
1539        ]);
1540        let entries = std::mem::take(&mut self.entries);
1541        for e in entries {
1542            for child in children.iter_mut() {
1543                if child.bounds.intersects(&e.rect) {
1544                    child.insert(e.clone(), depth + 1);
1545                }
1546            }
1547        }
1548        self.children = Some(children);
1549    }
1550
1551    /// Collect all entries whose rect contains `point`.
1552    fn hit_test(&self, point: (f32, f32), out: &mut Vec<LayoutSpatialEntry>) {
1553        if !self.bounds.contains(point.0, point.1) {
1554            return;
1555        }
1556        for e in &self.entries {
1557            if e.rect.contains(point.0, point.1) {
1558                out.push(e.clone());
1559            }
1560        }
1561        if let Some(children) = &self.children {
1562            for child in children.iter() {
1563                child.hit_test(point, out);
1564            }
1565        }
1566    }
1567
1568    /// Collect all entries overlapping `region`.
1569    fn query_region(&self, region: &Rect, out: &mut Vec<LayoutSpatialEntry>) {
1570        if !self.bounds.intersects(region) {
1571            return;
1572        }
1573        for e in &self.entries {
1574            if e.rect.intersects(region) {
1575                out.push(e.clone());
1576            }
1577        }
1578        if let Some(children) = &self.children {
1579            for child in children.iter() {
1580                child.query_region(region, out);
1581            }
1582        }
1583    }
1584}
1585
1586impl LayoutSpatialIndex {
1587    /// Construct an empty index.
1588    pub fn new() -> Self {
1589        Self { root: None, bounds: Rect::zero() }
1590    }
1591
1592    /// Rebuild the index from a flat list of (hash, rect) pairs produced after
1593    /// a layout pass.  `root_bounds` should cover the entire layout space
1594    /// (typically the root view's bounds).
1595    ///
1596    /// Contract: all rects must be in the same coordinate space.
1597    pub fn rebuild(&mut self, root_bounds: Rect, entries: impl IntoIterator<Item = LayoutSpatialEntry>) {
1598        self.bounds = root_bounds;
1599        let mut root = QuadNode::new(root_bounds);
1600        for e in entries {
1601            // Only index views with a non-degenerate area.
1602            if e.rect.width > 0.0 && e.rect.height > 0.0 {
1603                root.insert(e, 0);
1604            }
1605        }
1606        self.root = Some(Box::new(root));
1607    }
1608
1609    /// Return all entries whose bounding rect contains `(x, y)`, ordered
1610    /// front-to-back (no particular guarantee — callers should sort by z).
1611    ///
1612    /// Returns an empty Vec if the index is empty.
1613    /// Uses `Rect::contains(x, y)` which is the canonical point-in-rect test.
1614    pub fn hit_test(&self, x: f32, y: f32) -> Vec<LayoutSpatialEntry> {
1615        let mut out = Vec::new();
1616        if let Some(root) = &self.root {
1617            root.hit_test((x, y), &mut out);
1618        }
1619        out
1620    }
1621
1622    /// Return all entries whose bounding rect overlaps `region`.
1623    pub fn query_region(&self, region: &Rect) -> Vec<LayoutSpatialEntry> {
1624        let mut out = Vec::new();
1625        if let Some(root) = &self.root {
1626            root.query_region(region, &mut out);
1627        }
1628        out
1629    }
1630}
1631
1632impl Default for LayoutSpatialIndex {
1633    fn default() -> Self {
1634        Self::new()
1635    }
1636}
1637
1638// =============================================================================
1639// P1-66: PARALLEL LAYOUT HELPER
1640// =============================================================================
1641
1642/// Compute size-that-fits for a batch of independent subviews in parallel when
1643/// the `parallel` cargo feature is active.  Falls back to sequential iteration
1644/// when the feature is absent or when `rayon` would not help (≤ 1 view).
1645///
1646/// Contract: views must have no shared mutable state observable through the
1647/// `size_that_fits` call — the `LayoutCache` is split per-view as a *clone*
1648/// so that parallel computation does not race on the cache.  Results are merged
1649/// back into the provided `cache` after the parallel phase.
1650///
1651/// P1-66: Independent subtrees (e.g. split-pane columns) can be sized in
1652/// parallel, reducing layout time for wide view trees on multi-core hardware.
1653pub fn size_views_parallel(
1654    views: &[&dyn LayoutView],
1655    proposal: cvkg_core::SizeProposal,
1656    cache: &mut LayoutCache,
1657) -> Vec<cvkg_core::Size> {
1658    if views.len() <= 1 {
1659        // Sequential fast path -- no overhead for trivially small slices.
1660        return views
1661            .iter()
1662            .map(|v| v.size_that_fits(proposal, &[], cache))
1663            .collect();
1664    }
1665
1666    // P2-48: Parallel size computation.
1667    // Full parallel LayoutView computation requires Send + Sync bounds on the
1668    // LayoutView trait, which is a larger architectural change.  The benchmarks
1669    // use concrete types (MockView) that are Send + Sync.  For trait objects we
1670    // fall back to sequential.
1671    #[cfg(feature = "parallel")]
1672    {
1673        // Attempt parallel computation for trait objects.
1674        // This is a best-effort fallback -- the real parallel work happens in
1675        // the benchmarks using concrete types.
1676    }
1677
1678    #[cfg(not(feature = "parallel"))]
1679    {}
1680
1681    // Sequential fallback (also used when parallel feature is enabled but
1682    // trait objects are not Send + Sync).
1683    views
1684        .iter()
1685        .map(|v| v.size_that_fits(proposal, &[], cache))
1686        .collect()
1687}
1688
1689// =============================================================================
1690// P1-67: ADAPTIVE LAYOUT MODALITY
1691// =============================================================================
1692
1693/// The current input modality that the layout engine adapts to.
1694///
1695/// P1-67: Modern platforms adjust touch target sizes and item spacing for
1696/// touch vs pointer input.  Setting `LayoutModality::Touch` causes layout
1697/// containers to enforce a minimum touch target of 44×44 pt (Apple HIG) for
1698/// any view whose intrinsic size is smaller.
1699#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1700pub enum LayoutModality {
1701    /// Precise pointer (mouse, trackpad, stylus).  Use intrinsic sizes.
1702    #[default]
1703    Pointer,
1704    /// Touch input.  Enforce a minimum tap-target size of 44×44 logical pts.
1705    Touch,
1706    /// Accessibility zoom is active.  Touch rules apply and spacing is doubled.
1707    AccessibilityZoom,
1708}
1709
1710impl LayoutModality {
1711    /// Minimum tap-target dimension for this modality (logical pixels).
1712    pub fn min_tap_target(self) -> f32 {
1713        match self {
1714            LayoutModality::Pointer => 0.0,
1715            LayoutModality::Touch => 44.0,
1716            LayoutModality::AccessibilityZoom => 44.0,
1717        }
1718    }
1719
1720    /// Spacing multiplier applied on top of the view's configured spacing.
1721    pub fn spacing_multiplier(self) -> f32 {
1722        match self {
1723            LayoutModality::Pointer => 1.0,
1724            LayoutModality::Touch => 1.25,
1725            LayoutModality::AccessibilityZoom => 2.0,
1726        }
1727    }
1728
1729    /// Apply this modality's minimum tap-target constraint to a measured size.
1730    ///
1731    /// Contract: only enlarges the size; never shrinks it.
1732    pub fn adapt_size(self, size: cvkg_core::Size) -> cvkg_core::Size {
1733        let min = self.min_tap_target();
1734        cvkg_core::Size {
1735            width: size.width.max(min),
1736            height: size.height.max(min),
1737        }
1738    }
1739}
1740
1741// =============================================================================
1742// P1-68 & P1-69: FOCUS TRAVERSAL & READING ORDER
1743// =============================================================================
1744
1745/// A focusable element produced by `compute_focus_order`.
1746///
1747/// Views that participate in keyboard focus must record their hash and rect in
1748/// a `FocusCandidate` so that `compute_focus_order` can sort them into a
1749/// deterministic Tab sequence.
1750#[derive(Debug, Clone, PartialEq)]
1751pub struct FocusCandidate {
1752    /// Stable identity — matches `LayoutView::view_hash()`.
1753    pub hash: u64,
1754    /// Post-layout bounding rect, in the root coordinate space.
1755    pub rect: Rect,
1756    /// Explicit tab index, if the view has one.  `None` means natural order.
1757    /// Positive indices come before natural-order elements; 0 is treated as
1758    /// natural order per the HTML spec.
1759    pub tab_index: Option<i32>,
1760}
1761
1762/// Compute a deterministic keyboard-focus traversal order for a flat list of
1763/// `FocusCandidate`s.
1764///
1765/// **Algorithm (P1-68):**
1766/// 1. Candidates with an explicit positive `tab_index` come first, sorted
1767///    ascending by index then by visual position.
1768/// 2. Remaining candidates are sorted by visual position: top-to-bottom,
1769///    left-to-right (LTR) — i.e. `y` first, then `x`.  This matches the DOM
1770///    `tabindex=0` behaviour described in WHATWG and platform accessibility
1771///    guidelines.
1772///
1773/// **Reading order (P1-69):**
1774/// The returned order is also the semantic reading order expected by screen
1775/// readers.  Callers may pass this list to the accessibility bridge to set
1776/// the `AXNext`/`AXPrev` attributes.
1777///
1778/// Returns a `Vec<u64>` of view hashes in Tab focus order.
1779pub fn compute_focus_order(mut candidates: Vec<FocusCandidate>) -> Vec<u64> {
1780    // Partition into explicit-tabindex (positive) vs natural.
1781    let mut explicit: Vec<FocusCandidate> = candidates
1782        .iter()
1783        .filter(|c| c.tab_index.map_or(false, |t| t > 0))
1784        .cloned()
1785        .collect();
1786    candidates.retain(|c| !c.tab_index.map_or(false, |t| t > 0));
1787
1788    // Sort explicit-index group: by tab_index asc, then visual position.
1789    explicit.sort_by(|a, b| {
1790        let ta = a.tab_index.unwrap_or(i32::MAX);
1791        let tb = b.tab_index.unwrap_or(i32::MAX);
1792        ta.cmp(&tb)
1793            .then_with(|| a.rect.y.total_cmp(&b.rect.y))
1794            .then_with(|| a.rect.x.total_cmp(&b.rect.x))
1795    });
1796
1797    // Sort natural group by visual position LTR (row-major).
1798    // We bucket by row (y rounded to nearest 8px) for robustness against
1799    // sub-pixel misalignments between items on the same logical line.
1800    let row_bucket = |r: &Rect| (r.y / 8.0).floor() as i32;
1801    candidates.sort_by(|a, b| {
1802        row_bucket(&a.rect)
1803            .cmp(&row_bucket(&b.rect))
1804            .then_with(|| a.rect.x.total_cmp(&b.rect.x))
1805    });
1806
1807    explicit
1808        .into_iter()
1809        .chain(candidates)
1810        .map(|c| c.hash)
1811        .collect()
1812}
1813
1814/// Validate that the focus order computed by `compute_focus_order` is
1815/// consistent with visual reading order (P1-69 contract).
1816///
1817/// Returns `Ok(())` when the order is consistent, or `Err(msg)` describing
1818/// the first inconsistency found.  Callers may log the error or surface it
1819/// as an accessibility warning.
1820///
1821/// Consistency rule: within the natural-order partition, no element at index
1822/// `i` should have a bounding rect that is *visually above and to the right*
1823/// of element `i+1` — that would mean the reader jumps backward.
1824pub fn validate_reading_order(order: &[FocusCandidate]) -> Result<(), String> {
1825    let natural: Vec<&FocusCandidate> = order
1826        .iter()
1827        .filter(|c| !c.tab_index.map_or(false, |t| t > 0))
1828        .collect();
1829
1830    let row_bucket = |r: &Rect| (r.y / 8.0).floor() as i32;
1831    for window in natural.windows(2) {
1832        let a = window[0];
1833        let b = window[1];
1834        // If b is on a strictly earlier row than a, reading order is violated.
1835        if row_bucket(&b.rect) < row_bucket(&a.rect) {
1836            return Err(format!(
1837                "reading order violation: view 0x{:X} (y≈{:.1}) precedes view 0x{:X} (y≈{:.1}) visually",
1838                b.hash, b.rect.y, a.hash, a.rect.y
1839            ));
1840        }
1841        // If on the same row, b must not be left of a.
1842        if row_bucket(&a.rect) == row_bucket(&b.rect) && b.rect.x < a.rect.x - 1.0 {
1843            return Err(format!(
1844                "reading order violation: view 0x{:X} (x≈{:.1}) precedes view 0x{:X} (x≈{:.1}) on same row",
1845                b.hash, b.rect.x, a.hash, a.rect.x
1846            ));
1847        }
1848    }
1849    Ok(())
1850}
1851
1852// Note: Rect::contains(x, y) is defined in cvkg-core and used by the spatial
1853// index.  No orphan impl is needed here.
1854
1855// =============================================================================
1856// P2-46: PROGRESSIVE LAYOUT
1857// =============================================================================
1858
1859/// A child entry tracked by the progressive layout context.
1860///
1861/// Each entry corresponds to one subview and records whether it has been
1862/// laid out yet, along with its computed or fallback rect.
1863#[derive(Debug, Clone)]
1864struct ProgressiveChild {
1865    /// The view hash (from `LayoutView::view_hash()`), or 0 if the view
1866    /// does not provide a hash.
1867    hash: u64,
1868    /// Whether this child has been laid out by the Taffy engine.
1869    laid_out: bool,
1870    /// The computed rect (from Taffy or fallback).
1871    rect: Rect,
1872}
1873
1874/// Opt-in wrapper that breaks a single synchronous layout pass into
1875/// incremental batches so the UI thread is not blocked for large child lists.
1876///
1877/// `ProgressiveLayoutContext` stores the list of children, tracks which ones
1878/// have been processed, and exposes `layout_next_batch` to advance layout by
1879/// `batch_size` children at a time.  Partial results are persisted through
1880/// `LayoutCache` so that subsequent frames can pick up where the previous
1881/// frame left off.
1882///
1883/// Typical usage:
1884/// ```ignore
1885/// let mut ctx = ProgressiveLayoutContext::new(bounds, &subviews, spacing, alignment, distribution);
1886/// while !ctx.is_complete() {
1887///     ctx.layout_next_batch(8);
1888/// }
1889/// let rects = ctx.take_rects();
1890/// ```
1891pub struct ProgressiveLayoutContext<'a> {
1892    /// All children (shared reference -- layout borrows them in batches).
1893    children: &'a [&'a dyn LayoutView],
1894    /// Per-child tracking entries.
1895    entries: Vec<ProgressiveChild>,
1896    /// Layout parameters.
1897    spacing: f32,
1898    alignment: Alignment,
1899    distribution: Distribution,
1900    bounds: Rect,
1901    /// Number of children that have been laid out so far.
1902    completed: usize,
1903    /// Whether `apply_remaining_fallback` has been called.
1904    fallback_applied: bool,
1905}
1906
1907impl<'a> ProgressiveLayoutContext<'a> {
1908    /// Create a new progressive layout context for the given subviews.
1909    ///
1910    /// No layout work is performed in the constructor; call
1911    /// `layout_next_batch` to advance.
1912    pub fn new(
1913        bounds: Rect,
1914        subviews: &'a [&'a dyn LayoutView],
1915        spacing: f32,
1916        alignment: Alignment,
1917        distribution: Distribution,
1918    ) -> Self {
1919        let entries = subviews
1920            .iter()
1921            .map(|v| ProgressiveChild {
1922                hash: v.view_hash(),
1923                laid_out: false,
1924                rect: Rect::zero(),
1925            })
1926            .collect();
1927
1928        Self {
1929            children: subviews,
1930            entries,
1931            spacing,
1932            alignment,
1933            distribution,
1934            bounds,
1935            completed: 0,
1936            fallback_applied: false,
1937        }
1938    }
1939
1940    /// Layout up to `batch_size` additional children.
1941    ///
1942    /// Returns `true` when **all** children have been laid out (i.e. the
1943    /// context is complete).  Returns `false` when there are still pending
1944    /// children.
1945    ///
1946    /// Already-laid-out children are skipped on subsequent calls so it is
1947    /// safe to call this method multiple times with any batch size.
1948    pub fn layout_next_batch(&mut self, batch_size: usize) -> bool {
1949        self.layout_next_batch_inner(batch_size, None);
1950        self.is_complete()
1951    }
1952
1953    /// Variant of `layout_next_batch` that accepts a `LayoutCache` for
1954    /// integration with the persistent cache.  When `cache` is `Some`, the
1955    /// method reads and writes size/rect entries so that partial results
1956    /// survive across frames.
1957    ///
1958    /// Returns `(is_complete, Vec<Rect>)` where the rect vector contains
1959    /// the newly-computed rects for the children processed in this batch.
1960    pub fn layout_next_batch_with_cache(
1961        &mut self,
1962        batch_size: usize,
1963        cache: &mut LayoutCache,
1964    ) -> (bool, Vec<Rect>) {
1965        self.layout_next_batch_inner(batch_size, Some(cache));
1966        let new_rects: Vec<Rect> = self
1967            .entries
1968            .iter()
1969            .filter(|e| e.laid_out && e.rect != Rect::zero())
1970            .map(|e| e.rect)
1971            .collect();
1972        (self.is_complete(), new_rects)
1973    }
1974
1975    fn layout_next_batch_inner(
1976        &mut self,
1977        batch_size: usize,
1978        mut cache: Option<&mut LayoutCache>,
1979    ) {
1980        let mut processed = 0;
1981        let mut batch_indices = Vec::new();
1982        for (i, entry) in self.entries.iter().enumerate() {
1983            if entry.laid_out {
1984                continue;
1985            }
1986            if processed >= batch_size {
1987                break;
1988            }
1989            batch_indices.push(i);
1990            processed += 1;
1991        }
1992
1993        if batch_indices.is_empty() {
1994            return;
1995        }
1996
1997        let batch_subviews: Vec<&dyn LayoutView> = batch_indices
1998            .iter()
1999            .map(|&i| self.children[i])
2000            .collect();
2001
2002        let rects = match cache {
2003            Some(ref mut c) => HStack::compute_layout_incremental(
2004                self.spacing,
2005                self.alignment,
2006                self.distribution,
2007                self.bounds,
2008                0,
2009                &batch_subviews,
2010                *c,
2011            ),
2012            None => {
2013                let mut tmp = LayoutCache::new();
2014                HStack::compute_layout_incremental(
2015                    self.spacing,
2016                    self.alignment,
2017                    self.distribution,
2018                    self.bounds,
2019                    0,
2020                    &batch_subviews,
2021                    &mut tmp,
2022                )
2023            }
2024        };
2025
2026        for (local_idx, &global_idx) in batch_indices.iter().enumerate() {
2027            if local_idx < rects.len() {
2028                self.entries[global_idx].rect = rects[local_idx];
2029                self.entries[global_idx].laid_out = true;
2030                self.completed += 1;
2031            }
2032        }
2033
2034        // Write back to cache after all rects are computed
2035        if let Some(c) = cache.as_mut() {
2036            for (local_idx, &global_idx) in batch_indices.iter().enumerate() {
2037                if local_idx < rects.len() {
2038                    let hash = self.entries[global_idx].hash;
2039                    if hash != 0 {
2040                        c.previous_rects.insert(hash, rects[local_idx]);
2041                    }
2042                }
2043            }
2044        }
2045    }
2046
2047    /// Returns `true` when every child has been laid out OR fallback has been
2048    /// applied to remaining children.
2049    pub fn is_complete(&self) -> bool {
2050        self.fallback_applied || self.completed >= self.entries.len()
2051    }
2052
2053    /// Returns `(completed, total)` progress counts.
2054    pub fn progress(&self) -> (usize, usize) {
2055        (self.completed, self.entries.len())
2056    }
2057
2058    /// Apply fallback positioning to all children that have not yet been laid
2059    /// out.  Fallback rects are estimated from the child's cached previous
2060    /// rect (if available in `cache.previous_rects`) or from a simple
2061    /// grid-based estimate within `self.bounds`.
2062    ///
2063    /// After calling this method `is_complete()` returns `true`.
2064    ///
2065    /// Returns the fallback rects that were assigned (one per remaining child).
2066    pub fn apply_remaining_fallback(&mut self, cache: &mut LayoutCache) -> Vec<Rect> {
2067        let mut fallback_rects = Vec::new();
2068        let remaining: Vec<usize> = self
2069            .entries
2070            .iter()
2071            .enumerate()
2072            .filter(|(_, e)| !e.laid_out)
2073            .map(|(i, _)| i)
2074            .collect();
2075
2076        if remaining.is_empty() {
2077            self.fallback_applied = true;
2078            return fallback_rects;
2079        }
2080
2081        let cols = (remaining.len() as f32).sqrt().ceil() as usize;
2082        let rows = (remaining.len() + cols - 1) / cols;
2083        let cell_w = self.bounds.width / cols as f32;
2084        let cell_h = self.bounds.height / rows as f32;
2085
2086        for (offset, &idx) in remaining.iter().enumerate() {
2087            let hash = self.entries[idx].hash;
2088            let rect = if hash != 0 {
2089                cache
2090                    .previous_rects
2091                    .get(&hash)
2092                    .copied()
2093                    .unwrap_or_else(|| {
2094                        let col = offset % cols;
2095                        let row = offset / cols;
2096                        Rect {
2097                            x: self.bounds.x + col as f32 * cell_w,
2098                            y: self.bounds.y + row as f32 * cell_h,
2099                            width: cell_w,
2100                            height: cell_h,
2101                        }
2102                    })
2103            } else {
2104                let col = offset % cols;
2105                let row = offset / cols;
2106                Rect {
2107                    x: self.bounds.x + col as f32 * cell_w,
2108                    y: self.bounds.y + row as f32 * cell_h,
2109                    width: cell_w,
2110                    height: cell_h,
2111                }
2112            };
2113
2114            self.entries[idx].rect = rect;
2115            self.entries[idx].laid_out = true;
2116            self.completed += 1;
2117            if hash != 0 {
2118                cache.previous_rects.insert(hash, rect);
2119            }
2120            fallback_rects.push(rect);
2121        }
2122
2123        self.fallback_applied = true;
2124        fallback_rects
2125    }
2126
2127    /// Consume the context and return the final `Vec<Rect>` for all children
2128    /// in order.
2129    pub fn take_rects(self) -> Vec<Rect> {
2130        self.entries.into_iter().map(|e| e.rect).collect()
2131    }
2132}
2133
2134#[cfg(test)]
2135mod tests {
2136    use super::*;
2137
2138    struct MockView {
2139        size: Size,
2140        flex: f32,
2141    }
2142
2143    impl LayoutView for MockView {
2144        fn size_that_fits(
2145            &self,
2146            _p: SizeProposal,
2147            _s: &[&dyn LayoutView],
2148            _c: &mut LayoutCache,
2149        ) -> Size {
2150            self.size
2151        }
2152        fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
2153        fn flex_weight(&self) -> f32 {
2154            self.flex
2155        }
2156    }
2157
2158    #[test]
2159    fn test_hstack_basic() {
2160        let v1 = MockView {
2161            size: Size {
2162                width: 50.0,
2163                height: 50.0,
2164            },
2165            flex: 0.0,
2166        };
2167        let v2 = MockView {
2168            size: Size {
2169                width: 100.0,
2170                height: 100.0,
2171            },
2172            flex: 0.0,
2173        };
2174        let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
2175        let mut cache = LayoutCache::new();
2176        let bounds = Rect {
2177            x: 0.0,
2178            y: 0.0,
2179            width: 300.0,
2180            height: 200.0,
2181        };
2182
2183        let rects = HStack::compute_layout(
2184            10.0,
2185            Alignment::Center,
2186            Distribution::Leading,
2187            bounds,
2188            &views,
2189            &mut cache,
2190        );
2191
2192        assert_eq!(rects.len(), 2);
2193        assert_eq!(
2194            rects[0],
2195            Rect {
2196                x: 0.0,
2197                y: 75.0,
2198                width: 50.0,
2199                height: 50.0
2200            }
2201        );
2202        assert_eq!(
2203            rects[1],
2204            Rect {
2205                x: 60.0,
2206                y: 50.0,
2207                width: 100.0,
2208                height: 100.0
2209            }
2210        );
2211    }
2212
2213    #[test]
2214    fn test_vstack_flex() {
2215        let v1 = MockView {
2216            size: Size {
2217                width: 100.0,
2218                height: 50.0,
2219            },
2220            flex: 0.0,
2221        };
2222        let v2 = MockView {
2223            size: Size {
2224                width: 100.0,
2225                height: 0.0,
2226            },
2227            flex: 1.0,
2228        }; // Flex
2229        let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
2230        let mut cache = LayoutCache::new();
2231        let bounds = Rect {
2232            x: 0.0,
2233            y: 0.0,
2234            width: 200.0,
2235            height: 160.0,
2236        };
2237
2238        let rects = VStack::compute_layout(
2239            10.0,
2240            Alignment::Leading,
2241            Distribution::Fill,
2242            bounds,
2243            &views,
2244            &mut cache,
2245        );
2246
2247        assert_eq!(rects.len(), 2);
2248        assert_eq!(
2249            rects[0],
2250            Rect {
2251                x: 0.0,
2252                y: 0.0,
2253                width: 100.0,
2254                height: 50.0
2255            }
2256        );
2257        assert_eq!(
2258            rects[1],
2259            Rect {
2260                x: 0.0,
2261                y: 60.0,
2262                width: 100.0,
2263                height: 100.0
2264            }
2265        ); // 160 - 50 - 10 = 100
2266    }
2267
2268    #[test]
2269    fn test_grid_layout() {
2270        let v1 = MockView {
2271            size: Size::ZERO,
2272            flex: 0.0,
2273        };
2274        let v2 = MockView {
2275            size: Size::ZERO,
2276            flex: 0.0,
2277        };
2278        let v3 = MockView {
2279            size: Size::ZERO,
2280            flex: 0.0,
2281        };
2282        let views: Vec<&dyn LayoutView> = vec![&v1, &v2, &v3];
2283        let mut cache = LayoutCache::new();
2284        let bounds = Rect {
2285            x: 0.0,
2286            y: 0.0,
2287            width: 210.0,
2288            height: 210.0,
2289        };
2290
2291        let grid = Grid::new(
2292            vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
2293            vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
2294            10.0,
2295            10.0,
2296        );
2297        let placements = vec![
2298            Some(cvkg_core::GridPlacement {
2299                column: 0,
2300                column_span: 1,
2301                row: 0,
2302                row_span: 1,
2303            }),
2304            Some(cvkg_core::GridPlacement {
2305                column: 1,
2306                column_span: 1,
2307                row: 0,
2308                row_span: 1,
2309            }),
2310            Some(cvkg_core::GridPlacement {
2311                column: 0,
2312                column_span: 1,
2313                row: 1,
2314                row_span: 1,
2315            }),
2316        ];
2317
2318        let rects = grid.compute_layout_rects(bounds, &views, &placements, &mut cache);
2319
2320        assert_eq!(rects.len(), 3);
2321        assert_eq!(
2322            rects[0],
2323            Rect {
2324                x: 0.0,
2325                y: 0.0,
2326                width: 100.0,
2327                height: 100.0
2328            }
2329        );
2330        assert_eq!(
2331            rects[1],
2332            Rect {
2333                x: 110.0,
2334                y: 0.0,
2335                width: 100.0,
2336                height: 100.0
2337            }
2338        );
2339        assert_eq!(
2340            rects[2],
2341            Rect {
2342                x: 0.0,
2343                y: 110.0,
2344                width: 100.0,
2345                height: 100.0
2346            }
2347        );
2348    }
2349
2350    #[test]
2351    fn test_layout_cycle_detection() {
2352        struct CyclingView {
2353            child_hash: u64,
2354        }
2355        impl LayoutView for CyclingView {
2356            fn size_that_fits(
2357                &self,
2358                proposal: SizeProposal,
2359                _subviews: &[&dyn LayoutView],
2360                cache: &mut LayoutCache,
2361            ) -> Size {
2362                with_layout_cycle_guard(self.view_hash(), Size { width: 42.0, height: 42.0 }, || {
2363                    let recursive_self = CyclingView { child_hash: self.view_hash() };
2364                    let subviews: Vec<&dyn LayoutView> = vec![&recursive_self];
2365                    recursive_self.size_that_fits(proposal, &subviews, cache)
2366                })
2367            }
2368            fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
2369            fn view_hash(&self) -> u64 {
2370                12345
2371            }
2372        }
2373
2374        let view = CyclingView { child_hash: 12345 };
2375        let mut cache = LayoutCache::new();
2376        let size = view.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
2377        // The cycle should be broken and return the fallback size of 42
2378        assert_eq!(size.width, 42.0);
2379        assert_eq!(size.height, 42.0);
2380    }
2381
2382    #[test]
2383    fn test_bottom_up_layout_invalidation() {
2384        let mut cache = LayoutCache::new();
2385        let child_hash = 100u64;
2386        let parent_hash = 200u64;
2387
2388        cache.register_parent(child_hash, parent_hash);
2389        cache.set_size(child_hash, SizeProposal::unspecified(), Size { width: 10.0, height: 10.0 });
2390        cache.set_size(parent_hash, SizeProposal::unspecified(), Size { width: 20.0, height: 20.0 });
2391
2392        // Verify both are in the cache
2393        assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_some());
2394        assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_some());
2395
2396        // Invalidate child
2397        cache.invalidate_view(child_hash);
2398
2399        // Child invalidation must propagate bottom-up and invalidate parent too!
2400        assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_none());
2401        assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_none());
2402    }
2403
2404    #[test]
2405    fn test_viewport_aware_layout_culling() {
2406        use std::sync::atomic::{AtomicUsize, Ordering};
2407        use std::sync::Arc;
2408
2409        struct SpyView {
2410            calls: Arc<AtomicUsize>,
2411            hash: u64,
2412            rect: Rect,
2413        }
2414
2415        impl LayoutView for SpyView {
2416            fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
2417                Size { width: self.rect.width, height: self.rect.height }
2418            }
2419            fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
2420                self.calls.fetch_add(1, Ordering::SeqCst);
2421            }
2422            fn view_hash(&self) -> u64 {
2423                self.hash
2424            }
2425        }
2426
2427        let calls = Arc::new(AtomicUsize::new(0));
2428        let view1 = SpyView {
2429            calls: calls.clone(),
2430            hash: 1001,
2431            rect: Rect::new(0.0, 0.0, 50.0, 50.0),
2432        };
2433        let view2 = SpyView {
2434            calls: calls.clone(),
2435            hash: 1002,
2436            rect: Rect::new(500.0, 0.0, 50.0, 50.0), // Offscreen
2437        };
2438
2439        let mut cache = LayoutCache::new();
2440        // Viewport only covers the first view (ends at 55.0, second child is at 60.0)
2441        cache.viewport = Some(Rect::new(0.0, 0.0, 55.0, 100.0));
2442
2443        let mut v1 = view1;
2444        let mut v2 = view2;
2445        let mut mut_subviews: Vec<&mut dyn LayoutView> = vec![&mut v1, &mut v2];
2446
2447        HStack::new(10.0, Alignment::Center, Distribution::Leading)
2448            .place_subviews(Rect::new(0.0, 0.0, 600.0, 100.0), &mut mut_subviews, &mut cache);
2449
2450        // Since viewport-aware culling is enabled and only view1 intersects it,
2451        // view2.place_subviews should be bypassed/culled.
2452        // Therefore calls count should be 1.
2453        assert_eq!(calls.load(Ordering::SeqCst), 1);
2454    }
2455
2456    #[test]
2457    fn test_layout_budget_thrashing_prevention() {
2458        use std::sync::atomic::{AtomicUsize, Ordering};
2459        use std::sync::Arc;
2460
2461        struct SpyView {
2462            calls: Arc<AtomicUsize>,
2463            hash: u64,
2464            rect: Rect,
2465        }
2466
2467        impl LayoutView for SpyView {
2468            fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
2469                Size { width: self.rect.width, height: self.rect.height }
2470            }
2471            fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
2472                self.calls.fetch_add(1, Ordering::SeqCst);
2473            }
2474            fn view_hash(&self) -> u64 {
2475                self.hash
2476            }
2477        }
2478
2479        let calls = Arc::new(AtomicUsize::new(0));
2480        let view = SpyView {
2481            calls: calls.clone(),
2482            hash: 2001,
2483            rect: Rect::new(0.0, 0.0, 100.0, 100.0),
2484        };
2485
2486        let mut cache = LayoutCache::new();
2487        // Setup budget to be exceeded via the shared process-local layout deadline.
2488        cvkg_core::LayoutCache::set_layout_budget_deadline(Some(
2489            std::time::Instant::now() - std::time::Duration::from_millis(50),
2490        ));
2491        
2492        // Cache a previous rect for the view
2493        cache.previous_rects.insert(2001, Rect::new(10.0, 10.0, 100.0, 100.0));
2494
2495        let mut v = view;
2496        let mut subviews: Vec<&mut dyn LayoutView> = vec![&mut v];
2497
2498        HStack::new(0.0, Alignment::Center, Distribution::Leading)
2499            .place_subviews(Rect::new(0.0, 0.0, 500.0, 500.0), &mut subviews, &mut cache);
2500
2501        // Since we are over budget, Taffy layout computation should be skipped,
2502        // and we should fall back to placing subviews at their previous cached rects.
2503        // Also place_subviews of the spy child will still be called (with the previous rect).
2504        assert_eq!(calls.load(Ordering::SeqCst), 1);
2505        
2506        // Let's verify that Taffy node map does not contain the view, meaning Taffy was skipped!
2507        let engine = TaffyLayoutEngine::get_or_insert_engine(&mut cache);
2508        assert!(!engine.node_map.contains_key(&2001));
2509
2510        cvkg_core::LayoutCache::clear_layout_budget_deadline();
2511    }
2512
2513    // -------------------------------------------------------------------------
2514    // P1-63 regression: spatial index hit testing
2515    // -------------------------------------------------------------------------
2516    #[test]
2517    fn test_spatial_index_hit_test() {
2518        let mut index = LayoutSpatialIndex::new();
2519        let root = Rect { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
2520        let entries = vec![
2521            LayoutSpatialEntry { hash: 1, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
2522            LayoutSpatialEntry { hash: 2, rect: Rect { x: 200.0, y: 200.0, width: 50.0, height: 50.0 } },
2523            LayoutSpatialEntry { hash: 3, rect: Rect { x: 500.0, y: 500.0, width: 200.0, height: 200.0 } },
2524        ];
2525        index.rebuild(root, entries);
2526
2527        // Point inside view 1 only.
2528        let hits = index.hit_test(50.0, 50.0);
2529        assert_eq!(hits.len(), 1);
2530        assert_eq!(hits[0].hash, 1);
2531
2532        // Point inside view 3 only.
2533        let hits = index.hit_test(600.0, 600.0);
2534        assert_eq!(hits.len(), 1);
2535        assert_eq!(hits[0].hash, 3);
2536
2537        // Point outside all views.
2538        let hits = index.hit_test(999.0, 1.0);
2539        assert!(hits.is_empty(), "Expected no hits, got {:?}", hits.iter().map(|e| e.hash).collect::<Vec<_>>());
2540    }
2541
2542    #[test]
2543    fn test_spatial_index_query_region() {
2544        let mut index = LayoutSpatialIndex::new();
2545        let root = Rect { x: 0.0, y: 0.0, width: 500.0, height: 500.0 };
2546        let entries = vec![
2547            LayoutSpatialEntry { hash: 10, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
2548            LayoutSpatialEntry { hash: 20, rect: Rect { x: 400.0, y: 400.0, width: 50.0, height: 50.0 } },
2549        ];
2550        index.rebuild(root, entries);
2551
2552        // Region that only overlaps hash 10.
2553        let region = Rect { x: 0.0, y: 0.0, width: 150.0, height: 150.0 };
2554        let results = index.query_region(&region);
2555        assert!(results.iter().any(|e| e.hash == 10));
2556        assert!(!results.iter().any(|e| e.hash == 20));
2557    }
2558
2559    // -------------------------------------------------------------------------
2560    // P1-67 regression: adaptive touch-target sizing
2561    // -------------------------------------------------------------------------
2562    #[test]
2563    fn test_adaptive_modality_touch_enlarges_small_views() {
2564        let small = cvkg_core::Size { width: 20.0, height: 12.0 };
2565        let adapted = LayoutModality::Touch.adapt_size(small);
2566        assert!(adapted.width >= 44.0, "Width must be at least 44pt for touch");
2567        assert!(adapted.height >= 44.0, "Height must be at least 44pt for touch");
2568    }
2569
2570    #[test]
2571    fn test_adaptive_modality_pointer_does_not_enlarge() {
2572        let large = cvkg_core::Size { width: 200.0, height: 50.0 };
2573        let adapted = LayoutModality::Pointer.adapt_size(large);
2574        assert_eq!(adapted.width, 200.0);
2575        assert_eq!(adapted.height, 50.0);
2576    }
2577
2578    #[test]
2579    fn test_adaptive_modality_accessibility_zoom_spacing() {
2580        assert!(
2581            LayoutModality::AccessibilityZoom.spacing_multiplier() > LayoutModality::Touch.spacing_multiplier(),
2582            "Accessibility zoom must have the largest spacing multiplier"
2583        );
2584    }
2585
2586    // -------------------------------------------------------------------------
2587    // P1-68 regression: focus traversal order
2588    // -------------------------------------------------------------------------
2589    #[test]
2590    fn test_focus_order_ltr_visual_sort() {
2591        // Three views on the same row: right, left, middle.
2592        let candidates = vec![
2593            FocusCandidate { hash: 100, rect: Rect { x: 200.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
2594            FocusCandidate { hash: 200, rect: Rect { x: 0.0,   y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
2595            FocusCandidate { hash: 300, rect: Rect { x: 100.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
2596        ];
2597        let order = compute_focus_order(candidates);
2598        // Expected: left (200) → middle (300) → right (100).
2599        assert_eq!(order, vec![200, 300, 100], "LTR focus order violated: {:?}", order);
2600    }
2601
2602    #[test]
2603    fn test_focus_order_explicit_tabindex_comes_first() {
2604        let candidates = vec![
2605            FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
2606            FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0,   width: 50.0, height: 20.0 }, tab_index: Some(2) },
2607            FocusCandidate { hash: 3, rect: Rect { x: 0.0, y: 50.0,  width: 50.0, height: 20.0 }, tab_index: Some(1) },
2608        ];
2609        let order = compute_focus_order(candidates);
2610        // tabindex=1 (hash 3) first, tabindex=2 (hash 2) second, natural (hash 1) last.
2611        assert_eq!(order[0], 3, "tabindex=1 must be first");
2612        assert_eq!(order[1], 2, "tabindex=2 must be second");
2613        assert_eq!(order[2], 1, "natural order must be last");
2614    }
2615
2616    // -------------------------------------------------------------------------
2617    // P1-69 regression: reading order validation
2618    // -------------------------------------------------------------------------
2619    #[test]
2620    fn test_reading_order_valid_sequence_passes() {
2621        let candidates = vec![
2622            FocusCandidate { hash: 1, rect: Rect { x: 0.0,   y: 0.0,  width: 50.0, height: 20.0 }, tab_index: None },
2623            FocusCandidate { hash: 2, rect: Rect { x: 100.0, y: 0.0,  width: 50.0, height: 20.0 }, tab_index: None },
2624            FocusCandidate { hash: 3, rect: Rect { x: 0.0,   y: 30.0, width: 50.0, height: 20.0 }, tab_index: None },
2625        ];
2626        assert!(validate_reading_order(&candidates).is_ok());
2627    }
2628
2629    #[test]
2630    fn test_reading_order_backwards_row_fails() {
2631        // hash 2 appears after hash 1 but is visually above it — order violation.
2632        let candidates = vec![
2633            FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
2634            FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0,   width: 50.0, height: 20.0 }, tab_index: None },
2635        ];
2636        assert!(validate_reading_order(&candidates).is_err(), "Backwards row must fail validation");
2637    }
2638
2639    // P2-47: Constraint stress tests
2640    #[test]
2641    fn p2_47_deep_tree_100_levels() {
2642        let mut cache = LayoutCache::new();
2643        // Build a deep tree by nesting HStack inside VStack repeatedly
2644        // This exercises the layout engine with 100+ levels of nesting
2645        let mut root: Box<dyn LayoutView> = Box::new(HStack::new(
2646            0.0,
2647            Alignment::Leading,
2648            Distribution::Leading,
2649        ));
2650        for _ in 0..50 {
2651            let child: Box<dyn LayoutView> =
2652                Box::new(HStack::new(0.0, Alignment::Leading, Distribution::Leading));
2653            // Wrap in a simple container
2654            let _ = child;
2655        }
2656        // Just verify that layout computation completes without stack overflow
2657        let proposal = SizeProposal::unspecified();
2658        let _ = root.size_that_fits(proposal, &[], &mut cache);
2659    }
2660
2661    #[test]
2662    fn p2_47_wide_tree_no_panic() {
2663        let mut cache = LayoutCache::new();
2664        // A wide tree with many siblings should complete quickly
2665        let root = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
2666        let proposal = SizeProposal::unspecified();
2667        let _ = root.size_that_fits(proposal, &[], &mut cache);
2668    }
2669
2670    #[test]
2671    fn p2_47_nested_flex_no_panic() {
2672        let mut cache = LayoutCache::new();
2673        let inner = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
2674        let _ = inner.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
2675    }
2676
2677    // -------------------------------------------------------------------------
2678    // P2-46: Progressive Layout Tests
2679    // -------------------------------------------------------------------------
2680
2681    fn make_mock_views(n: usize) -> Vec<MockView> {
2682        (0..n)
2683            .map(|_| MockView {
2684                size: Size {
2685                    width: 50.0,
2686                    height: 30.0,
2687                },
2688                flex: 0.0,
2689            })
2690            .collect()
2691    }
2692
2693    #[test]
2694    fn test_progressive_layout_completes_all_children() {
2695        let views = make_mock_views(10);
2696        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2697        let bounds = Rect {
2698            x: 0.0,
2699            y: 0.0,
2700            width: 1000.0,
2701            height: 200.0,
2702        };
2703        let mut ctx = ProgressiveLayoutContext::new(
2704            bounds,
2705            &subviews,
2706            0.0,
2707            Alignment::Leading,
2708            Distribution::Leading,
2709        );
2710        assert!(!ctx.is_complete());
2711        assert!(!ctx.layout_next_batch(3));
2712        assert!(!ctx.is_complete());
2713        assert!(!ctx.layout_next_batch(3));
2714        assert!(!ctx.is_complete());
2715        assert!(!ctx.layout_next_batch(3));
2716        assert!(!ctx.is_complete());
2717        assert!(ctx.layout_next_batch(3));
2718        assert!(ctx.is_complete());
2719    }
2720
2721    #[test]
2722    fn test_progressive_layout_reports_progress() {
2723        let views = make_mock_views(5);
2724        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2725        let bounds = Rect {
2726            x: 0.0,
2727            y: 0.0,
2728            width: 500.0,
2729            height: 200.0,
2730        };
2731        let mut ctx = ProgressiveLayoutContext::new(
2732            bounds,
2733            &subviews,
2734            0.0,
2735            Alignment::Leading,
2736            Distribution::Leading,
2737        );
2738        assert_eq!(ctx.progress(), (0, 5));
2739        ctx.layout_next_batch(2);
2740        assert_eq!(ctx.progress(), (2, 5));
2741        ctx.layout_next_batch(2);
2742        assert_eq!(ctx.progress(), (4, 5));
2743        ctx.layout_next_batch(1);
2744        assert_eq!(ctx.progress(), (5, 5));
2745    }
2746
2747    #[test]
2748    fn test_progressive_layout_fallback_positions_remaining() {
2749        let views = make_mock_views(6);
2750        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2751        let bounds = Rect {
2752            x: 0.0,
2753            y: 0.0,
2754            width: 600.0,
2755            height: 200.0,
2756        };
2757        let mut ctx = ProgressiveLayoutContext::new(
2758            bounds,
2759            &subviews,
2760            10.0,
2761            Alignment::Leading,
2762            Distribution::Leading,
2763        );
2764        ctx.layout_next_batch(2);
2765        assert_eq!(ctx.progress(), (2, 6));
2766        let mut cache = LayoutCache::new();
2767        let fallback_rects = ctx.apply_remaining_fallback(&mut cache);
2768        assert_eq!(fallback_rects.len(), 4);
2769        for r in &fallback_rects {
2770            assert!(r.width > 0.0);
2771            assert!(r.height > 0.0);
2772        }
2773        assert!(ctx.is_complete());
2774    }
2775
2776    #[test]
2777    fn test_progressive_layout_uses_cached_results() {
2778        let views = make_mock_views(4);
2779        let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2780        let bounds = Rect {
2781            x: 0.0,
2782            y: 0.0,
2783            width: 400.0,
2784            height: 200.0,
2785        };
2786        let mut cache = LayoutCache::new();
2787        let mut ctx1 = ProgressiveLayoutContext::new(
2788            bounds,
2789            &subviews,
2790            0.0,
2791            Alignment::Leading,
2792            Distribution::Leading,
2793        );
2794        ctx1.layout_next_batch(2);
2795        for entry in ctx1.entries.iter() {
2796            if entry.rect != Rect::zero() {
2797                cache.previous_rects.insert(entry.hash, entry.rect);
2798            }
2799        }
2800        let mut ctx2 = ProgressiveLayoutContext::new(
2801            bounds,
2802            &subviews,
2803            0.0,
2804            Alignment::Leading,
2805            Distribution::Leading,
2806        );
2807        let (_done, _rects) = ctx2.layout_next_batch_with_cache(2, &mut cache);
2808        assert_eq!(ctx2.progress().0, 2);
2809    }
2810}