diff --git a/benches/stack.rs b/benches/stack.rs index 8471b716..58c33df1 100644 --- a/benches/stack.rs +++ b/benches/stack.rs @@ -36,7 +36,7 @@ fn compute_node_count(children_per_node: usize, depth: usize, node_count: &mut u } } - return *node_count; + *node_count } fn morphorm_benchmarks(c: &mut Criterion) { @@ -124,6 +124,27 @@ fn morphorm_benchmarks(c: &mut Criterion) { }); group.finish(); + + let mut group = c.benchmark_group("Steady State Tree"); + group.sample_size(20); + + let children_per_node = 10; + let depth = 6usize; + let mut world = World::default(); + let root = build_tree(&mut world, None, children_per_node, depth); + + let benchmark_label = format!( + "Steady state bench. {children_per_node} children per node, depth: {depth}. Total nodes: {}.", + compute_node_count(children_per_node, depth, &mut 0) + ); + + group.bench_function(benchmark_label, |b| { + b.iter(|| { + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + }) + }); + + group.finish(); } criterion_group!(benches, morphorm_benchmarks); diff --git a/ecs/src/implementations.rs b/ecs/src/implementations.rs index 8aac9cac..2691d37e 100644 --- a/ecs/src/implementations.rs +++ b/ecs/src/implementations.rs @@ -35,6 +35,14 @@ impl Node for Entity { store.position_type.get(*self).copied() } + fn direction(&self, store: &Store) -> Option { + store.direction.get(*self).copied() + } + + fn wrap(&self, store: &Store) -> Option { + store.wrap.get(*self).copied() + } + fn alignment(&self, store: &Store) -> Option { store.alignment.get(*self).copied() } @@ -186,7 +194,6 @@ pub struct Rect { pub height: f32, } -#[derive(Default, Debug)] pub struct NodeCache { // Computed size and position of nodes. pub rect: SecondaryMap, @@ -254,3 +261,9 @@ impl Cache for NodeCache { 0.0 } } + +impl Default for NodeCache { + fn default() -> Self { + Self { rect: SecondaryMap::new() } + } +} diff --git a/ecs/src/store.rs b/ecs/src/store.rs index 28d5f5cd..c92a6fc9 100644 --- a/ecs/src/store.rs +++ b/ecs/src/store.rs @@ -1,7 +1,7 @@ // Part of a very simple ECS for demonstration purposes only. use crate::{entity::Entity, TextWrap}; -use morphorm::{LayoutType, PositionType, Units, Alignment}; +use morphorm::{Alignment, Direction, LayoutType, LayoutWrap, PositionType, Units}; use slotmap::SecondaryMap; type ContentSizeType = Box, Option) -> (f32, f32)>; @@ -13,7 +13,9 @@ pub struct Store { pub layout_type: SecondaryMap, pub position_type: SecondaryMap, + pub direction: SecondaryMap, pub alignment: SecondaryMap, + pub wrap: SecondaryMap, pub grid_columns: SecondaryMap>, pub grid_rows: SecondaryMap>, @@ -72,6 +74,8 @@ impl Store { self.visible.remove(entity); self.layout_type.remove(entity); self.position_type.remove(entity); + self.direction.remove(entity); + self.wrap.remove(entity); self.left.remove(entity); self.right.remove(entity); self.top.remove(entity); @@ -110,6 +114,8 @@ impl Store { self.visible.clear(); self.layout_type.clear(); self.position_type.clear(); + self.direction.clear(); + self.wrap.clear(); self.left.clear(); self.right.clear(); self.top.clear(); diff --git a/ecs/src/world.rs b/ecs/src/world.rs index e2c68c65..c0ab609c 100644 --- a/ecs/src/world.rs +++ b/ecs/src/world.rs @@ -1,6 +1,6 @@ // Part of a very simple ECS for demonstration purposes only. -use morphorm::{LayoutType, PositionType, Units, Alignment}; +use morphorm::{Alignment, Direction, LayoutType, LayoutWrap, PositionType, Units}; use crate::entity::{Entity, EntityManager}; use crate::implementations::NodeCache; @@ -68,6 +68,16 @@ impl World { self.store.position_type.insert(entity, value); } + /// Set the inline direction used for horizontal positioning semantics for the given entity. + pub fn set_direction(&mut self, entity: Entity, value: Direction) { + self.store.direction.insert(entity, value); + } + + /// Set the wrap mode for children of the given entity. + pub fn set_wrap(&mut self, entity: Entity, value: LayoutWrap) { + self.store.wrap.insert(entity, value); + } + pub fn set_alignment(&mut self, entity: Entity, value: Alignment) { self.store.alignment.insert(entity, value); } @@ -183,18 +193,16 @@ impl World { 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); } - - + pub fn set_grid_rows(&mut self, entity: Entity, value: Vec) { self.store.grid_rows.insert(entity, value); } @@ -202,7 +210,7 @@ impl World { pub fn set_column_start(&mut self, entity: Entity, value: usize) { self.store.column_start.insert(entity, value); } - + pub fn set_row_start(&mut self, entity: Entity, value: usize) { self.store.row_start.insert(entity, value); } @@ -210,7 +218,7 @@ impl World { pub fn set_column_span(&mut self, entity: Entity, value: usize) { self.store.column_span.insert(entity, value); } - + pub fn set_row_span(&mut self, entity: Entity, value: usize) { self.store.row_span.insert(entity, value); } diff --git a/examples/advanced.rs b/examples/advanced.rs index d5d08b23..618d420b 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -374,7 +374,7 @@ pub fn render(mut cache: LayoutCache, mut root: Widget) { *control_flow = ControlFlow::Wait; match event { - Event::LoopDestroyed => return, + Event::LoopDestroyed => (), Event::WindowEvent { ref event, .. } => match event { WindowEvent::Resized(size) => { surface.resize(&context, size.width.try_into().unwrap(), size.height.try_into().unwrap()); @@ -386,10 +386,10 @@ pub fn render(mut cache: LayoutCache, mut root: Widget) { } WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::KeyboardInput { device_id: _, input, is_synthetic: _ } => { - if input.virtual_keycode == Some(VirtualKeyCode::H) && input.state == ElementState::Pressed { - print_node(&root, &cache, &(), true, false, String::new()); - } + WindowEvent::KeyboardInput { device_id: _, input, is_synthetic: _ } + if input.virtual_keycode == Some(VirtualKeyCode::H) && input.state == ElementState::Pressed => + { + print_node(&root, &cache, &(), true, false, String::new()); } _ => (), }, @@ -397,8 +397,8 @@ pub fn render(mut cache: LayoutCache, mut root: Widget) { let dpi_factor = window.scale_factor(); let size = window.inner_size(); - canvas.set_size(size.width as u32, size.height as u32, dpi_factor as f32); - canvas.clear_rect(0, 0, size.width as u32, size.height as u32, Color::rgbf(0.3, 0.3, 0.32)); + canvas.set_size(size.width, size.height, dpi_factor as f32); + canvas.clear_rect(0, 0, size.width, size.height, Color::rgbf(0.3, 0.3, 0.32)); draw_node(&root, &cache, &mut canvas, font); @@ -422,16 +422,16 @@ fn draw_node(node: &Widget, cache: &LayoutCache, canvas: &mut Canvas, fo let mut path = Path::new(); path.rect(posx, posy, width, height); let paint = Paint::color(color); - canvas.fill_path(&mut path, &paint); + canvas.fill_path(&path, &paint); let mut paint = Paint::color(Color::black()); paint.set_font_size(24.0); paint.set_text_align(Align::Center); paint.set_text_baseline(Baseline::Middle); - paint.set_font(&vec![font]); - let _ = canvas.fill_text(posx + width / 2.0, posy + height / 2.0, &node.key().to_string(), &paint); + paint.set_font(&[font]); + let _ = canvas.fill_text(posx + width / 2.0, posy + height / 2.0, node.key().to_string(), &paint); - for child in (&node).children(&()) { + for child in node.children(&()) { draw_node(child, cache, canvas, font); } } diff --git a/examples/common/mod.rs b/examples/common/mod.rs index efc6e457..45452637 100644 --- a/examples/common/mod.rs +++ b/examples/common/mod.rs @@ -102,7 +102,7 @@ pub fn render(mut world: World, root: Entity) { *control_flow = ControlFlow::Wait; match event { - Event::LoopDestroyed => return, + Event::LoopDestroyed => (), Event::WindowEvent { ref event, .. } => match event { WindowEvent::Resized(size) => { surface.resize(&context, size.width.try_into().unwrap(), size.height.try_into().unwrap()); @@ -126,20 +126,20 @@ pub fn render(mut world: World, root: Entity) { } WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::KeyboardInput { device_id: _, input, is_synthetic: _ } => { - if input.virtual_keycode == Some(VirtualKeyCode::H) && input.state == ElementState::Pressed { - print_node(&root, &world.cache, &world.tree, true, false, String::new()); - } + WindowEvent::KeyboardInput { device_id: _, input, is_synthetic: _ } + if input.virtual_keycode == Some(VirtualKeyCode::H) && input.state == ElementState::Pressed => + { + print_node(&root, &world.cache, &world.tree, true, false, String::new()); } _ => (), }, Event::RedrawRequested(_) => { let size = window.inner_size(); - canvas.set_size(size.width as u32, size.height as u32, 1.0); - canvas.clear_rect(0, 0, size.width as u32, size.height as u32, Color::rgbf(0.3, 0.3, 0.32)); + canvas.set_size(size.width, size.height, 1.0); + canvas.clear_rect(0, 0, size.width, size.height, Color::rgbf(0.3, 0.3, 0.32)); - draw_node(&root, &world.tree, &world.cache, &world.store, 0.0, 0.0, font, &mut canvas); + draw_node(&root, &world.tree, &world.cache, &world.store, (0.0, 0.0), font, &mut canvas); canvas.flush(); surface.swap_buffers(&context).unwrap(); @@ -157,11 +157,11 @@ fn draw_node>( tree: &N::Tree, cache: &impl Cache, store: &Store, - parent_posx: f32, - parent_posy: f32, + parent_pos: (f32, f32), font: FontId, canvas: &mut Canvas, ) { + let (parent_posx, parent_posy) = parent_pos; let posx = cache.posx(node); let posy = cache.posy(node); let width = cache.width(node); @@ -174,14 +174,14 @@ fn draw_node>( let mut path = Path::new(); path.rect(parent_posx + posx, parent_posy + posy, width, height); let paint = Paint::color(Color::rgb(*red, *green, *blue)); - canvas.fill_path(&mut path, &paint); + canvas.fill_path(&path, &paint); if let Some(text) = store.text.get(node.key()) { let mut paint = Paint::color(Color::black()); paint.set_font_size(48.0); paint.set_text_align(Align::Left); paint.set_text_baseline(Baseline::Top); - paint.set_font(&vec![font]); + paint.set_font(&[font]); let font_metrics = canvas.measure_font(&paint).expect("Error measuring font"); @@ -199,17 +199,17 @@ fn draw_node>( paint.set_font_size(48.0); paint.set_text_align(Align::Center); paint.set_text_baseline(Baseline::Middle); - paint.set_font(&vec![font]); + paint.set_font(&[font]); let _ = canvas.fill_text( parent_posx + posx + width / 2.0, parent_posy + posy + height / 2.0, - &node.key().0.to_string(), + node.key().0.to_string(), &paint, ); } for child in node.children(tree) { - draw_node(child, tree, cache, store, posx + parent_posx, posy + parent_posy, font, canvas); + draw_node(child, tree, cache, store, (posx + parent_posx, posy + parent_posy), font, canvas); } } diff --git a/examples/textwrap.rs b/examples/textwrap.rs index 8ebfd67d..bba0a58b 100644 --- a/examples/textwrap.rs +++ b/examples/textwrap.rs @@ -91,7 +91,7 @@ fn content_size(node: Entity, store: &Store, width: Option, height: Option< paint.set_font_size(48.0); paint.set_text_align(femtovg::Align::Left); paint.set_text_baseline(femtovg::Baseline::Top); - paint.set_font(&vec![store.font_id.unwrap()]); + paint.set_font(&[store.font_id.unwrap()]); // let should_wrap = store.text_wrap.get(&node).copied().unwrap_or_default(); let text_wrap = store.text_wrap.get(node).copied().unwrap_or_default(); diff --git a/examples/wrap.rs b/examples/wrap.rs new file mode 100644 index 00000000..ab9cfae3 --- /dev/null +++ b/examples/wrap.rs @@ -0,0 +1,70 @@ +mod common; +use common::*; + +fn main() { + 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_padding(root, Units::Pixels(20.0)); + world.set_layout_type(root, LayoutType::Column); + world.set_vertical_gap(root, Units::Pixels(20.0)); + world.set_alignment(root, Alignment::TopLeft); + + // Row wrapping example + let row_wrap_container = world.add(Some(root)); + world.set_width(row_wrap_container, Units::Stretch(1.0)); + world.set_height(row_wrap_container, Units::Auto); + world.set_layout_type(row_wrap_container, LayoutType::Row); + world.set_wrap(row_wrap_container, LayoutWrap::Wrap); + world.set_horizontal_gap(row_wrap_container, Units::Pixels(10.0)); + world.set_vertical_gap(row_wrap_container, Units::Pixels(10.0)); + world.set_alignment(row_wrap_container, Alignment::TopLeft); + + // Add items to row wrap container + for _i in 0..6 { + let item = world.add(Some(row_wrap_container)); + world.set_width(item, Units::Pixels(80.0)); + world.set_height(item, Units::Pixels(60.0)); + } + + // Column wrapping example + let col_wrap_container = world.add(Some(root)); + world.set_width(col_wrap_container, Units::Pixels(200.0)); + world.set_height(col_wrap_container, Units::Pixels(250.0)); + world.set_layout_type(col_wrap_container, LayoutType::Column); + world.set_wrap(col_wrap_container, LayoutWrap::Wrap); + world.set_horizontal_gap(col_wrap_container, Units::Pixels(10.0)); + world.set_vertical_gap(col_wrap_container, Units::Pixels(10.0)); + world.set_alignment(col_wrap_container, Alignment::TopLeft); + + // Add items to column wrap container + for _i in 0..6 { + let item = world.add(Some(col_wrap_container)); + world.set_width(item, Units::Pixels(60.0)); + world.set_height(item, Units::Pixels(50.0)); + } + + // RTL wrapping example + let rtl_wrap_container = world.add(Some(root)); + world.set_width(rtl_wrap_container, Units::Pixels(300.0)); + world.set_height(rtl_wrap_container, Units::Auto); + world.set_layout_type(rtl_wrap_container, LayoutType::Row); + world.set_direction(rtl_wrap_container, Direction::RightToLeft); + world.set_wrap(rtl_wrap_container, LayoutWrap::Wrap); + world.set_horizontal_gap(rtl_wrap_container, Units::Pixels(10.0)); + world.set_vertical_gap(rtl_wrap_container, Units::Pixels(10.0)); + world.set_alignment(rtl_wrap_container, Alignment::TopLeft); + + // Add items to RTL wrap container + for _i in 0..4 { + let item = world.add(Some(rtl_wrap_container)); + world.set_width(item, Units::Pixels(100.0)); + world.set_height(item, Units::Pixels(50.0)); + } + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + render(world, root); +} diff --git a/src/layout.rs b/src/layout.rs index c91b0288..c89f813c 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,6 +1,8 @@ use smallvec::SmallVec; -use crate::{Alignment, Cache, CacheExt, LayoutType, Node, NodeExt, PositionType, Size, Units::*}; +use crate::{ + Alignment, Cache, CacheExt, Direction, LayoutType, LayoutWrap, Node, NodeExt, PositionType, Size, Units::*, +}; const DEFAULT_MIN: f32 = -f32::MAX; const DEFAULT_MAX: f32 = f32::MAX; @@ -25,6 +27,8 @@ struct StretchItem { violation: f32, // The computed size of the stretch item. computed: f32, + // The measured size from the current flex iteration. + measured: f32, // Whether or not the stretch item is frozen. frozen: bool, // The minimum size of the stretch item. @@ -35,7 +39,7 @@ struct StretchItem { impl StretchItem { pub fn new(index: usize, factor: f32, item_type: ItemType, min: f32, max: f32) -> Self { - Self { index, factor, item_type, violation: 0.0, computed: 0.0, frozen: false, min, max } + Self { index, factor, item_type, violation: 0.0, computed: 0.0, measured: 0.0, frozen: false, min, max } } } @@ -49,6 +53,27 @@ struct ChildNode<'a, N: Node> { main: f32, main_after: f32, + // Last parent constraints used to lay out this child. + last_layout_main: f32, + last_layout_cross: f32, + has_layout_constraints: bool, +} + +fn flip_alignment_horizontal(alignment: Alignment) -> Alignment { + match alignment { + Alignment::TopLeft => Alignment::TopRight, + Alignment::TopRight => Alignment::TopLeft, + Alignment::Left => Alignment::Right, + Alignment::Right => Alignment::Left, + Alignment::BottomLeft => Alignment::BottomRight, + Alignment::BottomRight => Alignment::BottomLeft, + alignment => alignment, + } +} + +#[inline] +fn same_f32(a: f32, b: f32) -> bool { + a.to_bits() == b.to_bits() } #[allow(clippy::too_many_arguments)] @@ -164,6 +189,7 @@ where let clamped = actual_main.min(item.max).max(item.min); item.violation = clamped - actual_main; total_violation += item.violation; + item.measured = actual_main; item.computed = clamped; } @@ -178,9 +204,9 @@ where // If the item is frozen, adjust the used_space and sum of cross stretch factors. if item.frozen { col_flex_sum -= item.factor; + let prev = computed_grid_cols[item.index]; computed_grid_cols[item.index] = item.computed; - - width_sum = computed_grid_cols.iter().sum(); + width_sum += item.computed - prev; } } } @@ -204,6 +230,7 @@ where let clamped = actual_main.min(item.max).max(item.min); item.violation = clamped - actual_main; total_violation += item.violation; + item.measured = actual_main; item.computed = clamped; } @@ -218,9 +245,9 @@ where // If the item is frozen, adjust the used_space and sum of cross stretch factors. if item.frozen { row_flex_sum -= item.factor; + let prev = computed_grid_rows[item.index]; computed_grid_rows[item.index] = item.computed; - - height_sum = computed_grid_rows.iter().sum(); + height_sum += item.computed - prev; } } } @@ -229,20 +256,24 @@ where // println!("{:?} {:?}", computed_grid_cols, computed_grid_rows); let mut current_col_pos = 0.0; - for i in 0..computed_grid_cols.len() { - current_col_pos += computed_grid_cols[i]; - computed_grid_cols[i] = current_col_pos; + for col in &mut computed_grid_cols { + current_col_pos += *col; + *col = current_col_pos; } let mut current_row_pos = 0.0; - for i in 0..computed_grid_rows.len() { - current_row_pos += computed_grid_rows[i]; - computed_grid_rows[i] = current_row_pos; + for row in &mut computed_grid_rows { + current_row_pos += *row; + *row = current_row_pos; } // println!("{:?} {:?}", computed_grid_cols, computed_grid_rows); - let alignment = node.alignment(store).unwrap_or_default(); + let mut alignment = node.alignment(store).unwrap_or_default(); + + if node.direction(store).unwrap_or_default() == Direction::RightToLeft { + alignment = flip_alignment_horizontal(alignment); + } let (mut child_posx, mut child_posy) = match alignment { Alignment::TopLeft => (0.0, 0.0), @@ -259,15 +290,13 @@ where child_posx *= parent_width - width_sum; child_posy *= parent_height - height_sum; - let mut node_children = node + let node_children = node .children(tree) .filter(|child| child.visible(store)) - .filter(|child| child.position_type(store).unwrap_or_default() == PositionType::Relative) - .enumerate() - .peekable(); + .filter(|child| child.position_type(store).unwrap_or_default() == PositionType::Relative); // Compute space and size of non-flexible relative children. - while let Some((_, child)) = node_children.next() { + for child in node_children { let column_start = 2 * child.column_start(store).unwrap_or_default(); let column_span = 2 * child.column_span(store).unwrap_or(1) - 1; let column_end = column_start + column_span; @@ -297,6 +326,473 @@ where Size { main: computed_main, cross: computed_cross } } +/// Performs wrapped layout on the given node, arranging children into multiple lines +/// along the main axis when they overflow the available space. +/// +/// Called from [`layout`] when a node has [`LayoutWrap::Wrap`] set. +#[allow(clippy::too_many_arguments)] +pub(crate) fn layout_wrap( + node: &N, + parent_layout_type: LayoutType, + parent_main: f32, + parent_cross: f32, + cache: &mut C, + tree: &::Tree, + store: &::Store, + sublayout: &mut ::SubLayout<'_>, +) -> Size +where + N: Node, + C: Cache, +{ + let layout_type = node.layout_type(store).unwrap_or_default(); + + // Convert parent-provided main/cross (which are in parent layout axes) + // into this node's layout axes. + 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; + + // Gap between items within a line (on the main axis). + let min_main_between = node.min_main_between(store, layout_type); + let max_main_between = node.max_main_between(store, layout_type); + let item_gap_px = + node.main_between(store, layout_type).to_px_clamped(avail_main, 0.0, min_main_between, max_main_between); + + // Gap between lines (on the cross axis). + let line_gap_px = node.cross_between(store, layout_type).to_px(avail_cross, 0.0); + + let is_inline_rtl = matches!(layout_type, LayoutType::Row | LayoutType::Column) + && node.direction(store).unwrap_or_default() == Direction::RightToLeft; + + let relative_children = node + .children(tree) + .filter(|c| c.visible(store)) + .filter(|c| c.position_type(store).unwrap_or_default() == PositionType::Relative) + .collect::>(); + + let num_rel = relative_children.len(); + + // Per-item data used during layout. + struct WrapItem { + main: f32, + cross: f32, + /// Non-zero when this item has Stretch units on the main axis. + stretch_main_factor: f32, + cross_is_stretch: bool, + min_main: f32, + max_main: f32, + min_cross: f32, + max_cross: f32, + } + + // Phase 1: Compute sizes for all relative children. + // Stretch-main items are deferred; their sizes are resolved per-line in phase 3. + let mut items: SmallVec<[WrapItem; 32]> = SmallVec::with_capacity(num_rel); + for child in relative_children.iter() { + let child_main_units = child.main(store, layout_type); + let child_min_main = child.min_main(store, layout_type); + let child_max_main = child.max_main(store, layout_type); + let child_min_cross = child.min_cross(store, layout_type); + let child_max_cross = child.max_cross(store, layout_type); + let child_cross_is_stretch = child.cross(store, layout_type).is_stretch(); + + let min_main_px = child_min_main.to_px(avail_main, DEFAULT_MIN); + let max_main_px = child_max_main.to_px(avail_main, DEFAULT_MAX); + let min_cross_px = child_min_cross.to_px(avail_cross, DEFAULT_MIN); + let max_cross_px = child_max_cross.to_px(avail_cross, DEFAULT_MAX); + + if let Stretch(factor) = child_main_units { + // Use min size as base for line-break decisions; actual size resolved later. + let base = min_main_px.max(0.0); + items.push(WrapItem { + main: base, + cross: 0.0, + stretch_main_factor: factor, + cross_is_stretch: child_cross_is_stretch, + min_main: min_main_px, + max_main: max_main_px, + min_cross: min_cross_px, + max_cross: max_cross_px, + }); + } else { + let size = layout(*child, layout_type, avail_main, avail_cross, cache, tree, store, sublayout); + items.push(WrapItem { + main: size.main, + cross: size.cross, + stretch_main_factor: 0.0, + cross_is_stretch: child_cross_is_stretch, + min_main: min_main_px, + max_main: max_main_px, + min_cross: min_cross_px, + max_cross: max_cross_px, + }); + } + } + + // Phase 2: Assign children to lines. + // A new line begins when adding the next child would exceed avail_main. + // Stretch-main children use their min contribution for break decisions. + // If avail_main <= 0 (auto-width container), no breaks occur. + let mut lines: SmallVec<[std::ops::Range; 8]> = SmallVec::new(); + if num_rel > 0 { + let mut line_start = 0usize; + let mut line_main_used = 0.0f32; + let mut items_in_line = 0usize; + + for i in 0..num_rel { + // Stretch items contribute their min size to the line-break decision (0 if no min set). + let size_contribution = items[i].main; + let gap_before = if items_in_line > 0 { item_gap_px } else { 0.0 }; + let projected = line_main_used + gap_before + size_contribution; + + if avail_main > 0.0 && items_in_line > 0 && projected > avail_main { + // Finish current line and start a new one. + lines.push(line_start..i); + line_start = i; + line_main_used = size_contribution; + items_in_line = 1; + } else { + line_main_used = projected; + items_in_line += 1; + } + } + lines.push(line_start..num_rel); + } + + // Phase 3: Per-line flex resolution for stretch-main items. + for line in lines.iter() { + let start = line.start; + let end = line.end; + let count = line.len(); + if count == 0 { + continue; + } + + let mut stretch_sum = 0.0f32; + let mut fixed_sum = 0.0f32; + for i in start..end { + if items[i].stretch_main_factor > 0.0 { + stretch_sum += items[i].stretch_main_factor; + } else { + fixed_sum += items[i].main; + } + } + + if stretch_sum > 0.0 { + let gap_total = (count - 1) as f32 * item_gap_px; + let free_main = (avail_main - fixed_sum - gap_total).max(0.0); + + for i in start..end { + let factor = items[i].stretch_main_factor; + if factor > 0.0 { + let allocated = (factor / stretch_sum * free_main).round(); + let clamped = allocated.clamp(items[i].min_main, items[i].max_main); + let size = + layout(relative_children[i], layout_type, clamped, avail_cross, cache, tree, store, sublayout); + items[i].main = size.main; + items[i].cross = size.cross; + } + } + } + } + + // Phase 4: Compute the cross extent of each line from non-cross-stretch children. + let mut line_cross: SmallVec<[f32; 8]> = SmallVec::with_capacity(lines.len()); + for line in lines.iter() { + let start = line.start; + let end = line.end; + let mut max_cross = 0.0f32; + for i in start..end { + if !items[i].cross_is_stretch { + max_cross = max_cross.max(items[i].cross); + } + } + line_cross.push(max_cross); + } + + // Phase 5: Resolve cross-stretch children to fill their line's cross extent. + for (line_idx, line) in lines.iter().enumerate() { + let start = line.start; + let end = line.end; + let lc = line_cross[line_idx]; + for i in start..end { + if items[i].cross_is_stretch { + let child = relative_children[i]; + let clamped_cross = lc.clamp(items[i].min_cross, items[i].max_cross); + let size = layout(child, layout_type, items[i].main, clamped_cross, cache, tree, store, sublayout); + items[i].main = size.main; + items[i].cross = size.cross; + } + } + // Re-compute line cross to include cross-stretch items in case they changed. + let mut max_cross = 0.0f32; + for i in start..end { + max_cross = max_cross.max(items[i].cross); + } + line_cross[line_idx] = max_cross; + } + + // Phase 6: Determine the final cross size of the container. + let num_lines = lines.len(); + let total_content_cross = if num_lines > 0 { + line_cross.iter().sum::() + (num_lines.saturating_sub(1)) as f32 * line_gap_px + } else { + 0.0 + }; + + 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 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) + } else { + parent_cross + }; + + // Recompute auto main size (for containers with Auto main axis). + let main_units = node.main(store, layout_type); + let final_main = if main_units.is_auto() || parent_main == 0.0 { + let raw = lines + .iter() + .map(|line| { + let mut sum = 0.0f32; + for i in line.start..line.end { + sum += items[i].main; + } + 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 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) + } else { + parent_main + }; + + // 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_children = node + .children(tree) + .filter(|c| c.position_type(store).unwrap_or_default() == PositionType::Absolute) + .filter(|c| c.visible(store)); + + let mut abs_items: SmallVec<[ChildNode; 8]> = SmallVec::new(); + for child in abs_children { + let main = if child.main(store, layout_type).is_stretch() { + let child_min_main = child.min_main(store, layout_type).to_px(abs_avail_main, DEFAULT_MIN); + let child_max_main = child.max_main(store, layout_type).to_px(abs_avail_main, DEFAULT_MAX); + let main_before = child.main_before(store, layout_type).to_px(abs_avail_main, 0.0); + let main_after = child.main_after(store, layout_type).to_px(abs_avail_main, 0.0); + abs_avail_main.clamp(child_min_main, child_max_main) - main_before - main_after + } else { + abs_avail_main + }; + + let cross = if child.cross(store, layout_type).is_stretch() { + let child_min_cross = child.min_cross(store, layout_type).to_px(abs_avail_cross, DEFAULT_MIN); + let child_max_cross = child.max_cross(store, layout_type).to_px(abs_avail_cross, DEFAULT_MAX); + let cross_before = child.cross_before(store, layout_type).to_px(abs_avail_cross, 0.0); + let cross_after = child.cross_after(store, layout_type).to_px(abs_avail_cross, 0.0); + abs_avail_cross.clamp(child_min_cross, child_max_cross) - cross_before - cross_after + } else { + abs_avail_cross + }; + + let size = layout(child, layout_type, main, cross, cache, tree, store, sublayout); + abs_items.push(ChildNode { + node: child, + main: size.main, + cross: size.cross, + main_after: 0.0, + last_layout_main: 0.0, + last_layout_cross: 0.0, + has_layout_constraints: false, + }); + } + + // Phase 8: Position all children. + // Decompose alignment into (main-fraction, cross-fraction) where the + // main-fraction offsets the whole group and cross-fraction aligns each item + // within its line's cross extent. The swap mirrors what the non-wrap layout + // does so that `TopLeft` means the same visual position regardless of + // layout_type. + let mut alignment = node.alignment(store).unwrap_or_default(); + + // For RTL inline layouts, flip horizontal alignment so TopLeft becomes TopRight. + if is_inline_rtl { + alignment = flip_alignment_horizontal(alignment); + } + + let (mut main_align_frac, mut cross_align_frac) = match alignment { + Alignment::TopLeft => (0.0f32, 0.0f32), + 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 main_align_frac, &mut cross_align_frac); + } + + let mut cross_cursor = padding_cross_before + border_cross_before; + + for (line_idx, line) in lines.iter().enumerate() { + let start = line.start; + let end = line.end; + let lc = line_cross[line_idx]; + let count = line.len(); + let gap_total = (count.saturating_sub(1)) as f32 * item_gap_px; + let mut line_main_sum = 0.0f32; + for i in start..end { + line_main_sum += items[i].main; + } + let free_main = (avail_main - line_main_sum - gap_total).max(0.0); + + 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; + + for (item_idx, i) in (start..end).rev().enumerate() { + let item = &items[i]; + let child = relative_children[i]; + let item_cross_offset = cross_align_frac * (lc - item.cross); + + cache.set_rect( + child, + layout_type, + main_cursor, + cross_cursor + item_cross_offset, + item.main, + item.cross, + ); + + main_cursor += item.main; + if item_idx + 1 < count { + main_cursor += item_gap_px; + } + } + } 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; + + for i in start..end { + let item = &items[i]; + let child = relative_children[i]; + + let item_cross_offset = cross_align_frac * (lc - item.cross); + + cache.set_rect( + child, + layout_type, + main_cursor, + cross_cursor + item_cross_offset, + item.main, + item.cross, + ); + + main_cursor += item.main; + if i + 1 < end { + main_cursor += item_gap_px; + } + } + } + + cross_cursor += lc; + if line_idx + 1 < num_lines { + cross_cursor += line_gap_px; + } + } + + // Position absolute children. + for abs_child in &abs_items { + let (child_main_before, child_main_after) = if is_inline_rtl { + (abs_child.node.main_after(store, layout_type), abs_child.node.main_before(store, layout_type)) + } else { + (abs_child.node.main_before(store, layout_type), abs_child.node.main_after(store, layout_type)) + }; + let child_cross_before = abs_child.node.cross_before(store, layout_type); + let child_cross_after = abs_child.node.cross_after(store, layout_type); + + let pma = abs_avail_main; + let pca = abs_avail_cross; + + let child_main_pos = match (child_main_before, child_main_after) { + (Pixels(val), _) => val, + (Percentage(val), _) => val * 0.01 * pma, + (_, Pixels(val)) => pma - val - abs_child.main, + (_, Percentage(val)) => pma - abs_child.main - val * 0.01 * pma, + (Stretch(b), Stretch(a)) => { + if b == a { + (pma - abs_child.main) * 0.5 + } else { + (pma - abs_child.main) * (b / (b + a)) + } + } + (Stretch(_), Auto) => pma - abs_child.main, + (Auto, Stretch(_)) => 0.0, + (Auto, Auto) => 0.0, + }; + + let child_cross_pos = match (child_cross_before, child_cross_after) { + (Pixels(val), _) => val, + (Percentage(val), _) => val * 0.01 * pca, + (_, Pixels(val)) => pca - val - abs_child.cross, + (_, Percentage(val)) => pca - abs_child.cross - val * 0.01 * pca, + (Stretch(b), Stretch(a)) => { + if b == a { + (pca - abs_child.cross) * 0.5 + } else { + (pca - abs_child.cross) * (b / (b + a)) + } + } + (Stretch(_), Auto) => pca - abs_child.cross, + (Auto, Stretch(_)) => 0.0, + (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, + ); + } + + if parent_layout_type == layout_type { + Size { main: final_main, cross: final_cross } + } else { + Size { main: final_cross, cross: final_main } + } +} + /// Performs layout on the given node returning its computed size. /// /// The algorithm recurses down the tree, in depth-first order, and performs @@ -381,15 +877,21 @@ where 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(); + for child in node.children(tree).filter(|child| child.visible(store)) { + match child.position_type(store).unwrap_or_default() { + PositionType::Relative => relative_children.push(child), + PositionType::Absolute => absolute_children.push(child), + } + } + // Get the total number of children of the node. - let num_children = node.children(tree).filter(|child| child.visible(store)).count(); + let num_children = relative_children.len() + absolute_children.len(); // Get the total number of relative children of the node. - let num_parent_directed_children = node - .children(tree) - .filter(|child| child.position_type(store).unwrap_or_default() == PositionType::Relative) - .filter(|child| child.visible(store)) - .count(); + let num_parent_directed_children = relative_children.len(); // Apply content sizing. if (node.min_main(store, parent_layout_type).is_auto() || node.min_cross(store, parent_layout_type).is_auto()) @@ -421,6 +923,10 @@ where return layout_grid(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); } + if node.wrap(store).unwrap_or_default() == LayoutWrap::Wrap { + return layout_wrap(node, parent_layout_type, computed_main, computed_cross, cache, tree, store, sublayout); + } + // Determine the parent_main/cross size to pass to the children based on the layout type of the parent and the node. // i.e. if the parent layout type and the node layout type are different, swap the main and the cross axes. let (mut parent_main, mut parent_cross) = if parent_layout_type == layout_type { @@ -451,25 +957,20 @@ where 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; - // Determine index of first and last relative child nodes. - let mut iter = node - .children(tree) - .filter(|child| child.visible(store)) - .filter(|child| child.position_type(store).unwrap_or_default() == PositionType::Relative) - .enumerate(); + let is_row_rtl = + layout_type == LayoutType::Row && node.direction(store).unwrap_or_default() == Direction::RightToLeft; - let first = iter.next().map(|(index, _)| index); - let last = iter.last().map_or(first, |(index, _)| Some(index)); + let is_rtl = matches!(layout_type, LayoutType::Row | LayoutType::Column) + && node.direction(store).unwrap_or_default() == Direction::RightToLeft; - let mut node_children = node - .children(tree) - .filter(|child| child.visible(store)) - .filter(|child| child.position_type(store).unwrap_or_default() == PositionType::Relative) - .enumerate() - .peekable(); + if is_row_rtl { + relative_children.reverse(); + } + + let last = relative_children.len().checked_sub(1); // Compute space and size of non-flexible relative children. - while let Some((index, child)) = node_children.next() { + for (index, child) in relative_children.into_iter().enumerate() { let child_main = child.main(store, layout_type); let child_cross = child.cross(store, layout_type); @@ -519,11 +1020,18 @@ where let mut computed_child_cross = child_cross.to_px_clamped(parent_cross, 0.0, child_min_cross, child_max_cross); // Compute fixed-size child main and cross. + let mut has_layout_constraints = false; + let mut last_layout_main = 0.0; + let mut last_layout_cross = 0.0; + if !child_main.is_stretch() && (!child_cross.is_stretch() || child_min_cross.is_auto()) { let child_size = layout(child, layout_type, parent_main, parent_cross, cache, tree, store, sublayout); computed_child_main = child_size.main; computed_child_cross = child_size.cross; + has_layout_constraints = true; + last_layout_main = parent_main; + last_layout_cross = parent_cross; } children.push(ChildNode { @@ -531,6 +1039,9 @@ where cross: computed_child_cross, main: computed_child_main, main_after: computed_child_main_after, + last_layout_main, + last_layout_cross, + has_layout_constraints, }); } @@ -600,9 +1111,18 @@ where .filter(|child| child.node.cross(store, layout_type).is_stretch()) { if !child.node.main(store, layout_type).is_stretch() { - let child_size = layout(child.node, layout_type, parent_main, parent_cross, cache, tree, store, sublayout); - child.main = child_size.main; - child.cross = child_size.cross; + if !child.has_layout_constraints + || !same_f32(child.last_layout_main, parent_main) + || !same_f32(child.last_layout_cross, parent_cross) + { + let child_size = + layout(child.node, layout_type, parent_main, parent_cross, cache, tree, store, sublayout); + child.main = child_size.main; + child.cross = child_size.cross; + child.last_layout_main = parent_main; + child.last_layout_cross = parent_cross; + child.has_layout_constraints = true; + } } else { let child_min_cross = if child.node.min_cross(store, layout_type).is_auto() { child.cross @@ -677,26 +1197,33 @@ where let mut total_violation = 0.0; for item in main_axis.iter_mut().filter(|item| !item.frozen) { - let mut actual_main = (item.factor * free_main_space / main_flex_sum).round(); + let input_main = (item.factor * free_main_space / main_flex_sum).round(); + let mut actual_main = input_main; let child = &mut children[item.index]; if item.item_type == ItemType::Size { - let child_size = layout( - child.node, - layout_type, - actual_main, - if child.node.cross(store, layout_type).is_stretch() { child.cross } else { parent_cross }, - cache, - tree, - store, - sublayout, - ); - child.cross = child_size.cross; - actual_main = child_size.main; + let target_cross = + if child.node.cross(store, layout_type).is_stretch() { child.cross } else { parent_cross }; + + if !child.has_layout_constraints + || !same_f32(child.last_layout_main, input_main) + || !same_f32(child.last_layout_cross, target_cross) + { + let child_size = + layout(child.node, layout_type, input_main, target_cross, cache, tree, store, sublayout); + child.cross = child_size.cross; + actual_main = child_size.main; + item.measured = actual_main; + child.last_layout_main = input_main; + child.last_layout_cross = target_cross; + child.has_layout_constraints = true; + } else { + actual_main = item.measured; + } if child.node.min_main(store, layout_type).is_auto() { - item.min = child_size.main; + item.min = child.main; } } @@ -719,9 +1246,40 @@ where // If the item is frozen, adjust the used_space and sum of cross stretch factors. if item.frozen { main_flex_sum -= item.factor; + let previous_total = child.main + child.main_after; match item.item_type { ItemType::Size => { + if (item.computed - item.measured).abs() > f32::EPSILON { + let target_cross = if child.node.cross(store, layout_type).is_stretch() { + child.cross + } else { + parent_cross + }; + + if !child.has_layout_constraints + || !same_f32(child.last_layout_main, item.computed) + || !same_f32(child.last_layout_cross, target_cross) + { + let child_size = layout( + child.node, + layout_type, + item.computed, + target_cross, + cache, + tree, + store, + sublayout, + ); + + child.cross = child_size.cross; + item.measured = child_size.main; + child.last_layout_main = item.computed; + child.last_layout_cross = target_cross; + child.has_layout_constraints = true; + } + } + child.main = item.computed; } @@ -730,7 +1288,7 @@ where } } - main_sum = children.iter().map(|child| child.main + child.main_after).sum(); + main_sum += (child.main + child.main_after) - previous_total; } } } @@ -808,37 +1366,70 @@ where child.cross = parent_cross.clamp(child_min_cross, child_max_cross); } + // Re-run relative children with their final resolved constraints so descendant + // layout uses the same dimensions that are ultimately cached for each child. + // Only rerun children that have descendants (skip leaf nodes). + for child in children + .iter_mut() + .filter(|child| child.node.position_type(store).unwrap_or_default() == PositionType::Relative) + .filter(|child| child.node.children(tree).next().is_some()) + { + let child_main_is_stretch = child.node.main(store, layout_type).is_stretch(); + let child_cross_is_stretch = child.node.cross(store, layout_type).is_stretch(); + + let target_main = if child_main_is_stretch { child.main } else { parent_main }; + let target_cross = if child_cross_is_stretch { child.cross } else { parent_cross }; + + if child.has_layout_constraints + && same_f32(child.last_layout_main, target_main) + && same_f32(child.last_layout_cross, target_cross) + { + continue; + } + + let child_size = layout(child.node, layout_type, target_main, target_cross, cache, tree, store, sublayout); + + child.last_layout_main = target_main; + child.last_layout_cross = target_cross; + child.has_layout_constraints = true; + + if !child_main_is_stretch { + child.main = child_size.main; + } + + child.cross = child_size.cross; + } + // Absolute Children - let node_children = node - .children(tree) - .filter(|child| child.position_type(store).unwrap_or_default() == PositionType::Absolute) - .filter(|child| child.visible(store)); + // Absolute children are sized against the padding box (content box + padding, excluding border). + let abs_size_main = parent_main + padding_main_before + padding_main_after; + let abs_size_cross = parent_cross + padding_cross_before + padding_cross_after; // Compute space and size of non-flexible absolute children. - for child in node_children { + for child in absolute_children.into_iter() { let main = if child.main(store, layout_type).is_stretch() { - let child_min_main = child.min_main(store, layout_type).to_px(parent_cross, DEFAULT_MIN); - let child_max_main = child.max_main(store, layout_type).to_px(parent_cross, DEFAULT_MAX); + let child_min_main = child.min_main(store, layout_type).to_px(abs_size_main, DEFAULT_MIN); + let child_max_main = child.max_main(store, layout_type).to_px(abs_size_main, DEFAULT_MAX); - let child_main_before = child.main_before(store, layout_type).to_px(parent_main, 0.0); - let child_main_after = child.main_after(store, layout_type).to_px(parent_main, 0.0); + let child_main_before = child.main_before(store, layout_type).to_px(abs_size_main, 0.0); + let child_main_after = child.main_after(store, layout_type).to_px(abs_size_main, 0.0); - parent_main.clamp(child_min_main, child_max_main) - child_main_before - child_main_after + abs_size_main.clamp(child_min_main, child_max_main) - child_main_before - child_main_after } else { - parent_main + abs_size_main }; let cross = if child.cross(store, layout_type).is_stretch() { - let child_min_cross = child.min_cross(store, layout_type).to_px(parent_cross, DEFAULT_MIN); - let child_max_cross = child.max_cross(store, layout_type).to_px(parent_cross, DEFAULT_MAX); + let child_min_cross = child.min_cross(store, layout_type).to_px(abs_size_cross, DEFAULT_MIN); + let child_max_cross = child.max_cross(store, layout_type).to_px(abs_size_cross, DEFAULT_MAX); - let child_cross_before = child.cross_before(store, layout_type).to_px(parent_main, 0.0); - let child_cross_after = child.cross_after(store, layout_type).to_px(parent_main, 0.0); + let child_cross_before = child.cross_before(store, layout_type).to_px(abs_size_cross, 0.0); + let child_cross_after = child.cross_after(store, layout_type).to_px(abs_size_cross, 0.0); - parent_cross.clamp(child_min_cross, child_max_cross) - child_cross_before - child_cross_after + abs_size_cross.clamp(child_min_cross, child_max_cross) - child_cross_before - child_cross_after } else { - parent_cross + abs_size_cross }; let child_size = layout(child, layout_type, main, cross, cache, tree, store, sublayout); @@ -851,10 +1442,19 @@ where cross: computed_child_cross, main: computed_child_main, main_after: 0.0, + last_layout_main: 0.0, + last_layout_cross: 0.0, + has_layout_constraints: false, }); } - let alignment = node.alignment(store).unwrap_or_default(); + 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 + { + alignment = flip_alignment_horizontal(alignment); + } // Set size and position of children in the cache. let mut main_pos = padding_main_before + border_main_before; @@ -863,8 +1463,11 @@ where match child_position { PositionType::Absolute => { - let child_main_before = child.node.main_before(store, layout_type); - let child_main_after = child.node.main_after(store, layout_type); + let (child_main_before, child_main_after) = if is_rtl { + (child.node.main_after(store, layout_type), child.node.main_before(store, layout_type)) + } else { + (child.node.main_before(store, layout_type), child.node.main_after(store, layout_type)) + }; let child_cross_before = child.node.cross_before(store, layout_type); let child_cross_after = child.node.cross_after(store, layout_type); diff --git a/src/node.rs b/src/node.rs index 1a6b8349..4be70a06 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,4 +1,4 @@ -use crate::{layout, types::*, Cache}; +use crate::{layout, types::*, Cache, LayoutWrap}; /// A `Node` represents a layout element which can be sized and positioned based on /// a number of layout properties. @@ -51,7 +51,14 @@ pub trait Node: Sized { cache.set_bounds(self, cache.posx(self), cache.posy(self), width, height); - layout(self, LayoutType::Column, height, width, cache, tree, store, sublayout) + // Use the node's layout type instead of hardcoding Column + let layout_type = self.layout_type(store).unwrap_or_default(); + let (parent_main, parent_cross) = match layout_type { + LayoutType::Row | LayoutType::Grid => (width, height), // Row: 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) } /// Returns a key which can be used to set/get computed layout data from the [`cache`](crate::Cache). @@ -69,6 +76,18 @@ pub trait Node: Sized { /// Returns the position type of the node. fn position_type(&self, store: &Self::Store) -> Option; + /// Returns the inline direction used for horizontal positioning semantics. + fn direction(&self, _store: &Self::Store) -> Option { + None + } + + /// Returns whether children wrap to a new line when they overflow the main axis. + /// + /// Defaults to `None` which is treated as [`LayoutWrap::NoWrap`]. + fn wrap(&self, _store: &Self::Store) -> Option { + None + } + /// Returns the alignment of the node. fn alignment(&self, store: &Self::Store) -> Option; @@ -252,11 +271,25 @@ pub(crate) trait NodeExt: Node { } fn padding_main_before(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { - parent_layout_type.select_unwrap(store, |store| self.padding_left(store), |store| self.padding_top(store)) + if parent_layout_type == LayoutType::Row && self.direction(store).unwrap_or_default() == Direction::RightToLeft + { + self.padding_right(store).unwrap_or_default() + } else { + parent_layout_type.select_unwrap(store, |store| self.padding_left(store), |store| self.padding_top(store)) + } } fn padding_main_after(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { - parent_layout_type.select_unwrap(store, |store| self.padding_right(store), |store| self.padding_bottom(store)) + if parent_layout_type == LayoutType::Row && self.direction(store).unwrap_or_default() == Direction::RightToLeft + { + self.padding_left(store).unwrap_or_default() + } else { + parent_layout_type.select_unwrap( + store, + |store| self.padding_right(store), + |store| self.padding_bottom(store), + ) + } } fn padding_cross_before(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { @@ -287,8 +320,6 @@ pub(crate) trait NodeExt: Node { ) } - // Currently unused until wrapping is implemented - #[allow(dead_code)] fn cross_between(&self, store: &Self::Store, parent_layout_type: LayoutType) -> Units { parent_layout_type.select_unwrap(store, |store| self.vertical_gap(store), |store| self.horizontal_gap(store)) } diff --git a/src/types.rs b/src/types.rs index 74cac2e6..dc2a5e26 100644 --- a/src/types.rs +++ b/src/types.rs @@ -81,6 +81,25 @@ impl std::fmt::Display for PositionType { } } +/// The inline direction used for horizontal positioning semantics. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + /// Lay out inline horizontal semantics from left to right. + #[default] + LeftToRight, + /// Lay out inline horizontal semantics from right to left. + RightToLeft, +} + +impl std::fmt::Display for Direction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Direction::LeftToRight => write!(f, "ltr"), + Direction::RightToLeft => write!(f, "rtl"), + } + } +} + /// Units which describe spacing and size. #[derive(Default, Debug, Clone, Copy, PartialEq)] pub enum Units { @@ -206,6 +225,29 @@ impl std::fmt::Display for Alignment { } } } +/// Determines whether children wrap to a new line when they overflow the main axis. +/// +/// Wrapping is only applicable for [`LayoutType::Row`] and [`LayoutType::Column`] containers. +/// The cross-axis gap ([`vertical_gap`](crate::Node::vertical_gap) for Row, [`horizontal_gap`](crate::Node::horizontal_gap) for Column) +/// controls the spacing between lines. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum LayoutWrap { + /// Children do not wrap; they overflow the main axis (default). + #[default] + NoWrap, + /// Children wrap to a new line when they would overflow the main axis. + Wrap, +} + +impl std::fmt::Display for LayoutWrap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LayoutWrap::NoWrap => write!(f, "no-wrap"), + LayoutWrap::Wrap => write!(f, "wrap"), + } + } +} + /// A type which represents the computed size of a node after [`layout`](crate::Node::layout). #[derive(Default, Debug, Copy, Clone, PartialEq)] pub struct Size { diff --git a/tests/auto.rs b/tests/auto.rs index ec7082c0..2521e381 100644 --- a/tests/auto.rs +++ b/tests/auto.rs @@ -210,6 +210,40 @@ fn auto_min_width4() { assert_eq!(world.cache.bounds(child2), Some(&Rect { posx: 50.0, posy: 0.0, width: 50.0, height: 80.0 })); } +#[test] +fn auto_min_width_propagates_to_nested_children_after_flex_clamp() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(300.0)); + world.set_height(root, Units::Pixels(100.0)); + world.set_alignment(root, Alignment::TopLeft); + world.set_layout_type(root, LayoutType::Row); + + let child1 = world.add(Some(root)); + world.set_width(child1, Units::Stretch(1.0)); + world.set_min_width(child1, Units::Pixels(200.0)); + world.set_height(child1, Units::Stretch(1.0)); + world.set_layout_type(child1, LayoutType::Row); + + let grandchild1 = world.add(Some(child1)); + world.set_width(grandchild1, Units::Stretch(1.0)); + world.set_height(grandchild1, Units::Stretch(1.0)); + world.set_content_size(grandchild1, |_, width, height| (width.unwrap(), height.unwrap())); + + let child2 = world.add(Some(root)); + world.set_width(child2, Units::Stretch(1.0)); + world.set_min_width(child2, Units::Auto); + world.set_height(child2, Units::Stretch(1.0)); + world.set_content_size(child2, |_, _, height| (80.0, height.unwrap())); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + assert_eq!(world.cache.bounds(child1), Some(&Rect { posx: 0.0, posy: 0.0, width: 200.0, height: 100.0 })); + assert_eq!(world.cache.bounds(grandchild1), Some(&Rect { posx: 0.0, posy: 0.0, width: 200.0, height: 100.0 })); + assert_eq!(world.cache.bounds(child2), Some(&Rect { posx: 200.0, posy: 0.0, width: 100.0, height: 100.0 })); +} + // #[test] // fn percentage_left_pixels_width() { // let mut world = World::default(); diff --git a/tests/direction.rs b/tests/direction.rs new file mode 100644 index 00000000..80ad85b4 --- /dev/null +++ b/tests/direction.rs @@ -0,0 +1,87 @@ +use morphorm::*; +use morphorm_ecs::*; + +#[test] +fn rtl_reverses_row_order() { + let mut world = World::default(); + + let root = world.add(None); + world.set_layout_type(root, LayoutType::Row); + world.set_direction(root, Direction::RightToLeft); + world.set_width(root, Units::Pixels(300.0)); + world.set_height(root, Units::Pixels(100.0)); + + let first = world.add(Some(root)); + world.set_width(first, Units::Pixels(50.0)); + world.set_height(first, Units::Pixels(50.0)); + + let second = world.add(Some(root)); + world.set_width(second, Units::Pixels(50.0)); + world.set_height(second, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + assert_eq!(world.cache.bounds(first), Some(&Rect { posx: 250.0, posy: 0.0, width: 50.0, height: 50.0 })); + assert_eq!(world.cache.bounds(second), Some(&Rect { posx: 200.0, posy: 0.0, width: 50.0, height: 50.0 })); +} + +#[test] +fn rtl_swaps_row_padding_left_and_right() { + let mut world = World::default(); + + let root = world.add(None); + world.set_layout_type(root, LayoutType::Row); + world.set_direction(root, Direction::RightToLeft); + world.set_width(root, Units::Pixels(300.0)); + world.set_height(root, Units::Pixels(100.0)); + world.set_padding_left(root, Units::Pixels(20.0)); + world.set_padding_right(root, Units::Pixels(40.0)); + + let child = world.add(Some(root)); + world.set_width(child, Units::Pixels(50.0)); + world.set_height(child, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + assert_eq!(world.cache.bounds(child), Some(&Rect { posx: 230.0, posy: 0.0, width: 50.0, height: 50.0 })); +} + +#[test] +fn rtl_reverses_row_alignment_horizontally() { + let mut world = World::default(); + + let root = world.add(None); + world.set_layout_type(root, LayoutType::Row); + world.set_direction(root, Direction::RightToLeft); + world.set_alignment(root, Alignment::TopRight); + world.set_width(root, Units::Pixels(300.0)); + world.set_height(root, Units::Pixels(100.0)); + + let child = world.add(Some(root)); + world.set_width(child, Units::Pixels(50.0)); + world.set_height(child, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + assert_eq!(world.cache.bounds(child), Some(&Rect { posx: 0.0, posy: 0.0, width: 50.0, height: 50.0 })); +} + +#[test] +fn rtl_reverses_column_alignment_horizontally() { + let mut world = World::default(); + + let root = world.add(None); + world.set_layout_type(root, LayoutType::Column); + world.set_direction(root, Direction::RightToLeft); + world.set_alignment(root, Alignment::TopRight); + world.set_width(root, Units::Pixels(300.0)); + world.set_height(root, Units::Pixels(100.0)); + + let child = world.add(Some(root)); + world.set_width(child, Units::Pixels(50.0)); + world.set_height(child, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + assert_eq!(world.cache.bounds(child), Some(&Rect { posx: 0.0, posy: 0.0, width: 50.0, height: 50.0 })); +} diff --git a/tests/size_constraints.rs b/tests/size_constraints.rs index 3f8d1804..09e37ba9 100644 --- a/tests/size_constraints.rs +++ b/tests/size_constraints.rs @@ -286,7 +286,7 @@ fn min_width_auto_absolute() { world.set_width(node2, Units::Pixels(300.0)); world.set_height(node2, Units::Pixels(300.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: 300.0, height: 200.0 })); + assert_eq!(world.cache.bounds(node), Some(&Rect { posx: 0.0, posy: 0.0, width: 600.0, height: 600.0 })); assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 0.0, posy: 0.0, width: 300.0, height: 300.0 })); } @@ -311,7 +311,7 @@ fn min_height_auto_absolute() { world.set_height(node2, Units::Pixels(300.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: 200.0, height: 300.0 })); + assert_eq!(world.cache.bounds(node), Some(&Rect { posx: 0.0, posy: 0.0, width: 600.0, height: 600.0 })); assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 0.0, posy: 0.0, width: 300.0, height: 300.0 })); } @@ -337,7 +337,7 @@ fn min_size_auto_absolute() { world.set_height(node2, Units::Pixels(300.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: 300.0, height: 300.0 })); + assert_eq!(world.cache.bounds(node), Some(&Rect { posx: 0.0, posy: 0.0, width: 600.0, height: 600.0 })); assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 0.0, posy: 0.0, width: 300.0, height: 300.0 })); } diff --git a/tests/wrap.rs b/tests/wrap.rs new file mode 100644 index 00000000..2285d852 --- /dev/null +++ b/tests/wrap.rs @@ -0,0 +1,430 @@ +use morphorm::*; +use morphorm_ecs::*; + +#[test] +fn wrap_row_basic() { + // Test basic row wrapping - when wrap is enabled, items should stay on one line + // if they fit. Wrapping only occurs when items exceed available space. + 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 node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(100.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(100.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(100.0)); + world.set_height(node3, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // All items fit on one line (300px available, 300px used) + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 100.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 200.0, posy: 0.0, width: 100.0, height: 50.0 })); +} + +#[test] +fn wrap_row_with_gap() { + // Test row wrapping with horizontal gap between items and vertical gap between lines + 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); + world.set_horizontal_gap(root, Units::Pixels(20.0)); + world.set_vertical_gap(root, Units::Pixels(10.0)); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(80.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(80.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(80.0)); + world.set_height(node3, Units::Pixels(50.0)); + + let node4 = world.add(Some(root)); + world.set_width(node4, Units::Pixels(80.0)); + world.set_height(node4, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // First line: nodes 1, 2, 3 (80 + 20 + 80 + 20 + 80 = 280px fits in 300px) + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 80.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 100.0, posy: 0.0, width: 80.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 200.0, posy: 0.0, width: 80.0, height: 50.0 })); + + // Second line: node 4 (80px, offset by line_gap + line cross size) + assert_eq!(world.cache.bounds(node4), Some(&Rect { posx: 0.0, posy: 60.0, width: 80.0, height: 50.0 })); +} + +#[test] +fn wrap_column_basic() { + // Test basic column wrapping with fixed-size items that exceed container height + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(200.0)); + world.set_height(root, Units::Pixels(250.0)); + world.set_layout_type(root, LayoutType::Column); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_alignment(root, Alignment::TopLeft); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(80.0)); + world.set_height(node1, Units::Pixels(100.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(80.0)); + world.set_height(node2, Units::Pixels(100.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(80.0)); + world.set_height(node3, Units::Pixels(100.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // First column: nodes 1 and 2 (200px height fits in 250px) + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 80.0, height: 100.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 0.0, posy: 100.0, width: 80.0, height: 100.0 })); + + // Second column: node 3 + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 80.0, posy: 0.0, width: 80.0, height: 100.0 })); +} + +#[test] +fn wrap_row_with_stretch() { + // Test row wrapping with stretch items filling available space on each line + 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 node1 = world.add(Some(root)); + world.set_width(node1, Units::Stretch(1.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Stretch(1.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(100.0)); + world.set_height(node3, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // With wrapping, stretch items treat min size (0) for line-break decision + // node1 and node2 both stretch to fill (no size contribution to line break) + // All items fit on one line + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 100.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 200.0, posy: 0.0, width: 100.0, height: 50.0 })); +} + +#[test] +fn wrap_row_no_wrap_mode() { + // Test that NoWrap mode (default) doesn't wrap items + 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::NoWrap); + world.set_alignment(root, Alignment::TopLeft); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(100.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(100.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(100.0)); + world.set_height(node3, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // All items on one line (no wrapping, even though they sum to 300px) + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 100.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 200.0, posy: 0.0, width: 100.0, height: 50.0 })); +} + +#[test] +fn wrap_row_single_item_per_line() { + // Test wrapping where items are so large that only one fits per line + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(250.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 node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(200.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(200.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(200.0)); + world.set_height(node3, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // Each item on its own line + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 200.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 0.0, posy: 50.0, width: 200.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 0.0, posy: 100.0, width: 200.0, height: 50.0 })); +} + +#[test] +fn wrap_with_alignment() { + // Test wrapping with center alignment + 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::Center); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(100.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(100.0)); + world.set_height(node2, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // Items centered on first line: (300 - 200) / 2 = 50px offset on main axis + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 50.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 150.0, posy: 0.0, width: 100.0, height: 50.0 })); +} + +#[test] +fn wrap_auto_container() { + // Test wrap with fixed-size container and fixed-size children + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(250.0)); + world.set_height(root, Units::Pixels(250.0)); + world.set_layout_type(root, LayoutType::Row); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_alignment(root, Alignment::TopLeft); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(100.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(100.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(100.0)); + world.set_height(node3, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // First line: nodes 1 and 2 (200px fits in 250px) + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 100.0, posy: 0.0, width: 100.0, height: 50.0 })); + + // Second line: node 3 wraps (adding it would be 300px > 250px) + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 0.0, posy: 50.0, width: 100.0, height: 50.0 })); +} + +// Regression test for wrapping when items on different lines have different heights. +// Verifies line assignment and vertical positioning use each line's maximum height. + +#[test] +fn wrap_with_different_line_heights() { + // Test wrapping where items have different heights + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(250.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 node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(100.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(100.0)); + world.set_height(node2, Units::Pixels(80.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(100.0)); + world.set_height(node3, Units::Pixels(60.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // First line: nodes 1 and 2 (200px fits in 250px, line height is max = 80px) + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 100.0, posy: 0.0, width: 100.0, height: 80.0 })); + + // Second line: node 3 (line height is 60px) + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 0.0, posy: 80.0, width: 100.0, height: 60.0 })); +} + +#[test] +fn wrap_row_with_padding() { + // Test wrapping with padding on the container + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(320.0)); + world.set_height(root, Units::Pixels(320.0)); + world.set_padding_left(root, Units::Pixels(10.0)); + world.set_padding_right(root, Units::Pixels(10.0)); + world.set_padding_top(root, Units::Pixels(10.0)); + world.set_padding_bottom(root, Units::Pixels(10.0)); + world.set_layout_type(root, LayoutType::Row); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_alignment(root, Alignment::TopLeft); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(100.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(100.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(100.0)); + world.set_height(node3, Units::Pixels(50.0)); + + let node4 = world.add(Some(root)); + world.set_width(node4, Units::Pixels(100.0)); + world.set_height(node4, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // Available width is 320 - 10 - 10 = 300px + // First line: nodes 1, 2, 3 (300px fills the available space) + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 10.0, posy: 10.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 110.0, posy: 10.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 210.0, posy: 10.0, width: 100.0, height: 50.0 })); + + // Second line: node 4 wraps (adding it would exceed 300px) + assert_eq!(world.cache.bounds(node4), Some(&Rect { posx: 10.0, posy: 60.0, width: 100.0, height: 50.0 })); +} + +#[test] +fn wrap_row_rtl() { + // Test row wrapping with right-to-left direction + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(250.0)); + world.set_height(root, Units::Pixels(300.0)); + world.set_layout_type(root, LayoutType::Row); + world.set_direction(root, Direction::RightToLeft); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_alignment(root, Alignment::TopLeft); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Pixels(100.0)); + world.set_height(node1, Units::Pixels(50.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Pixels(100.0)); + world.set_height(node2, Units::Pixels(50.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Pixels(100.0)); + world.set_height(node3, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // RTL: wrapped line composition stays the same, but placement order per line is reversed. + // With TopLeft alignment flipped to TopRight in RTL: + // - Line 1: node1 (100px) + node2 (100px) = 200px fits in 250px + // Free space on left (50px), reversed placement: node2 at 50-150, node1 at 150-250 + // - Line 2: node3 (100px) alone, with free space on left (150px) + // node3 on right: node3 at 150-250 + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 150.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 50.0, posy: 0.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 150.0, posy: 50.0, width: 100.0, height: 50.0 })); +} + +#[test] +fn wrap_row_auto_height_includes_lines_gap_and_padding() { + 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_layout_type(root, LayoutType::Column); + world.set_alignment(root, Alignment::TopLeft); + + let wrap = world.add(Some(root)); + world.set_width(wrap, Units::Pixels(250.0)); + world.set_height(wrap, Units::Auto); + world.set_layout_type(wrap, LayoutType::Row); + world.set_wrap(wrap, LayoutWrap::Wrap); + world.set_alignment(wrap, Alignment::TopLeft); + world.set_padding_top(wrap, Units::Pixels(5.0)); + world.set_padding_bottom(wrap, Units::Pixels(5.0)); + world.set_vertical_gap(wrap, Units::Pixels(10.0)); + + let a = world.add(Some(wrap)); + world.set_width(a, Units::Pixels(100.0)); + world.set_height(a, Units::Pixels(50.0)); + + let b = world.add(Some(wrap)); + world.set_width(b, Units::Pixels(100.0)); + world.set_height(b, Units::Pixels(50.0)); + + let c = world.add(Some(wrap)); + world.set_width(c, Units::Pixels(100.0)); + world.set_height(c, Units::Pixels(50.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // Two lines: [a, b] then [c]. + // Height = 50 + 10 + 50 + 5 + 5 = 120. + assert_eq!(world.cache.bounds(wrap), Some(&Rect { posx: 0.0, posy: 0.0, width: 250.0, height: 120.0 })); + assert_eq!(world.cache.bounds(a), Some(&Rect { posx: 0.0, posy: 5.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(b), Some(&Rect { posx: 100.0, posy: 5.0, width: 100.0, height: 50.0 })); + assert_eq!(world.cache.bounds(c), Some(&Rect { posx: 0.0, posy: 65.0, width: 100.0, height: 50.0 })); +}