Skip to main content

eui_core/
engine.rs

1//! The layout ENGINE: taffy's algorithms (flexbox, grid, block) over the
2//! surface's own node slots (taffy's "custom tree" API), so the core
3//! controls what `TaffyTree` hides:
4//!
5//! - a measured leaf answers with its size AND its first BASELINE
6//!   (`alignItems: baseline` aligns text by its first line, not its bottom);
7//! - nodes live in STABLE SLOTS (the surface's node indices): a re-windowed
8//!   list or an opened overlay touches only the slots that changed, and
9//!   `mark_dirty` clears the layout cache of a node and its ancestors only;
10//! - `content_size` per node (scroll containers report their scrollable
11//!   extent).
12
13use taffy::prelude::*;
14use taffy::{
15    compute_block_layout, compute_cached_layout, compute_flexbox_layout, compute_grid_layout, compute_hidden_layout, compute_leaf_layout,
16    compute_root_layout, round_layout, CacheTree, LayoutInput, LayoutOutput, RunMode,
17};
18
19/// VAPP-103: the per-slot layout CACHE. taffy 0.12's own `Cache` keys
20/// every size request on the PARENT size too, and nested flex items get
21/// asked once with the parent's width known and once without: both land in
22/// the same slot, evict each other, and every level re-measures its subtree
23/// twice (layout time doubled per level: 20 nested Stacks = 0.6 s, 14
24/// nested Cards = 160 s). The parent size only matters to a node whose OWN
25/// style holds a percentage (its size, insets, margins, padding, border,
26/// gap or flex basis resolve against it), so only those nodes key on it, in
27/// a second bank of slots per parent-width state. A request HITS when its
28/// known dimensions equal the entry's and each unknown dimension's
29/// available space is roughly equal (taffy's own rule otherwise), the
30/// entry answered the same AXIS or both (grid sizes items one axis at a
31/// time: a width-only answer carries a meaningless height) and the same
32/// SIZING MODE (a content-size answer ignores the node's own size style;
33/// taffy's cache conflates the two).
34#[derive(Debug, Clone, Copy)]
35struct CacheEntry<T> {
36    known: Size<Option<f32>>,
37    available: Size<AvailableSpace>,
38    parent: Size<Option<f32>>,
39    axis: taffy::RequestedAxis,
40    sizing: taffy::SizingMode,
41    content: T,
42}
43
44impl<T> CacheEntry<T> {
45    fn matches(&self, input: &LayoutInput, percent: bool) -> bool {
46        let known = input.known_dimensions;
47        known.width == self.known.width
48            && known.height == self.known.height
49            && (known.width.is_some() || self.available.width.is_roughly_equal(input.available_space.width))
50            && (known.height.is_some() || self.available.height.is_roughly_equal(input.available_space.height))
51            && (!percent || self.parent == input.parent_size)
52            && (self.axis == input.axis || self.axis == taffy::RequestedAxis::Both)
53            && self.sizing == input.sizing_mode
54    }
55}
56
57/// The size slots: taffy's nine (by how many dimensions are known and
58/// whether an unknown one is sized under min-content), × parent width
59/// known / unknown for a percentage-sensitive node, × the sizing mode.
60#[derive(Debug, Clone, Default)]
61struct Cache {
62    layout: Option<CacheEntry<LayoutOutput>>,
63    /// Only the slots this node was asked for (most use one to three), by
64    /// slot number.
65    sizes: Vec<(u8, CacheEntry<Size<f32>>)>,
66}
67
68impl Cache {
69    /// The slot of a size request. The sizing mode has slots of its own:
70    /// an entry only answers its own mode, so sharing a slot made an
71    /// inherent-size and a content-size request of one node evict each
72    /// other, and nested BLOCK boxes over a wrapping text re-measured their
73    /// subtree per level (2^depth: 47 nested Boxes never finished).
74    fn slot(input: &LayoutInput, percent: bool) -> u8 {
75        use AvailableSpace::MinContent;
76        let (known, available) = (input.known_dimensions, input.available_space);
77        let base = match (known.width.is_some(), known.height.is_some()) {
78            (true, true) => 0,
79            (true, false) => 1 + (available.height == MinContent) as u8,
80            (false, true) => 3 + (available.width == MinContent) as u8,
81            (false, false) => 5 + (available.height == MinContent) as u8 + 2 * (available.width == MinContent) as u8,
82        };
83        let parent = if percent && input.parent_size.width.is_some() { 9 } else { 0 };
84        let sizing = if input.sizing_mode == taffy::SizingMode::ContentSize { 18 } else { 0 };
85        base + parent + sizing
86    }
87
88    fn get(&self, input: &LayoutInput, percent: bool) -> Option<LayoutOutput> {
89        match input.run_mode {
90            RunMode::PerformLayout => self.layout.filter(|e| e.matches(input, percent)).map(|e| e.content),
91            RunMode::ComputeSize => self.sizes.iter().find(|(_, e)| e.matches(input, percent)).map(|(_, e)| LayoutOutput::from_outer_size(e.content)),
92            RunMode::PerformHiddenLayout => None,
93        }
94    }
95
96    fn store(&mut self, input: &LayoutInput, output: LayoutOutput, percent: bool) {
97        let (known, available, parent, axis, sizing) = (input.known_dimensions, input.available_space, input.parent_size, input.axis, input.sizing_mode);
98        match input.run_mode {
99            RunMode::PerformLayout => self.layout = Some(CacheEntry { known, available, parent, axis, sizing, content: output }),
100            RunMode::ComputeSize => {
101                let entry = CacheEntry { known, available, parent, axis, sizing, content: output.size };
102                let slot = Self::slot(input, percent);
103                match self.sizes.iter_mut().find(|(s, _)| *s == slot) {
104                    Some(held) => held.1 = entry,
105                    None => self.sizes.push((slot, entry)),
106                }
107            }
108            RunMode::PerformHiddenLayout => {}
109        }
110    }
111
112    fn clear(&mut self) {
113        self.layout = None;
114        self.sizes.clear();
115    }
116}
117
118/// Does any length of the node's OWN style resolve against its parent's size?
119fn percent_sensitive(style: &Style) -> bool {
120    let rect = |r: &Rect<LengthPercentage>| [r.left, r.right, r.top, r.bottom].iter().any(|v| v.into_raw().uses_percentage());
121    let rect_auto = |r: &Rect<LengthPercentageAuto>| [r.left, r.right, r.top, r.bottom].iter().any(|v| v.into_raw().uses_percentage());
122    let size = |s: &Size<Dimension>| s.width.into_raw().uses_percentage() || s.height.into_raw().uses_percentage();
123    size(&style.size)
124        || size(&style.min_size)
125        || size(&style.max_size)
126        || rect(&style.padding)
127        || rect(&style.border)
128        || rect_auto(&style.margin)
129        || rect_auto(&style.inset)
130        || style.flex_basis.into_raw().uses_percentage()
131        || style.gap.width.into_raw().uses_percentage()
132        || style.gap.height.into_raw().uses_percentage()
133}
134
135/// What a measured leaf answers for one taffy request.
136#[derive(Debug, Clone, Copy, PartialEq, Default)]
137pub struct LeafAnswer {
138    pub size: Size<f32>,
139    /// The first baseline, from the top of the border box; `None` = the
140    /// bottom edge (CSS synthesizes it there).
141    pub baseline: Option<f32>,
142}
143
144#[derive(Debug, Clone)]
145struct ENode {
146    style: Style,
147    cache: Cache,
148    unrounded: Layout,
149    rounded: Layout,
150    children: Vec<u32>,
151    parent: Option<u32>,
152    /// A measured leaf (its size comes from the measure callback).
153    measured: bool,
154    /// Its own style resolves a percentage against the parent size.
155    percent: bool,
156}
157
158impl Default for ENode {
159    fn default() -> Self {
160        ENode {
161            style: Style::default(),
162            cache: Cache::default(),
163            unrounded: Layout::with_order(0),
164            rounded: Layout::with_order(0),
165            children: Vec::new(),
166            parent: None,
167            measured: false,
168            percent: false,
169        }
170    }
171}
172
173/// One tree of slots; several roots (the main tree, every overlay layer).
174#[derive(Debug, Clone, Default)]
175pub struct Engine {
176    nodes: Vec<ENode>,
177    rounding: bool,
178    /// Tests only: every request misses (the differential reference).
179    #[cfg(test)]
180    no_cache: bool,
181}
182
183/// The measure callback: `(slot, known size, available space) → answer`.
184pub type MeasureFn<'a> = dyn FnMut(u32, Size<Option<f32>>, Size<AvailableSpace>) -> LeafAnswer + 'a;
185
186struct View<'t, 'm> {
187    engine: &'t mut Engine,
188    measure: &'t mut MeasureFn<'m>,
189}
190
191pub struct ChildIter<'a>(std::slice::Iter<'a, u32>);
192
193impl Iterator for ChildIter<'_> {
194    type Item = NodeId;
195    fn next(&mut self) -> Option<NodeId> {
196        self.0.next().map(|i| NodeId::from(*i as usize))
197    }
198}
199
200fn slot(id: NodeId) -> usize {
201    usize::from(id)
202}
203
204impl taffy::TraversePartialTree for View<'_, '_> {
205    type ChildIter<'a>
206        = ChildIter<'a>
207    where
208        Self: 'a;
209
210    fn child_ids(&self, node: NodeId) -> Self::ChildIter<'_> {
211        ChildIter(self.engine.nodes[slot(node)].children.iter())
212    }
213
214    fn child_count(&self, node: NodeId) -> usize {
215        self.engine.nodes[slot(node)].children.len()
216    }
217
218    fn get_child_id(&self, node: NodeId, index: usize) -> NodeId {
219        NodeId::from(self.engine.nodes[slot(node)].children[index] as usize)
220    }
221}
222
223impl taffy::TraverseTree for View<'_, '_> {}
224
225impl taffy::LayoutPartialTree for View<'_, '_> {
226    type CoreContainerStyle<'a>
227        = &'a Style
228    where
229        Self: 'a;
230    type CustomIdent = String;
231
232    fn get_core_container_style(&self, node: NodeId) -> Self::CoreContainerStyle<'_> {
233        &self.engine.nodes[slot(node)].style
234    }
235
236    fn set_unrounded_layout(&mut self, node: NodeId, layout: &Layout) {
237        self.engine.nodes[slot(node)].unrounded = *layout;
238    }
239
240    fn resolve_calc_value(&self, _val: *const (), _basis: f32) -> f32 {
241        0.0
242    }
243
244    fn compute_child_layout(&mut self, node: NodeId, inputs: LayoutInput) -> LayoutOutput {
245        crate::deep(|| self.compute_child(node, inputs))
246    }
247}
248
249impl View<'_, '_> {
250    fn compute_child(&mut self, node: NodeId, inputs: LayoutInput) -> LayoutOutput {
251        if inputs.run_mode == RunMode::PerformHiddenLayout {
252            return compute_hidden_layout(self, node);
253        }
254        compute_cached_layout(self, node, inputs, |view, node, inputs| {
255            let n = &view.engine.nodes[slot(node)];
256            let display = n.style.display;
257            let has_children = !n.children.is_empty();
258            match (display, has_children) {
259                (Display::None, _) => compute_hidden_layout(view, node),
260                (Display::Block, true) => compute_block_layout(view, node, inputs, None),
261                (Display::Flex, true) => compute_flexbox_layout(view, node, inputs),
262                (Display::Grid, true) => compute_grid_layout(view, node, inputs),
263                (_, false) => {
264                    let index = slot(node) as u32;
265                    let View { engine, measure } = view;
266                    let n = &engine.nodes[slot(node)];
267                    let measured = n.measured;
268                    let ratio = n.style.aspect_ratio.filter(|r| r.is_finite() && *r > 0.0);
269                    let mut baseline = None;
270                    let mut output = compute_leaf_layout(inputs, &n.style, |_, _| 0.0, |known, available| {
271                        // Round 2 §7: an aspect-ratio box whose width is known
272                        // (stretched, flexed) takes height = width / ratio, as
273                        // CSS transfers it (taffy only does for SIZED axes).
274                        if let (Some(r), Some(w), None) = (ratio, known.width, known.height) {
275                            return Size { width: w, height: w / r };
276                        }
277                        if !measured {
278                            return Size::ZERO;
279                        }
280                        let answer = measure(index, known, available);
281                        baseline = answer.baseline;
282                        answer.size
283                    });
284                    if let Some(b) = baseline {
285                        output.first_baselines.y = Some(b);
286                    }
287                    output
288                }
289            }
290        })
291    }
292}
293
294impl CacheTree for View<'_, '_> {
295    fn cache_get(&self, node: NodeId, input: &LayoutInput) -> Option<LayoutOutput> {
296        #[cfg(test)]
297        if self.engine.no_cache {
298            return None;
299        }
300        let n = &self.engine.nodes[slot(node)];
301        n.cache.get(input, n.percent)
302    }
303
304    fn cache_store(&mut self, node: NodeId, input: &LayoutInput, output: LayoutOutput) {
305        let n = &mut self.engine.nodes[slot(node)];
306        n.cache.store(input, output, n.percent)
307    }
308
309    fn cache_clear(&mut self, node: NodeId) {
310        self.engine.nodes[slot(node)].cache.clear();
311    }
312}
313
314impl taffy::LayoutFlexboxContainer for View<'_, '_> {
315    type FlexboxContainerStyle<'a>
316        = &'a Style
317    where
318        Self: 'a;
319    type FlexboxItemStyle<'a>
320        = &'a Style
321    where
322        Self: 'a;
323
324    fn get_flexbox_container_style(&self, node: NodeId) -> Self::FlexboxContainerStyle<'_> {
325        &self.engine.nodes[slot(node)].style
326    }
327
328    fn get_flexbox_child_style(&self, child: NodeId) -> Self::FlexboxItemStyle<'_> {
329        &self.engine.nodes[slot(child)].style
330    }
331}
332
333impl taffy::LayoutGridContainer for View<'_, '_> {
334    type GridContainerStyle<'a>
335        = &'a Style
336    where
337        Self: 'a;
338    type GridItemStyle<'a>
339        = &'a Style
340    where
341        Self: 'a;
342
343    fn get_grid_container_style(&self, node: NodeId) -> Self::GridContainerStyle<'_> {
344        &self.engine.nodes[slot(node)].style
345    }
346
347    fn get_grid_child_style(&self, child: NodeId) -> Self::GridItemStyle<'_> {
348        &self.engine.nodes[slot(child)].style
349    }
350}
351
352impl taffy::LayoutBlockContainer for View<'_, '_> {
353    type BlockContainerStyle<'a>
354        = &'a Style
355    where
356        Self: 'a;
357    type BlockItemStyle<'a>
358        = &'a Style
359    where
360        Self: 'a;
361
362    fn get_block_container_style(&self, node: NodeId) -> Self::BlockContainerStyle<'_> {
363        &self.engine.nodes[slot(node)].style
364    }
365
366    fn get_block_child_style(&self, child: NodeId) -> Self::BlockItemStyle<'_> {
367        &self.engine.nodes[slot(child)].style
368    }
369}
370
371impl taffy::RoundTree for View<'_, '_> {
372    fn get_unrounded_layout(&self, node: NodeId) -> Layout {
373        self.engine.nodes[slot(node)].unrounded
374    }
375
376    fn set_final_layout(&mut self, node: NodeId, layout: &Layout) {
377        self.engine.nodes[slot(node)].rounded = *layout;
378    }
379}
380
381impl Engine {
382    pub fn new() -> Engine {
383        Engine::default()
384    }
385
386    /// Off = fractional frames (the default).
387    pub fn set_rounding(&mut self, on: bool) {
388        self.rounding = on;
389    }
390
391    pub fn len(&self) -> usize {
392        self.nodes.len()
393    }
394
395    pub fn is_empty(&self) -> bool {
396        self.nodes.is_empty()
397    }
398
399    /// Grow (or shrink) to `n` slots; new slots are empty leaves.
400    pub fn resize(&mut self, n: usize) {
401        self.nodes.resize_with(n, ENode::default);
402    }
403
404    pub fn clear(&mut self) {
405        self.nodes.clear();
406    }
407
408    /// Replace a slot's style; its cache and its ancestors' are cleared when
409    /// the style differs.
410    pub fn set_style(&mut self, i: u32, style: Style) -> bool {
411        if self.nodes[i as usize].style == style {
412            return false;
413        }
414        self.nodes[i as usize].percent = percent_sensitive(&style);
415        self.nodes[i as usize].style = style;
416        self.mark_dirty(i);
417        true
418    }
419
420    pub fn style(&self, i: u32) -> &Style {
421        &self.nodes[i as usize].style
422    }
423
424    /// Set a slot's children (parent pointers follow); dirty when changed.
425    pub fn set_children(&mut self, i: u32, children: &[u32]) {
426        if self.nodes[i as usize].children == children {
427            return;
428        }
429        for c in std::mem::take(&mut self.nodes[i as usize].children) {
430            if self.nodes[c as usize].parent == Some(i) {
431                self.nodes[c as usize].parent = None;
432            }
433        }
434        for &c in children {
435            self.nodes[c as usize].parent = Some(i);
436        }
437        self.nodes[i as usize].children = children.to_vec();
438        self.mark_dirty(i);
439    }
440
441    pub fn children(&self, i: u32) -> &[u32] {
442        &self.nodes[i as usize].children
443    }
444
445    pub fn set_measured(&mut self, i: u32, measured: bool) {
446        if self.nodes[i as usize].measured != measured {
447            self.nodes[i as usize].measured = measured;
448            self.mark_dirty(i);
449        }
450    }
451
452    /// Forget the cached layout of `i` and every ancestor.
453    pub fn mark_dirty(&mut self, i: u32) {
454        let mut cur = Some(i);
455        while let Some(n) = cur {
456            let node = &mut self.nodes[n as usize];
457            node.cache.clear();
458            cur = node.parent;
459        }
460    }
461
462    pub fn mark_all_dirty(&mut self) {
463        for n in &mut self.nodes {
464            n.cache.clear();
465        }
466    }
467
468    /// Lay out the tree under `root` against the available space.
469    pub fn compute(&mut self, root: u32, available: Size<AvailableSpace>, measure: &mut MeasureFn) {
470        let rounding = self.rounding;
471        let mut view = View { engine: self, measure };
472        compute_root_layout(&mut view, NodeId::from(root as usize), available);
473        if rounding {
474            round_layout(&mut view, NodeId::from(root as usize));
475        }
476    }
477
478    /// The final layout of a slot (relative to its parent).
479    pub fn layout(&self, i: u32) -> &Layout {
480        let n = &self.nodes[i as usize];
481        if self.rounding {
482            &n.rounded
483        } else {
484            &n.unrounded
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn baseline_alignment_uses_the_measured_first_baseline() {
495        let mut e = Engine::new();
496        e.resize(3);
497        e.set_style(0, Style { display: Display::Flex, align_items: Some(AlignItems::BASELINE), ..Style::default() });
498        e.set_children(0, &[1, 2]);
499        e.set_measured(1, true);
500        e.set_measured(2, true);
501        // A 40 px control whose text baseline sits at 26, beside a 20 px
502        // text line whose baseline is 15: the text moves down by 11.
503        let mut measure = |i: u32, _k: Size<Option<f32>>, _a: Size<AvailableSpace>| match i {
504            1 => LeafAnswer { size: Size { width: 80.0, height: 40.0 }, baseline: Some(26.0) },
505            _ => LeafAnswer { size: Size { width: 50.0, height: 20.0 }, baseline: Some(15.0) },
506        };
507        e.compute(0, Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent }, &mut measure);
508        assert_eq!(e.layout(1).location.y, 0.0);
509        assert_eq!(e.layout(2).location.y, 11.0);
510        // Without baselines the bottoms align (CSS synthesizes them there).
511        let mut bottoms = |i: u32, _k: Size<Option<f32>>, _a: Size<AvailableSpace>| match i {
512            1 => LeafAnswer { size: Size { width: 80.0, height: 40.0 }, baseline: None },
513            _ => LeafAnswer { size: Size { width: 50.0, height: 20.0 }, baseline: None },
514        };
515        e.mark_all_dirty();
516        e.compute(0, Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent }, &mut bottoms);
517        assert_eq!(e.layout(2).location.y, 20.0);
518    }
519
520    #[test]
521    fn dirty_marking_climbs_to_the_root_only() {
522        let mut e = Engine::new();
523        e.resize(4);
524        e.set_style(0, Style { display: Display::Flex, flex_direction: FlexDirection::Column, ..Style::default() });
525        e.set_children(0, &[1, 2]);
526        e.set_style(2, Style { display: Display::Flex, ..Style::default() });
527        e.set_children(2, &[3]);
528        for i in [1, 3] {
529            e.set_measured(i, true);
530        }
531        let calls = std::cell::Cell::new(0);
532        let mut measure = |_i: u32, _k: Size<Option<f32>>, _a: Size<AvailableSpace>| {
533            calls.set(calls.get() + 1);
534            LeafAnswer { size: Size { width: 10.0, height: 10.0 }, baseline: None }
535        };
536        e.compute(0, Size { width: AvailableSpace::Definite(100.0), height: AvailableSpace::MaxContent }, &mut measure);
537        let first = calls.get();
538        e.compute(0, Size { width: AvailableSpace::Definite(100.0), height: AvailableSpace::MaxContent }, &mut measure);
539        assert_eq!(calls.get(), first, "a clean tree answers from its caches");
540        e.mark_dirty(3);
541        e.compute(0, Size { width: AvailableSpace::Definite(100.0), height: AvailableSpace::MaxContent }, &mut measure);
542        assert!(calls.get() > first, "the dirty leaf is measured again");
543    }
544}
545
546
547#[cfg(test)]
548mod cache_tests {
549    use super::*;
550
551    /// VAPP-103: nested flex columns measure their leaf a constant number
552    /// of times at any depth (taffy 0.12's cache made it 2^depth), with and
553    /// without percentages in every level's own style.
554    #[test]
555    fn nested_flex_columns_measure_the_leaf_a_constant_number_of_times() {
556        for percent in [false, true] {
557            for depth in [4usize, 30, 50] {
558                let mut e = Engine::new();
559                e.resize(depth + 1);
560                for i in 0..depth {
561                    let mut style = Style { display: Display::Flex, flex_direction: FlexDirection::Column, ..Style::default() };
562                    if percent {
563                        style.size.width = Dimension::percent(1.0);
564                        style.padding.left = LengthPercentage::percent(0.01);
565                    }
566                    e.set_style(i as u32, style);
567                    e.set_children(i as u32, &[(i + 1) as u32]);
568                }
569                e.set_measured(depth as u32, true);
570                let calls = std::cell::Cell::new(0);
571                let mut measure = |_i: u32, _k: Size<Option<f32>>, _a: Size<AvailableSpace>| {
572                    calls.set(calls.get() + 1);
573                    LeafAnswer { size: Size { width: 10.0, height: 10.0 }, baseline: None }
574                };
575                e.compute(0, Size { width: AvailableSpace::Definite(400.0), height: AvailableSpace::MaxContent }, &mut measure);
576                assert!(calls.get() <= 8, "depth {depth} (percent {percent}): {} measures", calls.get());
577                assert_eq!(e.layout(depth as u32).size.height, 10.0);
578            }
579        }
580    }
581
582    /// Nested BLOCK boxes over a WRAPPING leaf (its height follows the
583    /// width it gets) measure it a constant number of times at any depth:
584    /// a content-size and an inherent-size request no longer evict each
585    /// other's slot (47 nested Boxes over a sentence used to never finish).
586    #[test]
587    fn nested_block_boxes_over_a_wrapping_leaf_measure_it_a_constant_number_of_times() {
588        for display in [Display::Block, Display::Flex] {
589            for depth in [4usize, 30, 50] {
590                let mut e = Engine::new();
591                e.resize(depth + 1);
592                for i in 0..depth {
593                    e.set_style(i as u32, Style { display, flex_direction: FlexDirection::Column, ..Style::default() });
594                    e.set_children(i as u32, &[(i + 1) as u32]);
595                }
596                e.set_measured(depth as u32, true);
597                let calls = std::cell::Cell::new(0);
598                let mut measure = |_i: u32, k: Size<Option<f32>>, a: Size<AvailableSpace>| {
599                    calls.set(calls.get() + 1);
600                    let width = k.width.unwrap_or(match a.width {
601                        AvailableSpace::Definite(w) => w.min(300.0),
602                        AvailableSpace::MinContent => 60.0,
603                        AvailableSpace::MaxContent => 300.0,
604                    });
605                    LeafAnswer { size: Size { width, height: k.height.unwrap_or((300.0 / width).ceil() * 16.0) }, baseline: None }
606                };
607                e.compute(0, Size { width: AvailableSpace::Definite(200.0), height: AvailableSpace::MaxContent }, &mut measure);
608                assert!(calls.get() <= 12, "{display:?} depth {depth}: {} measures", calls.get());
609                assert_eq!(e.layout(depth as u32).size.height, 32.0);
610            }
611        }
612    }
613}
614
615#[cfg(test)]
616mod differential_tests {
617    use super::*;
618
619    struct Rng(u64);
620    impl Rng {
621        fn n(&mut self, m: u64) -> u64 {
622            self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
623            (self.0 >> 33) % m
624        }
625    }
626
627    fn dim(r: &mut Rng) -> Dimension {
628        match r.n(4) {
629            0 => Dimension::length((r.n(20) * 10) as f32),
630            1 => Dimension::percent((r.n(10) as f32 + 1.0) / 10.0),
631            _ => Dimension::auto(),
632        }
633    }
634
635    fn lp(r: &mut Rng) -> LengthPercentage {
636        match r.n(3) {
637            0 => LengthPercentage::length(r.n(8) as f32),
638            1 => LengthPercentage::percent(r.n(5) as f32 / 100.0),
639            _ => LengthPercentage::length(0.0),
640        }
641    }
642
643    fn style(r: &mut Rng, leaf: bool) -> Style {
644        let mut s = Style::default();
645        s.display = if leaf {
646            Display::Flex
647        } else {
648            match r.n(3) {
649                0 => Display::Block,
650                1 => Display::Grid,
651                _ => Display::Flex,
652            }
653        };
654        s.flex_direction = if r.n(2) == 0 { FlexDirection::Row } else { FlexDirection::Column };
655        if r.n(3) == 0 {
656            s.flex_wrap = FlexWrap::Wrap;
657        }
658        s.size = Size { width: dim(r), height: if r.n(3) == 0 { dim(r) } else { Dimension::auto() } };
659        if r.n(4) == 0 {
660            s.min_size.width = dim(r);
661        }
662        if r.n(4) == 0 {
663            s.max_size.width = dim(r);
664        }
665        s.padding = Rect { left: lp(r), right: lp(r), top: lp(r), bottom: lp(r) };
666        s.flex_grow = r.n(3) as f32;
667        s.flex_shrink = if r.n(4) == 0 { 0.0 } else { 1.0 };
668        if r.n(4) == 0 {
669            s.flex_basis = dim(r);
670        }
671        if r.n(4) == 0 {
672            s.align_items = Some(AlignItems::FLEX_START);
673        }
674        if r.n(5) == 0 {
675            s.gap = Size { width: lp(r), height: lp(r) };
676        }
677        s
678    }
679
680    /// A wrapping text leaf: `words` 13 px words on 16 px lines.
681    fn text(words: u32, known: Size<Option<f32>>, avail: Size<AvailableSpace>) -> Size<f32> {
682        let total = words as f32 * 13.0;
683        let w = known.width.unwrap_or(match avail.width {
684            AvailableSpace::Definite(w) => w.min(total).max(13.0),
685            AvailableSpace::MinContent => 13.0,
686            AvailableSpace::MaxContent => total,
687        });
688        let per = (w / 13.0).floor().max(1.0);
689        Size { width: w, height: known.height.unwrap_or((words as f32 / per).ceil() * 16.0) }
690    }
691
692    fn build(styles: &[Style], kids: &[Vec<u32>], no_cache: bool) -> Engine {
693        let mut e = Engine::new();
694        e.no_cache = no_cache;
695        e.resize(styles.len());
696        for i in 0..styles.len() {
697            e.set_style(i as u32, styles[i].clone());
698            e.set_children(i as u32, &kids[i]);
699            if kids[i].is_empty() {
700                e.set_measured(i as u32, true);
701            }
702        }
703        e
704    }
705
706    /// VAPP-103 rfix: a grid sizes its items one axis at a time; a
707    /// width-only cache answer must never serve a height request (the box
708    /// holding a 160 px child got height 0 and the next item overlapped it).
709    #[test]
710    fn grid_items_keep_their_heights_under_the_cache() {
711        let grid = Style { display: Display::Grid, ..Style::default() };
712        let a = Style { size: Size { width: Dimension::length(180.0), height: Dimension::auto() }, ..Style::default() };
713        let tall = Style { size: Size { width: Dimension::auto(), height: Dimension::length(160.0) }, ..Style::default() };
714        let styles = vec![grid, a, tall, Style::default()];
715        let kids = vec![vec![1, 3], vec![2], vec![], vec![]];
716        let mut e = build(&styles, &kids, false);
717        e.set_measured(2, false);
718        let mut m = |_i: u32, k: Size<Option<f32>>, a: Size<AvailableSpace>| LeafAnswer { size: text(5, k, a), baseline: None };
719        e.compute(0, Size { width: AvailableSpace::Definite(400.0), height: AvailableSpace::MaxContent }, &mut m);
720        assert_eq!(e.layout(1).size.height, 160.0);
721        assert_eq!(e.layout(3).location.y, 160.0);
722    }
723
724    /// The cache never changes a layout: random flex/grid/block trees with
725    /// percentages, laid out over a changing viewport by ONE cached engine,
726    /// equal a fresh uncached engine at every pass.
727    #[test]
728    fn cached_layout_equals_uncached_layout_on_random_trees() {
729        let viewports = [(400.0f32, None), (250.0, Some(300.0)), (400.0, None), (137.0, Some(300.0)), (400.0, Some(90.0))];
730        for seed in 0..1000u64 {
731            let mut r = Rng(seed * 7919 + 1);
732            let n = if seed % 2 == 0 { 2 + r.n(4) as usize } else { 3 + r.n(25) as usize };
733            let mut kids: Vec<Vec<u32>> = vec![vec![]; n];
734            for i in 1..n {
735                kids[r.n(i as u64) as usize].push(i as u32);
736            }
737            let styles: Vec<Style> = (0..n).map(|i| style(&mut r, kids[i].is_empty())).collect();
738            let words: Vec<u32> = (0..n).map(|_| 1 + r.n(30) as u32).collect();
739            let mut cached = build(&styles, &kids, false);
740            for (pass, &(w, h)) in viewports.iter().enumerate() {
741                let avail = Size { width: AvailableSpace::Definite(w), height: h.map_or(AvailableSpace::MaxContent, AvailableSpace::Definite) };
742                let mut m = |i: u32, k: Size<Option<f32>>, a: Size<AvailableSpace>| LeafAnswer { size: text(words[i as usize], k, a), baseline: None };
743                cached.compute(0, avail, &mut m);
744                let mut reference = build(&styles, &kids, true);
745                reference.compute(0, avail, &mut m);
746                for i in 0..n as u32 {
747                    let (a, b) = (cached.layout(i), reference.layout(i));
748                    let d = (a.size.width - b.size.width).abs()
749                        + (a.size.height - b.size.height).abs()
750                        + (a.location.x - b.location.x).abs()
751                        + (a.location.y - b.location.y).abs();
752                    assert!(d <= 0.01, "seed {seed} pass {pass} node {i}: cached {:?}@{:?} uncached {:?}@{:?}", a.size, a.location, b.size, b.location);
753                }
754            }
755        }
756    }
757
758    /// The depth bench: nested grid and block columns measure their leaf a
759    /// bounded number of times at depth 30 (the axis key must not bring
760    /// back the per-level doubling).
761    #[test]
762    fn nested_grid_and_block_measure_the_leaf_a_bounded_number_of_times() {
763        for display in [Display::Grid, Display::Block, Display::Flex] {
764            let depth = 30usize;
765            let mut e = Engine::new();
766            e.resize(depth + 1);
767            for i in 0..depth {
768                e.set_style(i as u32, Style { display, flex_direction: FlexDirection::Column, ..Style::default() });
769                e.set_children(i as u32, &[(i + 1) as u32]);
770            }
771            e.set_measured(depth as u32, true);
772            let calls = std::cell::Cell::new(0u64);
773            let mut measure = |_i: u32, _k: Size<Option<f32>>, _a: Size<AvailableSpace>| {
774                calls.set(calls.get() + 1);
775                LeafAnswer { size: Size { width: 10.0, height: 10.0 }, baseline: None }
776            };
777            e.compute(0, Size { width: AvailableSpace::Definite(400.0), height: AvailableSpace::MaxContent }, &mut measure);
778            assert!(calls.get() <= 16, "{display:?} depth {depth}: {} measures", calls.get());
779            assert_eq!(e.layout(depth as u32).size.height, 10.0);
780        }
781    }
782}