From 88bb97e574cb56ac249a093edc18c6c29d5a09b1 Mon Sep 17 00:00:00 2001 From: Dr George Atkinson Date: Fri, 21 Aug 2026 17:16:53 +0100 Subject: [PATCH 1/2] Add layout_modes bench, refactor layout.rs Add a new `layout_modes` benchmark covering stack, wrap, overlay, grid, and incremental layout scenarios. Refactor `layout.rs` to cache repeated calls to `node.direction()` and `node.main_between()`, and simplify alignment fraction computation by extracting `alignment_fractions` helper instead of a match expression with manual axis swapping. --- Cargo.toml | 4 + benches/layout_modes.rs | 214 ++++++++++++++++++++++++++++++++++++++++ src/layout.rs | 45 +++------ 3 files changed, 234 insertions(+), 29 deletions(-) create mode 100644 benches/layout_modes.rs diff --git a/Cargo.toml b/Cargo.toml index 967e847d..2992f58e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,4 +26,8 @@ doctest = false [[bench]] name = "stack" +harness = false + +[[bench]] +name = "layout_modes" harness = false \ No newline at end of file diff --git a/benches/layout_modes.rs b/benches/layout_modes.rs new file mode 100644 index 00000000..eaac832d --- /dev/null +++ b/benches/layout_modes.rs @@ -0,0 +1,214 @@ +use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput}; +use morphorm::*; +use morphorm_ecs::*; + +fn root(world: &mut World, layout_type: LayoutType, width: f32, height: f32) -> Entity { + let root = world.add(None); + world.set_layout_type(root, layout_type); + world.set_width(root, Units::Pixels(width)); + world.set_height(root, Units::Pixels(height)); + world.set_alignment(root, Alignment::TopLeft); + root +} + +fn build_stack(world: &mut World, parent: Entity, layout_type: LayoutType, breadth: usize, depth: usize) { + if depth == 0 { + return; + } + + for index in 0..breadth { + let child = world.add(Some(parent)); + world.set_layout_type(child, layout_type); + world.set_width(child, Units::Stretch(1.0)); + world.set_height(child, if index % 3 == 0 { Units::Pixels(24.0) } else { Units::Stretch(1.0) }); + build_stack(world, child, layout_type, breadth, depth - 1); + } +} + +fn stack_world(layout_type: LayoutType) -> (World, Entity) { + let mut world = World::default(); + let root = root(&mut world, layout_type, 1200.0, 900.0); + build_stack(&mut world, root, layout_type, 8, 3); + (world, root) +} + +fn wrap_world(layout_type: LayoutType) -> (World, Entity) { + let mut world = World::default(); + let root = root(&mut world, layout_type, 800.0, 600.0); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_horizontal_gap(root, Units::Pixels(4.0)); + world.set_vertical_gap(root, Units::Pixels(4.0)); + + for index in 0..256 { + let child = world.add(Some(root)); + world.set_width(child, if index % 4 == 0 { Units::Stretch(1.0) } else { Units::Pixels(48.0) }); + world.set_height(child, if index % 5 == 0 { Units::Stretch(1.0) } else { Units::Pixels(24.0) }); + world.set_min_width(child, Units::Pixels(20.0)); + world.set_min_height(child, Units::Pixels(16.0)); + } + + (world, root) +} + +fn overlay_world(auto_size: bool) -> (World, Entity) { + let mut world = World::default(); + let root = root(&mut world, LayoutType::Overlay, 800.0, 600.0); + world.set_alignment(root, Alignment::Center); + + if auto_size { + world.set_width(root, Units::Auto); + world.set_height(root, Units::Auto); + world.set_min_width(root, Units::Auto); + world.set_min_height(root, Units::Auto); + } + + for index in 0..256 { + let child = world.add(Some(root)); + world.set_width(child, Units::Pixels(32.0 + (index % 8) as f32 * 4.0)); + world.set_height(child, Units::Pixels(20.0 + (index % 5) as f32 * 3.0)); + } + + (world, root) +} + +fn grid_world(auto_tracks: bool) -> (World, Entity) { + const SIDE: usize = 16; + + let mut world = World::default(); + let root = root(&mut world, LayoutType::Grid, 1024.0, 768.0); + let track = if auto_tracks { Units::Auto } else { Units::Stretch(1.0) }; + world.set_grid_columns(root, vec![track; SIDE]); + world.set_grid_rows(root, vec![track; SIDE]); + world.set_horizontal_gap(root, Units::Pixels(2.0)); + world.set_vertical_gap(root, Units::Pixels(2.0)); + + for row in 0..SIDE { + for column in 0..SIDE { + let child = world.add(Some(root)); + world.set_column_start(child, column); + world.set_row_start(child, row); + world.set_width(child, Units::Pixels(24.0 + (column % 4) as f32 * 4.0)); + world.set_height(child, Units::Pixels(18.0 + (row % 3) as f32 * 3.0)); + } + } + + (world, root) +} + +fn incremental_world(auto_parent: bool) -> (World, Entity, Entity) { + let mut world = World::default(); + let root = root(&mut world, LayoutType::Column, 1200.0, 900.0); + let mut dirty = root; + + for branch in 0..8 { + let parent = world.add(Some(root)); + world.set_layout_type(parent, LayoutType::Column); + world.set_width(parent, Units::Pixels(120.0)); + world.set_height(parent, if auto_parent && branch == 0 { Units::Auto } else { Units::Pixels(100.0) }); + + for child_index in 0..64 { + let child = world.add(Some(parent)); + world.set_width(child, Units::Pixels(20.0)); + world.set_height(child, Units::Pixels(12.0)); + if branch == 0 && child_index == 0 { + dirty = child; + } + } + } + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + world.set_height(dirty, Units::Pixels(16.0)); + (world, root, dirty) +} + +fn benchmark_full_layout(c: &mut Criterion) { + let mut group = c.benchmark_group("layout modes"); + group.sample_size(20); + + for layout_type in [LayoutType::Row, LayoutType::Column] { + group.throughput(Throughput::Elements(585)); + group.bench_with_input( + BenchmarkId::new("stack", format!("{layout_type:?}")), + &layout_type, + |b, &layout_type| { + b.iter_batched( + || stack_world(layout_type), + |(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()), + BatchSize::SmallInput, + ); + }, + ); + } + + for layout_type in [LayoutType::Row, LayoutType::Column] { + group.throughput(Throughput::Elements(257)); + group.bench_with_input( + BenchmarkId::new("wrap", format!("{layout_type:?}")), + &layout_type, + |b, &layout_type| { + b.iter_batched( + || wrap_world(layout_type), + |(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()), + BatchSize::SmallInput, + ); + }, + ); + } + + for auto_size in [false, true] { + group.throughput(Throughput::Elements(257)); + group.bench_with_input( + BenchmarkId::new("overlay", if auto_size { "auto" } else { "fixed" }), + &auto_size, + |b, &auto_size| { + b.iter_batched( + || overlay_world(auto_size), + |(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()), + BatchSize::SmallInput, + ); + }, + ); + } + + for auto_tracks in [false, true] { + group.throughput(Throughput::Elements(257)); + group.bench_with_input( + BenchmarkId::new("grid", if auto_tracks { "auto" } else { "stretch" }), + &auto_tracks, + |b, &auto_tracks| { + b.iter_batched( + || grid_world(auto_tracks), + |(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()), + BatchSize::SmallInput, + ); + }, + ); + } + + group.finish(); +} + +fn benchmark_incremental_layout(c: &mut Criterion) { + let mut group = c.benchmark_group("incremental layout"); + group.sample_size(20); + group.throughput(Throughput::Elements(521)); + + for auto_parent in [false, true] { + group.bench_with_input( + BenchmarkId::new("dirty leaf", if auto_parent { "auto parent" } else { "fixed parent" }), + &auto_parent, + |b, &auto_parent| { + b.iter_batched( + || incremental_world(auto_parent), + |(mut world, _root, dirty)| dirty.layout(&mut world.cache, &world.tree, &world.store, &mut ()), + BatchSize::SmallInput, + ); + }, + ); + } + + group.finish(); +} + +criterion_group!(benches, benchmark_full_layout, benchmark_incremental_layout); +criterion_main!(benches); diff --git a/src/layout.rs b/src/layout.rs index ea580034..68e1685b 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1463,17 +1463,17 @@ where let padding_cross_before = node.padding_cross_before(store, layout_type).to_px(parent_cross, 0.0); let padding_cross_after = node.padding_cross_after(store, layout_type).to_px(parent_cross, 0.0); + let main_between = if relative_children.len() > 1 { node.main_between(store, layout_type) } else { Auto }; let min_main_between = node.min_main_between(store, layout_type); let max_main_between = node.max_main_between(store, layout_type); parent_main = parent_main - padding_main_before - padding_main_after; parent_cross = parent_cross - padding_cross_before - padding_cross_after; - let is_row_rtl = - layout_type == LayoutType::Row && node.direction(store).unwrap_or_default() == Direction::RightToLeft; + let direction = node.direction(store).unwrap_or_default(); + let is_row_rtl = layout_type == LayoutType::Row && direction == Direction::RightToLeft; - let is_column_rtl = - layout_type == LayoutType::Column && node.direction(store).unwrap_or_default() == Direction::RightToLeft; + let is_column_rtl = layout_type == LayoutType::Column && direction == Direction::RightToLeft; if is_row_rtl { relative_children.reverse(); @@ -1496,9 +1496,7 @@ where let mut computed_child_main_after = 0.0f32; if last != Some(index) { - let child_main_after = node.main_between(store, layout_type); - - if let Stretch(factor) = child_main_after { + if let Stretch(factor) = main_between { main_flex_sum += factor; main_axis.push(StretchItem::new( index, @@ -1509,7 +1507,7 @@ where )); } else { computed_child_main_after = - child_main_after.to_px_clamped(parent_main, 0.0, min_main_between, max_main_between); + main_between.to_px_clamped(parent_main, 0.0, min_main_between, max_main_between); } } @@ -1974,12 +1972,17 @@ where let mut alignment = node.alignment(store).unwrap_or_default(); - if matches!(layout_type, LayoutType::Row | LayoutType::Column) - && node.direction(store).unwrap_or_default() == Direction::RightToLeft - { + if matches!(layout_type, LayoutType::Row | LayoutType::Column) && direction == Direction::RightToLeft { alignment = flip_alignment_horizontal(alignment); } + let (horizontal_alignment, vertical_alignment) = alignment_fractions(alignment); + let (main_alignment, cross_alignment) = if layout_type == LayoutType::Row { + (horizontal_alignment, vertical_alignment) + } else { + (vertical_alignment, horizontal_alignment) + }; + // Set size and position of children in the cache. let mut main_pos = padding_main_before; for child in children.iter() { @@ -2013,24 +2016,8 @@ where } PositionType::Relative => { - let (mut child_main_pos, mut child_cross_pos) = match alignment { - Alignment::TopLeft => (0.0, 0.0), - Alignment::TopCenter => (0.0, 0.5), - Alignment::TopRight => (0.0, 1.0), - Alignment::Left => (0.5, 0.0), - Alignment::Center => (0.5, 0.5), - Alignment::Right => (0.5, 1.0), - Alignment::BottomLeft => (1.0, 0.0), - Alignment::BottomCenter => (1.0, 0.5), - Alignment::BottomRight => (1.0, 1.0), - }; - - if layout_type == LayoutType::Row { - std::mem::swap(&mut child_main_pos, &mut child_cross_pos); - } - - child_main_pos *= parent_main - main_sum; - child_cross_pos *= parent_cross - child.cross; + let child_main_pos = main_alignment * (parent_main - main_sum); + let child_cross_pos = cross_alignment * (parent_cross - child.cross); cache.set_rect( child.node, From 9e50ee23fc73bf64c5134633b0de0b161f11524c Mon Sep 17 00:00:00 2001 From: Dr George Atkinson Date: Fri, 21 Aug 2026 19:34:55 +0100 Subject: [PATCH 2/2] Optimize incremental and wrapped layout --- ecs/src/implementations.rs | 17 +++++++- src/cache.rs | 21 ++++++++++ src/layout.rs | 80 +++++++++++++++++++++++++++++++++++--- src/node.rs | 20 ++++++++++ tests/incremental.rs | 42 ++++++++++++++++++++ tests/wrap.rs | 37 ++++++++++++++++++ 6 files changed, 210 insertions(+), 7 deletions(-) diff --git a/ecs/src/implementations.rs b/ecs/src/implementations.rs index 56ae0e63..5ebf186c 100644 --- a/ecs/src/implementations.rs +++ b/ecs/src/implementations.rs @@ -181,6 +181,7 @@ pub struct Rect { pub struct NodeCache { // Computed size and position of nodes. pub rect: SecondaryMap, + pub layout: SecondaryMap, } impl NodeCache { @@ -190,10 +191,12 @@ impl NodeCache { pub fn remove(&mut self, entity: Entity) { self.rect.remove(entity); + self.layout.remove(entity); } pub fn clear(&mut self) { self.rect.clear(); + self.layout.clear(); } pub fn bounds(&self, entity: Entity) -> Option<&Rect> { @@ -244,10 +247,22 @@ impl Cache for NodeCache { 0.0 } + + fn cached_layout(&self, node: &Self::Node) -> Option { + self.layout.get(*node).copied() + } + + fn set_cached_layout(&mut self, node: &Self::Node, layout: CachedLayout) { + self.layout.insert(*node, layout); + } + + fn invalidate_cached_layout(&mut self, node: &Self::Node) { + self.layout.remove(*node); + } } impl Default for NodeCache { fn default() -> Self { - Self { rect: SecondaryMap::new() } + Self { rect: SecondaryMap::new(), layout: SecondaryMap::new() } } } diff --git a/src/cache.rs b/src/cache.rs index cea907ba..62574974 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,5 +1,15 @@ use crate::{LayoutType, Node}; +/// The inputs and result of a completed node layout, used to reuse unchanged subtrees. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CachedLayout { + pub parent_layout_type: LayoutType, + pub parent_main: f32, + pub parent_cross: f32, + pub main: f32, + pub cross: f32, +} + /// The `Cache` is a store which contains the computed size and position of nodes /// after a layout calculation. /// @@ -20,6 +30,17 @@ pub trait Cache { /// Sets the cached position and size of the given node. fn set_bounds(&mut self, node: &Self::Node, posx: f32, posy: f32, width: f32, height: f32); + + /// Returns the inputs and result of the node's most recent valid layout. + fn cached_layout(&self, _node: &Self::Node) -> Option { + None + } + + /// Stores the inputs and result of a completed node layout. + fn set_cached_layout(&mut self, _node: &Self::Node, _layout: CachedLayout) {} + + /// Invalidates a previously cached node layout. + fn invalidate_cached_layout(&mut self, _node: &Self::Node) {} } /// Helper trait for getting/setting node position/size in a direction agnostic way. diff --git a/src/layout.rs b/src/layout.rs index 68e1685b..77ff9edd 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,7 +1,8 @@ use smallvec::SmallVec; use crate::{ - Alignment, Cache, CacheExt, Direction, LayoutType, LayoutWrap, Node, NodeExt, PositionType, Size, Units, Units::*, + Alignment, Cache, CacheExt, CachedLayout, Direction, LayoutType, LayoutWrap, Node, NodeExt, PositionType, Size, + Units, Units::*, }; const DEFAULT_MIN: f32 = -f32::MAX; @@ -826,6 +827,9 @@ where max_main: f32, min_cross: f32, max_cross: f32, + last_layout_main: f32, + last_layout_cross: f32, + has_layout_constraints: bool, } // Phase 1: Compute sizes for all relative children. @@ -856,6 +860,9 @@ where max_main: max_main_px, min_cross: min_cross_px, max_cross: max_cross_px, + last_layout_main: 0.0, + last_layout_cross: 0.0, + has_layout_constraints: false, }); } else { let size = layout(*child, layout_type, avail_main, avail_cross, cache, tree, store, sublayout); @@ -868,6 +875,9 @@ where max_main: max_main_px, min_cross: min_cross_px, max_cross: max_cross_px, + last_layout_main: avail_main, + last_layout_cross: avail_cross, + has_layout_constraints: true, }); } } @@ -986,6 +996,11 @@ where } for item in stretch_items.iter() { + if items[item.index].cross_is_stretch { + items[item.index].main = item.computed; + continue; + } + let size = layout( relative_children[item.index], layout_type, @@ -998,6 +1013,9 @@ where ); items[item.index].main = size.main; items[item.index].cross = size.cross; + items[item.index].last_layout_main = item.computed; + items[item.index].last_layout_cross = avail_cross; + items[item.index].has_layout_constraints = true; } } } @@ -1033,10 +1051,21 @@ where for i in start..end { if items[i].cross_is_stretch { let child = relative_children[i]; + let layout_main = items[i].main; let clamped_cross = target_cross.clamp(items[i].min_cross, items[i].max_cross); - let size = layout(child, layout_type, items[i].main, clamped_cross, cache, tree, store, sublayout); + if items[i].has_layout_constraints + && same_f32(items[i].last_layout_main, layout_main) + && same_f32(items[i].last_layout_cross, clamped_cross) + { + continue; + } + + let size = layout(child, layout_type, layout_main, clamped_cross, cache, tree, store, sublayout); items[i].main = size.main; items[i].cross = size.cross; + items[i].last_layout_main = layout_main; + items[i].last_layout_cross = clamped_cross; + items[i].has_layout_constraints = true; } } }; @@ -1335,6 +1364,18 @@ where N: Node, C: Cache, { + let input_parent_main = parent_main; + let input_parent_cross = parent_cross; + + if let Some(cached) = cache.cached_layout(node) { + if cached.parent_layout_type == parent_layout_type + && same_f32(cached.parent_main, parent_main) + && same_f32(cached.parent_cross, parent_cross) + { + return Size { main: cached.main, cross: cached.cross }; + } + } + // The layout type of the node. Determines the main and cross axes of the children. let layout_type = node.layout_type(store).unwrap_or_default(); @@ -1428,15 +1469,31 @@ where ); if layout_type == LayoutType::Grid { - return layout_grid(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); + let size = layout_grid(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); + cache.set_cached_layout( + node, + CachedLayout { parent_layout_type, parent_main, parent_cross, main: size.main, cross: size.cross }, + ); + return size; } if layout_type == LayoutType::Overlay { - return layout_overlay(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); + let size = + layout_overlay(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); + cache.set_cached_layout( + node, + CachedLayout { parent_layout_type, parent_main, parent_cross, main: size.main, cross: size.cross }, + ); + return size; } if node.wrap(store).unwrap_or_default() == LayoutWrap::Wrap { - return layout_wrap(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); + let size = layout_wrap(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); + cache.set_cached_layout( + node, + CachedLayout { parent_layout_type, parent_main, parent_cross, main: size.main, cross: size.cross }, + ); + return size; } // Determine the parent_main/cross size to pass to the children based on the layout type of the parent and the node. @@ -2033,5 +2090,16 @@ where } // Return the computed size, propagating it back up the tree. - Size { main: computed_main, cross: computed_cross } + let size = Size { main: computed_main, cross: computed_cross }; + cache.set_cached_layout( + node, + CachedLayout { + parent_layout_type, + parent_main: input_parent_main, + parent_cross: input_parent_cross, + main: size.main, + cross: size.cross, + }, + ); + size } diff --git a/src/node.rs b/src/node.rs index 7f175b10..71fd9d21 100644 --- a/src/node.rs +++ b/src/node.rs @@ -48,6 +48,8 @@ pub trait Node: Sized { store: &Self::Store, sublayout: &mut Self::SubLayout<'_>, ) { + self.invalidate_layout_cache(cache, tree); + // Incremental layout: `self` is the node which has been marked as dirty. Rather than // always laying out from `self`, find the best ancestor to restart layout from based on // whether the change can affect the ancestor. Layout is then performed from that ancestor, @@ -95,6 +97,24 @@ pub trait Node: Sized { layout(root, layout_type, parent_main, parent_cross, cache, tree, store, sublayout); } + /// Invalidates cached layout for this subtree and its ancestor path. + /// + /// Call this for every dirty node before collapsing multiple dirty nodes into a shared + /// layout pass. [`layout`](Node::layout) calls it automatically for its input node. + fn invalidate_layout_cache>(&self, cache: &mut C, tree: &Self::Tree) { + let mut pending = vec![self]; + while let Some(node) = pending.pop() { + cache.invalidate_cached_layout(node); + pending.extend(node.children(tree)); + } + + let mut parent = self.parent(tree); + while let Some(node) = parent { + cache.invalidate_cached_layout(node); + parent = node.parent(tree); + } + } + /// Returns a key which can be used to set/get computed layout data from the [`cache`](crate::Cache). fn key(&self) -> Self::CacheKey; diff --git a/tests/incremental.rs b/tests/incremental.rs index 7b57907f..8116f5e0 100644 --- a/tests/incremental.rs +++ b/tests/incremental.rs @@ -1,5 +1,6 @@ use morphorm::*; use morphorm_ecs::*; +use std::{cell::Cell, rc::Rc}; /// Collect the computed bounds of the given entities into a comparable snapshot. fn snapshot(world: &World, entities: &[Entity]) -> Vec> { @@ -286,3 +287,44 @@ fn incremental_on_root_is_full_layout() { assert_eq!(world.cache.bounds(a), Some(&Rect { posx: 0.0, posy: 0.0, width: 100.0, height: 150.0 })); assert_eq!(world.cache.bounds(b), Some(&Rect { posx: 100.0, posy: 0.0, width: 120.0, height: 150.0 })); } + +#[test] +fn incremental_reuses_unchanged_sibling_subtree() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(600.0)); + world.set_height(root, Units::Pixels(600.0)); + world.set_alignment(root, Alignment::TopLeft); + world.set_layout_type(root, LayoutType::Column); + + let changed = world.add(Some(root)); + world.set_width(changed, Units::Pixels(300.0)); + world.set_height(changed, Units::Pixels(100.0)); + + let sibling = world.add(Some(root)); + world.set_width(sibling, Units::Pixels(300.0)); + world.set_height(sibling, Units::Pixels(100.0)); + + let measurements = Rc::new(Cell::new(0)); + let content = world.add(Some(sibling)); + world.set_width(content, Units::Auto); + world.set_height(content, Units::Auto); + world.set_content_size(content, { + let measurements = Rc::clone(&measurements); + move |_, _, _| { + measurements.set(measurements.get() + 1); + (50.0, 20.0) + } + }); + + full_layout(&mut world, root); + let measurements_after_full_layout = measurements.get(); + + world.set_height(changed, Units::Pixels(120.0)); + incremental_layout(&mut world, changed); + + assert_eq!(measurements.get(), measurements_after_full_layout); + assert_eq!(world.cache.bounds(sibling).unwrap().posy, 120.0); + assert_eq!(world.cache.bounds(content).unwrap().width, 50.0); +} diff --git a/tests/wrap.rs b/tests/wrap.rs index 642bd8ca..0e68bb24 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -1,5 +1,42 @@ use morphorm::*; use morphorm_ecs::*; +use std::{cell::Cell, rc::Rc}; + +#[test] +fn wrap_stretch_child_does_not_repeat_identical_sublayout() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(300.0)); + world.set_height(root, Units::Pixels(300.0)); + world.set_layout_type(root, LayoutType::Row); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_alignment(root, Alignment::TopLeft); + + let card = world.add(Some(root)); + world.set_width(card, Units::Stretch(1.0)); + world.set_height(card, Units::Stretch(1.0)); + world.set_min_width(card, Units::Pixels(100.0)); + world.set_min_height(card, Units::Pixels(100.0)); + + let measurements = Rc::new(Cell::new(0)); + let content = world.add(Some(card)); + world.set_width(content, Units::Stretch(1.0)); + world.set_height(content, Units::Auto); + world.set_content_size(content, { + let measurements = Rc::clone(&measurements); + move |_, width, _| { + measurements.set(measurements.get() + 1); + (width.unwrap_or_default(), 20.0) + } + }); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + assert_eq!(measurements.get(), 1); + assert_eq!(world.cache.bounds(card), Some(&Rect { posx: 0.0, posy: 0.0, width: 300.0, height: 100.0 })); + assert_eq!(world.cache.bounds(content), Some(&Rect { posx: 0.0, posy: 0.0, width: 300.0, height: 20.0 })); +} #[test] fn wrap_row_basic() {