diff --git a/ecs/src/implementations.rs b/ecs/src/implementations.rs index 2691d37e..3f07f4e4 100644 --- a/ecs/src/implementations.rs +++ b/ecs/src/implementations.rs @@ -23,6 +23,10 @@ impl Node for Entity { ChildIterator { tree, current_node } } + fn parent<'t>(&'t self, tree: &'t Tree) -> Option<&'t Self> { + tree.get_parent(self) + } + fn visible(&self, store: &Store) -> bool { store.visible.get(*self).copied().unwrap_or(true) } @@ -105,14 +109,6 @@ impl Node for Entity { store.horizontal_gap.get(*self).copied() } - fn vertical_scroll(&self, store: &Store) -> Option { - store.vertical_scroll.get(*self).copied() - } - - fn horizontal_scroll(&self, store: &Store) -> Option { - store.horizontal_scroll.get(*self).copied() - } - fn min_width(&self, store: &Store) -> Option { store.min_width.get(*self).copied() } @@ -129,22 +125,6 @@ impl Node for Entity { store.max_height.get(*self).copied() } - fn border_left(&self, store: &Store) -> Option { - store.border_left.get(*self).copied() - } - - fn border_right(&self, store: &Store) -> Option { - store.border_right.get(*self).copied() - } - - fn border_top(&self, store: &Store) -> Option { - store.border_top.get(*self).copied() - } - - fn border_bottom(&self, store: &Store) -> Option { - store.border_bottom.get(*self).copied() - } - fn min_horizontal_gap(&self, store: &Store) -> Option { store.min_horizontal_gap.get(*self).copied() } diff --git a/ecs/src/store.rs b/ecs/src/store.rs index c92a6fc9..cd04f247 100644 --- a/ecs/src/store.rs +++ b/ecs/src/store.rs @@ -24,9 +24,6 @@ pub struct Store { pub column_span: SecondaryMap, pub row_span: SecondaryMap, - pub vertical_scroll: SecondaryMap, - pub horizontal_scroll: SecondaryMap, - pub left: SecondaryMap, pub right: SecondaryMap, pub top: SecondaryMap, @@ -62,11 +59,6 @@ pub struct Store { pub red: SecondaryMap, pub green: SecondaryMap, pub blue: SecondaryMap, - - pub border_left: SecondaryMap, - pub border_right: SecondaryMap, - pub border_top: SecondaryMap, - pub border_bottom: SecondaryMap, } impl Store { @@ -80,8 +72,6 @@ impl Store { self.right.remove(entity); self.top.remove(entity); self.bottom.remove(entity); - self.vertical_scroll.remove(entity); - self.horizontal_scroll.remove(entity); self.width.remove(entity); self.height.remove(entity); self.min_width.remove(entity); @@ -104,10 +94,6 @@ impl Store { self.red.remove(entity); self.green.remove(entity); self.blue.remove(entity); - self.border_left.remove(entity); - self.border_right.remove(entity); - self.border_top.remove(entity); - self.border_bottom.remove(entity); } pub fn clear(&mut self) { @@ -120,8 +106,6 @@ impl Store { self.right.clear(); self.top.clear(); self.bottom.clear(); - self.vertical_scroll.clear(); - self.horizontal_scroll.clear(); self.width.clear(); self.height.clear(); self.min_width.clear(); @@ -144,9 +128,5 @@ impl Store { self.red.clear(); self.green.clear(); self.blue.clear(); - self.border_left.clear(); - self.border_right.clear(); - self.border_top.clear(); - self.border_bottom.clear(); } } diff --git a/ecs/src/world.rs b/ecs/src/world.rs index c0ab609c..b6104883 100644 --- a/ecs/src/world.rs +++ b/ecs/src/world.rs @@ -189,16 +189,6 @@ impl World { self.store.max_horizontal_gap.insert(entity, value); } - /// Set the desired vertical scroll offset. - pub fn set_vertical_scroll(&mut self, entity: Entity, value: f32) { - self.store.vertical_scroll.insert(entity, value); - } - - /// Set the desired horizontal scroll offset. - pub fn set_horizontal_scroll(&mut self, entity: Entity, value: f32) { - self.store.horizontal_scroll.insert(entity, value); - } - pub fn set_grid_columns(&mut self, entity: Entity, value: Vec) { self.store.grid_columns.insert(entity, value); } @@ -255,11 +245,4 @@ impl World { self.set_width(entity, Units::Stretch(1.0)); self.set_height(entity, Units::Stretch(1.0)); } - - pub fn set_border(&mut self, entity: Entity, width: Units) { - self.store.border_left.insert(entity, width); - self.store.border_right.insert(entity, width); - self.store.border_top.insert(entity, width); - self.store.border_bottom.insert(entity, width); - } } diff --git a/examples/advanced.rs b/examples/advanced.rs index 618d420b..df3cc409 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -43,8 +43,6 @@ pub struct Widget { min_horizontal_gap: Units, max_vertical_gap: Units, max_horizontal_gap: Units, - vertical_scroll: f32, - horizontal_scroll: f32, alignment: Alignment, layout_type: LayoutType, position_type: PositionType, @@ -79,6 +77,10 @@ impl Node for Widget { self.child.iter() } + fn parent<'t>(&'t self, _tree: &'t Self::Tree) -> Option<&'t Self> { + None + } + fn key(&self) -> Self::CacheKey { self.id } @@ -169,34 +171,10 @@ impl Node for Widget { Some(self.horizontal_gap) } - fn border_left(&self, _store: &Self::Store) -> Option { - Some(Units::Pixels(0.0)) - } - - fn border_right(&self, _store: &Self::Store) -> Option { - Some(Units::Pixels(0.0)) - } - - fn border_top(&self, _store: &Self::Store) -> Option { - Some(Units::Pixels(0.0)) - } - - fn border_bottom(&self, _store: &Self::Store) -> Option { - Some(Units::Pixels(0.0)) - } - fn alignment(&self, _store: &Self::Store) -> Option { Some(self.alignment) } - fn vertical_scroll(&self, _store: &Self::Store) -> Option { - Some(self.vertical_scroll) - } - - fn horizontal_scroll(&self, _store: &Self::Store) -> Option { - Some(self.horizontal_scroll) - } - fn min_vertical_gap(&self, _store: &Self::Store) -> Option { Some(self.min_vertical_gap) } diff --git a/src/layout.rs b/src/layout.rs index 6f155ef7..1948be48 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -6,7 +6,6 @@ use crate::{ const DEFAULT_MIN: f32 = -f32::MAX; const DEFAULT_MAX: f32 = f32::MAX; -const DEFAULT_BORDER_WIDTH: f32 = 0.0; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ItemType { @@ -148,11 +147,6 @@ where let mut min_height = node.min_height(store).unwrap_or(Pixels(0.0)).to_px(contain_height, DEFAULT_MIN); let mut max_height = node.max_height(store).unwrap_or(Pixels(f32::MAX)).to_px(contain_height, DEFAULT_MAX); - let border_left = node.border_left(store).unwrap_or_default().to_px(contain_width, DEFAULT_BORDER_WIDTH); - let border_right = node.border_right(store).unwrap_or_default().to_px(contain_width, DEFAULT_BORDER_WIDTH); - let border_top = node.border_top(store).unwrap_or_default().to_px(contain_height, DEFAULT_BORDER_WIDTH); - let border_bottom = node.border_bottom(store).unwrap_or_default().to_px(contain_height, DEFAULT_BORDER_WIDTH); - let padding_left = node.padding_left(store).unwrap_or_default().to_px(contain_width, 0.0); let padding_right = node.padding_right(store).unwrap_or_default().to_px(contain_width, 0.0); let padding_top = node.padding_top(store).unwrap_or_default().to_px(contain_height, 0.0); @@ -181,8 +175,8 @@ where children.clear(); // Relative children are laid out against the parent content box. - let available_width = computed_width - padding_left - padding_right - border_left - border_right; - let available_height = computed_height - padding_top - padding_bottom - border_top - border_bottom; + let available_width = computed_width - padding_left - padding_right; + let available_height = computed_height - padding_top - padding_bottom; for child in relative_children.iter().copied() { let child_width = child.width(store).unwrap_or(Stretch(1.0)); @@ -239,19 +233,19 @@ where // Auto-size in overlay is based on max extents (not sums), because // children can overlap and are independently aligned in the same box. if width.is_auto() || node.min_width(store).unwrap_or(Pixels(0.0)).is_auto() { - min_width = max_child_width + padding_left + padding_right + border_left + border_right; + min_width = max_child_width + padding_left + padding_right; } if node.max_width(store).unwrap_or(Pixels(f32::MAX)).is_auto() && max_child_width != 0.0 { - max_width = max_child_width + padding_left + padding_right + border_left + border_right; + max_width = max_child_width + padding_left + padding_right; } if height.is_auto() || node.min_height(store).unwrap_or(Pixels(0.0)).is_auto() { - min_height = max_child_height + padding_top + padding_bottom + border_top + border_bottom; + min_height = max_child_height + padding_top + padding_bottom; } if node.max_height(store).unwrap_or(Pixels(f32::MAX)).is_auto() && max_child_height != 0.0 { - max_height = max_child_height + padding_top + padding_bottom + border_top + border_bottom; + max_height = max_child_height + padding_top + padding_bottom; } let next_width = computed_width.max(min_width).min(max_width); @@ -270,8 +264,8 @@ where computed_height = computed_height.max(min_height).min(max_height); // Relative children are positioned inside the padded content box. - let available_width = computed_width - padding_left - padding_right - border_left - border_right; - let available_height = computed_height - padding_top - padding_bottom - border_top - border_bottom; + let available_width = computed_width - padding_left - padding_right; + let available_height = computed_height - padding_top - padding_bottom; let mut alignment = node.alignment(store).unwrap_or_default(); if node.direction(store).unwrap_or_default() == Direction::RightToLeft { @@ -282,32 +276,23 @@ where let (align_x, align_y) = alignment_fractions(alignment); for child in &children { - let mut child_posx = align_x * (available_width - child.main); - let mut child_posy = align_y * (available_height - child.cross); - - // Parent scroll offsets can override alignment-derived positions. - if let Some(scroll_x) = node.horizontal_scroll(store) { - child_posx = scroll_x; - } - - if let Some(scroll_y) = node.vertical_scroll(store) { - child_posy = scroll_y; - } + let child_posx = align_x * (available_width - child.main); + let child_posy = align_y * (available_height - child.cross); cache.set_rect( child.node, LayoutType::Overlay, - child_posx + padding_left + border_left, - child_posy + padding_top + border_top, + child_posx + padding_left, + child_posy + padding_top, child.main, child.cross, ); } // Absolute children are sized in the same box model used by stack/wrap: - // padding box (content + padding), excluding border. - let abs_width = computed_width - border_left - border_right; - let abs_height = computed_height - border_top - border_bottom; + // padding box (content + padding). + let abs_width = computed_width; + let abs_height = computed_height; // Mirror the stack layout's RTL semantics: under RightToLeft, left/right are // swapped so that `left` becomes the trailing edge and `right` the leading edge, @@ -358,14 +343,7 @@ where child_size.cross, ); - cache.set_rect( - child, - LayoutType::Overlay, - child_posx + border_left, - child_posy + border_top, - child_size.main, - child_size.cross, - ); + cache.set_rect(child, LayoutType::Overlay, child_posx, child_posy, child_size.main, child_size.cross); } // Return in caller's axis orientation (main/cross abstraction). @@ -653,20 +631,14 @@ where let (parent_main, parent_cross) = if parent_layout_type == layout_type { (parent_main, parent_cross) } else { (parent_cross, parent_main) }; - let border_main_before = node.border_main_before(store, layout_type).to_px(parent_main, DEFAULT_BORDER_WIDTH); - let border_main_after = node.border_main_after(store, layout_type).to_px(parent_main, DEFAULT_BORDER_WIDTH); - let border_cross_before = node.border_cross_before(store, layout_type).to_px(parent_cross, DEFAULT_BORDER_WIDTH); - let border_cross_after = node.border_cross_after(store, layout_type).to_px(parent_cross, DEFAULT_BORDER_WIDTH); - let padding_main_before = node.padding_main_before(store, layout_type).to_px(parent_main, 0.0); let padding_main_after = node.padding_main_after(store, layout_type).to_px(parent_main, 0.0); 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); - // Available space for children after subtracting padding and border. - let avail_main = parent_main - padding_main_before - padding_main_after - border_main_before - border_main_after; - let avail_cross = - parent_cross - padding_cross_before - padding_cross_after - border_cross_before - border_cross_after; + // Available space for children after subtracting padding. + let avail_main = parent_main - padding_main_before - padding_main_after; + let avail_cross = parent_cross - padding_cross_before - padding_cross_after; // Gap between items within a line (on the main axis). let min_main_between = node.min_main_between(store, layout_type); @@ -938,8 +910,7 @@ where let cross_units = node.cross(store, layout_type); let final_cross = if cross_units.is_auto() || parent_cross == 0.0 { - let raw = - total_content_cross + padding_cross_before + padding_cross_after + border_cross_before + border_cross_after; + let raw = total_content_cross + padding_cross_before + padding_cross_after; let min_c = node.min_cross(store, layout_type).to_px(0.0, DEFAULT_MIN); let max_c = node.max_cross(store, layout_type).to_px(0.0, DEFAULT_MAX); raw.max(min_c).min(max_c) @@ -960,7 +931,7 @@ where sum + (line.len().saturating_sub(1)) as f32 * item_gap_px }) .fold(0.0f32, f32::max); - let raw = raw + padding_main_before + padding_main_after + border_main_before + border_main_after; + let raw = raw + padding_main_before + padding_main_after; let min_m = node.min_main(store, layout_type).to_px(0.0, DEFAULT_MIN); let max_m = node.max_main(store, layout_type).to_px(0.0, DEFAULT_MAX); raw.max(min_m).min(max_m) @@ -970,8 +941,8 @@ where // Phase 7: Lay out absolute children against the container bounds. // Absolute children are sized against the padding box (content box + padding, excluding border). - let abs_avail_main = final_main - border_main_before - border_main_after; - let abs_avail_cross = final_cross - border_cross_before - border_cross_after; + let abs_avail_main = final_main; + let abs_avail_cross = final_cross; let abs_children = node .children(tree) @@ -1040,7 +1011,7 @@ where std::mem::swap(&mut main_align_frac, &mut cross_align_frac); } - let mut cross_cursor = padding_cross_before + border_cross_before; + let mut cross_cursor = padding_cross_before; for (line_idx, line) in lines.iter().enumerate() { let start = line.start; @@ -1057,7 +1028,7 @@ where if layout_type == LayoutType::Row && node.direction(store).unwrap_or_default() == Direction::RightToLeft { // RTL positioning: place items in reverse order within each wrapped line. // Alignment is flipped above so TopLeft maps to TopRight semantics. - let mut main_cursor = padding_main_before + border_main_before + main_align_frac * free_main; + let mut main_cursor = padding_main_before + main_align_frac * free_main; for (item_idx, i) in (start..end).rev().enumerate() { let item = &items[i]; @@ -1080,7 +1051,7 @@ where } } else { // LTR positioning: items are positioned left-to-right within the line - let mut main_cursor = padding_main_before + border_main_before + main_align_frac * free_main; + let mut main_cursor = padding_main_before + main_align_frac * free_main; for i in start..end { let item = &items[i]; @@ -1163,14 +1134,7 @@ where (Auto, Auto) => 0.0, }; - cache.set_rect( - abs_child.node, - layout_type, - child_main_pos + border_main_before, - child_cross_pos + border_cross_before, - abs_child.main, - abs_child.cross, - ); + cache.set_rect(abs_child.node, layout_type, child_main_pos, child_cross_pos, abs_child.main, abs_child.cross); } if parent_layout_type == layout_type { @@ -1255,15 +1219,6 @@ where Auto => 0.0, }; - let border_main_before = - node.border_main_before(store, parent_layout_type).to_px(computed_main, DEFAULT_BORDER_WIDTH); - let border_main_after = - node.border_main_after(store, parent_layout_type).to_px(computed_main, DEFAULT_BORDER_WIDTH); - let border_cross_before = - node.border_cross_before(store, parent_layout_type).to_px(computed_cross, DEFAULT_BORDER_WIDTH); - let border_cross_after = - node.border_cross_after(store, parent_layout_type).to_px(computed_cross, DEFAULT_BORDER_WIDTH); - // Classify visible children once to avoid repeated tree traversals. let mut relative_children = SmallVec::<[&N; 32]>::new(); let mut absolute_children = SmallVec::<[&N; 8]>::new(); @@ -1345,8 +1300,8 @@ where 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 - border_main_before - border_main_after; - parent_cross = parent_cross - padding_cross_before - padding_cross_after - border_cross_before - border_cross_after; + 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; @@ -1446,39 +1401,33 @@ where if num_parent_directed_children != 0 { if main.is_auto() || node.min_main(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type { - min_main = main_sum + border_main_before + border_main_after + padding_main_before + padding_main_after; + min_main = main_sum + padding_main_before + padding_main_after; } else { - min_main = - cross_max + border_main_before + border_main_after + padding_cross_before + padding_cross_after; + min_main = cross_max + padding_cross_before + padding_cross_after; } } if node.max_main(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type && main_sum != 0.0 { - max_main = main_sum + border_main_before + border_main_after + padding_main_before + padding_main_after; + max_main = main_sum + padding_main_before + padding_main_after; } else if cross_max != 0.0 { - max_main = - cross_max + border_main_before + border_main_after + padding_cross_before + padding_cross_after; + max_main = cross_max + padding_cross_before + padding_cross_after; } } if cross.is_auto() || node.min_cross(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type { - min_cross = - cross_max + border_cross_before + border_cross_after + padding_cross_before + padding_cross_after; + min_cross = cross_max + padding_cross_before + padding_cross_after; } else { - min_cross = - main_sum + border_cross_before + border_cross_after + padding_main_before + padding_main_after; + min_cross = main_sum + padding_main_before + padding_main_after; } } if node.max_cross(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type && cross_max != 0.0 { - max_cross = - cross_max + border_cross_before + border_cross_after + padding_cross_before + padding_cross_after; + max_cross = cross_max + padding_cross_before + padding_cross_after; } else if main_sum != 0.0 { - max_cross = - main_sum + border_cross_before + border_cross_after + padding_main_before + padding_main_after; + max_cross = main_sum + padding_main_before + padding_main_after; } } } @@ -1492,8 +1441,8 @@ where (computed_cross, computed_main) }; - parent_main = parent_main - padding_main_before - padding_main_after - border_main_before - border_main_after; - parent_cross = parent_cross - padding_cross_before - padding_cross_after - border_cross_before - border_cross_after; + parent_main = parent_main - padding_main_before - padding_main_after; + parent_cross = parent_cross - padding_cross_before - padding_cross_after; // Compute stretch size on the cross-axis for relative children. for child in children @@ -1534,39 +1483,33 @@ where if num_parent_directed_children != 0 { if main.is_auto() || node.min_main(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type { - min_main = main_sum + border_main_before + border_main_after + padding_main_before + padding_main_after; + min_main = main_sum + padding_main_before + padding_main_after; } else { - min_main = - cross_max + border_main_before + border_main_after + padding_cross_before + padding_cross_after; + min_main = cross_max + padding_cross_before + padding_cross_after; } } if node.max_main(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type && main_sum != 0.0 { - max_main = main_sum + border_main_before + border_main_after + padding_main_before + padding_main_after; + max_main = main_sum + padding_main_before + padding_main_after; } else if cross_max != 0.0 { - max_main = - cross_max + border_main_before + border_main_after + padding_cross_before + padding_cross_after; + max_main = cross_max + padding_cross_before + padding_cross_after; } } if cross.is_auto() || node.min_cross(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type { - min_cross = - cross_max + border_cross_before + border_cross_after + padding_cross_before + padding_cross_after; + min_cross = cross_max + padding_cross_before + padding_cross_after; } else { - min_cross = - main_sum + border_cross_before + border_cross_after + padding_main_before + padding_main_after; + min_cross = main_sum + padding_main_before + padding_main_after; } } if node.max_cross(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type && cross_max != 0.0 { - max_cross = - cross_max + border_cross_before + border_cross_after + padding_cross_before + padding_cross_after; + max_cross = cross_max + padding_cross_before + padding_cross_after; } else if main_sum != 0.0 { - max_cross = - main_sum + border_cross_before + border_cross_after + padding_main_before + padding_main_after; + max_cross = main_sum + padding_main_before + padding_main_after; } } } @@ -1692,39 +1635,33 @@ where if num_parent_directed_children != 0 { if main.is_auto() || node.min_main(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type { - min_main = main_sum + border_main_before + border_main_after + padding_main_before + padding_main_after; + min_main = main_sum + padding_main_before + padding_main_after; } else { - min_main = - cross_max + border_main_before + border_main_after + padding_cross_before + padding_cross_after; + min_main = cross_max + padding_cross_before + padding_cross_after; } } if node.max_main(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type && main_sum != 0.0 { - max_main = main_sum + border_main_before + border_main_after + padding_main_before + padding_main_after; + max_main = main_sum + padding_main_before + padding_main_after; } else if cross_max != 0.0 { - max_main = - cross_max + border_main_before + border_main_after + padding_cross_before + padding_cross_after; + max_main = cross_max + padding_cross_before + padding_cross_after; } } if cross.is_auto() || node.min_cross(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type { - min_cross = - cross_max + border_cross_before + border_cross_after + padding_cross_before + padding_cross_after; + min_cross = cross_max + padding_cross_before + padding_cross_after; } else { - min_cross = - main_sum + border_cross_before + border_cross_after + padding_main_before + padding_main_after; + min_cross = main_sum + padding_main_before + padding_main_after; } } if node.max_cross(store, parent_layout_type).is_auto() { if parent_layout_type == layout_type && cross_max != 0.0 { - max_cross = - cross_max + border_cross_before + border_cross_after + padding_cross_before + padding_cross_after; + max_cross = cross_max + padding_cross_before + padding_cross_after; } else if main_sum != 0.0 { - max_cross = - main_sum + border_cross_before + border_cross_after + padding_main_before + padding_main_after; + max_cross = main_sum + padding_main_before + padding_main_after; } } } @@ -1738,8 +1675,8 @@ where (computed_cross, computed_main) }; - parent_main = parent_main - padding_main_before - padding_main_after - border_main_before - border_main_after; - parent_cross = parent_cross - padding_cross_before - padding_cross_after - border_cross_before - border_cross_after; + parent_main = parent_main - padding_main_before - padding_main_after; + parent_cross = parent_cross - padding_cross_before - padding_cross_after; for child in children .iter_mut() @@ -1848,7 +1785,7 @@ where } // Set size and position of children in the cache. - let mut main_pos = padding_main_before + border_main_before; + let mut main_pos = padding_main_before; for child in children.iter() { let child_position = child.node.position_type(store).unwrap_or_default(); @@ -1876,14 +1813,7 @@ where let child_cross_pos = absolute_axis_position(child_cross_before, child_cross_after, parent_cross, child.cross); - cache.set_rect( - child.node, - layout_type, - child_main_pos + border_main_before, - child_cross_pos + border_cross_before, - child.main, - child.cross, - ); + cache.set_rect(child.node, layout_type, child_main_pos, child_cross_pos, child.main, child.cross); } PositionType::Relative => { @@ -1906,19 +1836,11 @@ where child_main_pos *= parent_main - main_sum; child_cross_pos *= parent_cross - child.cross; - if let Some(main_scroll) = node.main_scroll(store, layout_type) { - child_main_pos = main_scroll - } - - if let Some(cross_scroll) = node.cross_scroll(store, layout_type) { - child_cross_pos = cross_scroll - } - cache.set_rect( child.node, layout_type, main_pos + child_main_pos, - child_cross_pos + padding_cross_before + border_cross_before, + child_cross_pos + padding_cross_before, child.main, child.cross, ); diff --git a/src/node.rs b/src/node.rs index 614e1439..ff4313bc 100644 --- a/src/node.rs +++ b/src/node.rs @@ -27,10 +27,12 @@ pub trait Node: Sized { /// then be used to size an `Auto` layout node using content size. type SubLayout<'a>; - /// Performs layout on the given node returning its computed size. + /// Performs layout on the given node. /// - /// The algorithm recurses down the tree, in depth-first order, and performs - /// layout on every node starting from the input `node`. + /// The algorithm recurses down the tree in depth-first order and performs + /// layout on every node in the restarted subtree. During incremental relayout, + /// the input node is treated as dirty and layout may restart from an ancestor. + /// Calling this on the tree root still performs a full layout pass. /// /// # Arguments /// @@ -45,20 +47,52 @@ pub trait Node: Sized { tree: &Self::Tree, store: &Self::Store, sublayout: &mut Self::SubLayout<'_>, - ) -> Size { - let width = self.width(store).unwrap_or(Units::Pixels(0.0)).to_px(0.0, 0.0); - let height = self.height(store).unwrap_or(Units::Pixels(0.0)).to_px(0.0, 0.0); + ) { + // 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, + // recursing through all of its descendants (no unchanged descendants are skipped). + // + // When `self` is the root of the tree the returned ancestor is `self`, so calling + // `root.layout(..)` performs a full layout pass exactly as before. + let mut root = self.find_relayout_root(tree, store); + + // If incremental relayout restarts from a non-root ancestor before any prior layout pass, + // some cache implementations may still hold default 0x0 bounds. In that case, fall back + // to the tree root so constraints are computed from source properties instead of stale + // cache values. + if root.parent(tree).is_some() && cache.width(root) == 0.0 && cache.height(root) == 0.0 { + while let Some(parent) = root.parent(tree) { + root = parent; + } + } + + // Determine the size of the restart root. + // + // A non-root ancestor keeps its previously computed (cached) size. By construction this + // ancestor is sized in `Pixels` or `Stretch`, so its size is stable under the change and is + // reproduced exactly by the layout algorithm when fed the cached size. + // + // The tree root (which has no parent) is sized from its own properties, preserving the + // behavior of a full layout pass and working on the first pass when the cache is empty. + let (width, height) = if root.parent(tree).is_some() { + (cache.width(root), cache.height(root)) + } else { + let width = root.width(store).unwrap_or(Units::Pixels(0.0)).to_px(0.0, 0.0); + let height = root.height(store).unwrap_or(Units::Pixels(0.0)).to_px(0.0, 0.0); + (width, height) + }; - cache.set_bounds(self, cache.posx(self), cache.posy(self), width, height); + cache.set_bounds(root, cache.posx(root), cache.posy(root), width, height); // Use the node's layout type instead of hardcoding Column - let layout_type = self.layout_type(store).unwrap_or_default(); + let layout_type = root.layout_type(store).unwrap_or_default(); let (parent_main, parent_cross) = match layout_type { LayoutType::Row | LayoutType::Overlay | LayoutType::Grid => (width, height), // Row/Overlay/Grid: main=width, cross=height LayoutType::Column => (height, width), // Column: main=height, cross=width }; - layout(self, layout_type, parent_main, parent_cross, cache, tree, store, sublayout) + layout(root, layout_type, parent_main, parent_cross, cache, tree, store, sublayout); } /// Returns a key which can be used to set/get computed layout data from the [`cache`](crate::Cache). @@ -67,6 +101,9 @@ pub trait Node: Sized { /// Returns an iterator over the children of the node. fn children<'t>(&'t self, tree: &'t Self::Tree) -> Self::ChildIter<'t>; + /// Returns an optional reference to the parent of the node. + fn parent<'t>(&'t self, tree: &'t Self::Tree) -> Option<&'t Self>; + /// Returns a boolean representing whether the node is visible to layout. fn visible(&self, store: &Self::Store) -> bool; @@ -161,24 +198,6 @@ pub trait Node: Sized { /// Returns the maximum height of the node. fn max_height(&self, store: &Self::Store) -> Option; - /// Returns the left-side border width of the node. - fn border_left(&self, store: &Self::Store) -> Option; - - /// Returns the right-side border width of the node. - fn border_right(&self, store: &Self::Store) -> Option; - - /// Returns the top-side border width of the node. - fn border_top(&self, store: &Self::Store) -> Option; - - /// Returns the bottom-side border width of the node. - fn border_bottom(&self, store: &Self::Store) -> Option; - - /// Returns the vertical scroll offset of the node. - fn vertical_scroll(&self, store: &Self::Store) -> Option; - - /// Returns the horizontal scroll offset of the node. - fn horizontal_scroll(&self, store: &Self::Store) -> Option; - fn grid_columns(&self, store: &Self::Store) -> Option>; fn grid_rows(&self, store: &Self::Store) -> Option>; @@ -328,22 +347,6 @@ pub(crate) trait NodeExt: Node { parent_layout_type.select_unwrap(store, |store| self.vertical_gap(store), |store| self.horizontal_gap(store)) } - fn border_main_before(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { - parent_layout_type.select_unwrap(store, |store| self.border_left(store), |store| self.border_top(store)) - } - - fn border_main_after(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { - parent_layout_type.select_unwrap(store, |store| self.border_right(store), |store| self.border_bottom(store)) - } - - fn border_cross_before(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { - parent_layout_type.select_unwrap(store, |store| self.border_top(store), |store| self.border_left(store)) - } - - fn border_cross_after(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { - parent_layout_type.select_unwrap(store, |store| self.border_bottom(store), |store| self.border_right(store)) - } - fn content_sizing( &self, store: &Self::Store, @@ -363,12 +366,85 @@ pub(crate) trait NodeExt: Node { } } - fn cross_scroll(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Option { - parent_layout_type.select(store, |store| self.vertical_scroll(store), |store| self.horizontal_scroll(store)) + /// Returns whether the node can be used as a restart point for incremental layout. + /// + /// A node is a valid restart point when its size is both *stable* under changes to its + /// descendants and *reproducible* from its cached size: + /// - [`Units::Pixels`] is fixed regardless of children or parent. + /// - [`Units::Stretch`] is determined by the parent's allocation (which is unaffected by the + /// node's own descendants) and is reproduced by the layout algorithm when fed the cached size. + /// + /// [`Units::Auto`] sizes to fit children (so a descendant change can change the node's size, + /// affecting its parent) and [`Units::Percentage`] cannot be reproduced from the cached size + /// alone, so neither is a valid restart point. + /// + /// A missing width/height is treated as [`Units::Stretch`], matching the default used by the + /// layout algorithm ([`main`](NodeExt::main)/[`cross`](NodeExt::cross)). + fn is_restartable(&self, store: &Self::Store) -> bool { + fn stable(units: Units) -> bool { + units.is_pixels() || units.is_stretch() + } + + let width = self.width(store).unwrap_or(Units::Stretch(1.0)); + let height = self.height(store).unwrap_or(Units::Stretch(1.0)); + let min_width = self.min_width(store).unwrap_or(Units::Pixels(0.0)); + let max_width = self.max_width(store).unwrap_or(Units::Pixels(f32::MAX)); + let min_height = self.min_height(store).unwrap_or(Units::Pixels(0.0)); + let max_height = self.max_height(store).unwrap_or(Units::Pixels(f32::MAX)); + + stable(width) + && stable(height) + && stable(min_width) + && stable(max_width) + && stable(min_height) + && stable(max_height) } - fn main_scroll(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Option { - parent_layout_type.select(store, |store| self.horizontal_scroll(store), |store| self.vertical_scroll(store)) + /// Finds the best ancestor to restart layout from for a node which has been marked as dirty. + /// + /// Layout must restart from at least the node's parent, since the parent determines the size and + /// position of the node relative to its siblings. From there the search walks up the tree while + /// each ancestor's size could still affect its own parent (i.e. it is not + /// [restartable](NodeExt::is_restartable)), stopping at the first restartable ancestor or at the + /// root of the tree. + /// + /// An [absolutely-positioned](PositionType::Absolute) ancestor is taken out of its parent's flow, + /// so it does not affect the parent's size. However, its own position can depend on its size + /// (e.g. right/bottom anchoring), so relayout should restart from that absolute ancestor's parent + /// to recompute the absolute position. + /// + /// If the node is the root of the tree it is returned unchanged, so a call on the root performs + /// a full layout pass. + fn find_relayout_root<'t>(&'t self, tree: &'t Self::Tree, store: &Self::Store) -> &'t Self { + // Always restart from at least the parent of the dirty node. + let mut root = match self.parent(tree) { + Some(parent) => parent, + None => return self, + }; + + // If the dirty node is itself absolutely positioned it is out of its parent's flow, so a + // change to it (including being added or removed) cannot alter the parent's size. Restart + // from the parent — which positions the absolute node relative to its cached bounds — + // without walking any further up the tree. + if self.position_type(store).unwrap_or_default() == PositionType::Absolute { + return root; + } + + // Walk up while the current ancestor's size could affect its own parent. + while let Some(parent) = root.parent(tree) { + // Absolutely-positioned nodes don't affect their parent's size, but their own + // position can depend on their size (e.g. right/bottom anchoring). Restart at the + // parent so absolute positioning is recomputed. + if root.position_type(store).unwrap_or_default() == PositionType::Absolute { + return parent; + } + if root.is_restartable(store) { + break; + } + root = parent; + } + + root } } diff --git a/src/types.rs b/src/types.rs index bb3e2de2..ad2bfcb0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -13,6 +13,7 @@ pub enum LayoutType { } impl LayoutType { + #[allow(dead_code)] // Helper function for selecting between optional values depending on the layout type. pub(crate) fn select( &self, diff --git a/tests/border.rs b/tests/border.rs deleted file mode 100644 index d4ccb3b7..00000000 --- a/tests/border.rs +++ /dev/null @@ -1,91 +0,0 @@ -use morphorm::*; -use morphorm_ecs::*; - -#[test] -fn border_pixels_stretch_child() { - 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_border(root, Units::Pixels(50.0)); - - let node = world.add(Some(root)); - world.set_width(node, Units::Stretch(1.0)); - world.set_height(node, Units::Stretch(1.0)); - - root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); - - assert_eq!(world.cache.bounds(node), Some(&Rect { posx: 50.0, posy: 50.0, width: 500.0, height: 500.0 })); -} - -#[test] -fn border_pixels_stretch_child2() { - 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_border(root, Units::Pixels(50.0)); - - let node1 = world.add(Some(root)); - world.set_width(node1, Units::Stretch(1.0)); - world.set_height(node1, Units::Stretch(1.0)); - - let node2 = world.add(Some(root)); - world.set_width(node2, Units::Stretch(1.0)); - world.set_height(node2, Units::Stretch(1.0)); - - root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); - - assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 50.0, posy: 50.0, width: 500.0, height: 250.0 })); - assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 50.0, posy: 300.0, width: 500.0, height: 250.0 })); - - world.set_layout_type(root, LayoutType::Row); - - root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); - - assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 50.0, posy: 50.0, width: 250.0, height: 500.0 })); - assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 300.0, posy: 50.0, width: 250.0, height: 500.0 })); -} - -#[test] -fn border_percentage_stretch_child() { - 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_border(root, Units::Percentage(10.0)); - - let node = world.add(Some(root)); - world.set_width(node, Units::Stretch(1.0)); - world.set_height(node, Units::Stretch(1.0)); - - root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); - - assert_eq!(world.cache.bounds(node), Some(&Rect { posx: 60.0, posy: 60.0, width: 480.0, height: 480.0 })); -} - -#[test] -fn border_parent_auto() { - 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); - - let node = world.add(Some(root)); - world.set_width(node, Units::Auto); - world.set_height(node, Units::Auto); - world.set_border(node, Units::Pixels(10.0)); - - let child = world.add(Some(node)); - world.set_width(child, Units::Pixels(10.0)); - world.set_height(child, Units::Pixels(10.0)); - - root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); - - assert_eq!(world.cache.bounds(node), Some(&Rect { posx: 0.0, posy: 0.0, width: 30.0, height: 30.0 })); -} diff --git a/tests/incremental.rs b/tests/incremental.rs new file mode 100644 index 00000000..7b57907f --- /dev/null +++ b/tests/incremental.rs @@ -0,0 +1,288 @@ +use morphorm::*; +use morphorm_ecs::*; + +/// Collect the computed bounds of the given entities into a comparable snapshot. +fn snapshot(world: &World, entities: &[Entity]) -> Vec> { + entities.iter().map(|entity| world.cache.bounds(*entity).copied()).collect() +} + +/// Perform a full layout pass from the root. +fn full_layout(world: &mut World, root: Entity) { + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); +} + +/// Perform an incremental layout pass starting from the given (dirty) node. +fn incremental_layout(world: &mut World, node: Entity) { + node.layout(&mut world.cache, &world.tree, &world.store, &mut ()); +} + +/// A change under a fixed (pixels) parent restarts layout at the parent and repositions siblings, +/// producing the same result as a full layout pass. +#[test] +fn incremental_matches_full_under_fixed_parent() { + 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 parent = world.add(Some(root)); + world.set_width(parent, Units::Pixels(400.0)); + world.set_height(parent, Units::Pixels(400.0)); + world.set_layout_type(parent, LayoutType::Column); + + let a = world.add(Some(parent)); + world.set_width(a, Units::Pixels(100.0)); + world.set_height(a, Units::Pixels(100.0)); + + let b = world.add(Some(parent)); + world.set_width(b, Units::Pixels(100.0)); + world.set_height(b, Units::Pixels(100.0)); + + full_layout(&mut world, root); + + // `a` grows; its parent is a fixed-pixels node, so layout restarts at the parent. + world.set_height(a, Units::Pixels(250.0)); + incremental_layout(&mut world, a); + let incremental = snapshot(&world, &[root, parent, a, b]); + + // Ground truth: a full layout pass must produce the same result. + full_layout(&mut world, root); + let full = snapshot(&world, &[root, parent, a, b]); + + assert_eq!(incremental, full); + // `b` should have been pushed down by `a` growing. + assert_eq!(world.cache.bounds(b).unwrap().posy, 250.0); +} + +/// A change under an `Auto`-sized parent bubbles up to the nearest fixed ancestor, because the +/// auto parent resizes to fit its children and therefore affects its own parent. +#[test] +fn incremental_bubbles_through_auto_parent() { + 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 grandparent = world.add(Some(root)); + world.set_width(grandparent, Units::Pixels(500.0)); + world.set_height(grandparent, Units::Pixels(500.0)); + world.set_layout_type(grandparent, LayoutType::Column); + + // Auto height -> fits its children on the main (vertical) axis. + let parent = world.add(Some(grandparent)); + world.set_width(parent, Units::Pixels(300.0)); + world.set_height(parent, Units::Auto); + world.set_layout_type(parent, LayoutType::Column); + + // Sibling positioned below `parent` within `grandparent`. + let sibling = world.add(Some(grandparent)); + world.set_width(sibling, Units::Pixels(50.0)); + world.set_height(sibling, Units::Pixels(50.0)); + + let child = world.add(Some(parent)); + world.set_width(child, Units::Pixels(100.0)); + world.set_height(child, Units::Pixels(100.0)); + + full_layout(&mut world, root); + // Auto parent initially fits the 100px child, so the sibling sits at y = 100. + assert_eq!(world.cache.bounds(sibling).unwrap().posy, 100.0); + + // Growing the child grows the auto parent, which shifts the sibling: must restart high enough. + world.set_height(child, Units::Pixels(220.0)); + incremental_layout(&mut world, child); + let incremental = snapshot(&world, &[grandparent, parent, sibling, child]); + + full_layout(&mut world, root); + let full = snapshot(&world, &[grandparent, parent, sibling, child]); + + assert_eq!(incremental, full); + assert_eq!(world.cache.bounds(sibling).unwrap().posy, 220.0); +} + +/// A `Stretch`-sized ancestor is a valid restart point: its size is determined by the parent's +/// allocation (unaffected by its own descendants) and is reproduced from the cached size. +#[test] +fn incremental_restarts_at_stretch_ancestor() { + 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::Row); + + // Stretch on both axes -> fills the root. + let stretch = world.add(Some(root)); + world.set_width(stretch, Units::Stretch(1.0)); + world.set_height(stretch, Units::Stretch(1.0)); + world.set_layout_type(stretch, LayoutType::Column); + + let child = world.add(Some(stretch)); + world.set_width(child, Units::Pixels(100.0)); + world.set_height(child, Units::Pixels(100.0)); + + let child2 = world.add(Some(stretch)); + world.set_width(child2, Units::Pixels(100.0)); + world.set_height(child2, Units::Pixels(100.0)); + + full_layout(&mut world, root); + assert_eq!(world.cache.bounds(child2).unwrap().posy, 100.0); + + world.set_height(child, Units::Pixels(180.0)); + incremental_layout(&mut world, child); + let incremental = snapshot(&world, &[root, stretch, child, child2]); + + full_layout(&mut world, root); + let full = snapshot(&world, &[root, stretch, child, child2]); + + assert_eq!(incremental, full); + assert_eq!(world.cache.bounds(child2).unwrap().posy, 180.0); +} + +/// Calling incremental layout before any full/root pass should still produce valid results. +/// Regression: non-root restart roots must not seed from uninitialized cached 0x0 bounds. +#[test] +fn incremental_before_first_full_layout_matches_full() { + 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 parent = world.add(Some(root)); + world.set_width(parent, Units::Pixels(400.0)); + world.set_height(parent, Units::Pixels(400.0)); + world.set_layout_type(parent, LayoutType::Column); + + let a = world.add(Some(parent)); + world.set_width(a, Units::Pixels(100.0)); + world.set_height(a, Units::Pixels(100.0)); + + let b = world.add(Some(parent)); + world.set_width(b, Units::Pixels(100.0)); + world.set_height(b, Units::Pixels(100.0)); + + // No prior full/root layout here: run incremental directly from a non-root node. + incremental_layout(&mut world, a); + let incremental = snapshot(&world, &[root, parent, a, b]); + + full_layout(&mut world, root); + let full = snapshot(&world, &[root, parent, a, b]); + + assert_eq!(incremental, full); + assert_eq!(world.cache.bounds(b).unwrap().posy, 100.0); +} + +/// If an absolutely-positioned ancestor is right/bottom anchored, changing its child size must +/// restart at the absolute node's parent so the ancestor is repositioned. +#[test] +fn incremental_repositions_right_bottom_anchored_absolute_ancestor() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(400.0)); + world.set_height(root, Units::Pixels(400.0)); + world.set_alignment(root, Alignment::TopLeft); + world.set_layout_type(root, LayoutType::Overlay); + + let absolute = world.add(Some(root)); + world.set_position_type(absolute, PositionType::Absolute); + world.set_layout_type(absolute, LayoutType::Column); + world.set_width(absolute, Units::Auto); + world.set_height(absolute, Units::Auto); + world.set_right(absolute, Units::Pixels(0.0)); + world.set_bottom(absolute, Units::Pixels(0.0)); + + let child = world.add(Some(absolute)); + world.set_width(child, Units::Pixels(100.0)); + world.set_height(child, Units::Pixels(100.0)); + + full_layout(&mut world, root); + assert_eq!(world.cache.bounds(absolute).unwrap().posx, 300.0); + assert_eq!(world.cache.bounds(absolute).unwrap().posy, 300.0); + + world.set_width(child, Units::Pixels(150.0)); + world.set_height(child, Units::Pixels(150.0)); + incremental_layout(&mut world, child); + let incremental = snapshot(&world, &[root, absolute, child]); + + full_layout(&mut world, root); + let full = snapshot(&world, &[root, absolute, child]); + + assert_eq!(incremental, full); + assert_eq!(world.cache.bounds(absolute).unwrap().posx, 250.0); + assert_eq!(world.cache.bounds(absolute).unwrap().posy, 250.0); +} + +/// A `Percentage`-sized ancestor cannot be reproduced from the cached size alone, so the search +/// bubbles past it up to the fixed root, still matching a full layout pass. +#[test] +fn incremental_bubbles_through_percentage_ancestor() { + 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 percent = world.add(Some(root)); + world.set_width(percent, Units::Percentage(50.0)); + world.set_height(percent, Units::Percentage(50.0)); + world.set_layout_type(percent, LayoutType::Column); + + let child = world.add(Some(percent)); + world.set_width(child, Units::Pixels(100.0)); + world.set_height(child, Units::Pixels(100.0)); + + let child2 = world.add(Some(percent)); + world.set_width(child2, Units::Pixels(100.0)); + world.set_height(child2, Units::Pixels(100.0)); + + full_layout(&mut world, root); + + world.set_height(child, Units::Pixels(150.0)); + incremental_layout(&mut world, child); + let incremental = snapshot(&world, &[root, percent, child, child2]); + + full_layout(&mut world, root); + let full = snapshot(&world, &[root, percent, child, child2]); + + assert_eq!(incremental, full); + // The percentage container keeps its size (50% of 600 = 300) while children reflow. + assert_eq!(world.cache.bounds(percent).unwrap().height, 300.0); + assert_eq!(world.cache.bounds(child2).unwrap().posy, 150.0); +} + +/// Calling layout on the root is a full layout pass (regression: root behavior is unchanged). +#[test] +fn incremental_on_root_is_full_layout() { + 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::Row); + + let a = world.add(Some(root)); + world.set_width(a, Units::Pixels(100.0)); + world.set_height(a, Units::Pixels(150.0)); + + let b = world.add(Some(root)); + world.set_width(b, Units::Pixels(120.0)); + world.set_height(b, Units::Pixels(150.0)); + + incremental_layout(&mut world, root); + + 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 })); +}