diff --git a/src/layout.rs b/src/layout.rs index 28c67bfc..6f155ef7 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -796,19 +796,82 @@ where 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); + let mut remaining_main = (avail_main - fixed_sum - gap_total).max(0.0); + let mut remaining_stretch_sum = stretch_sum; + + #[derive(Clone, Copy)] + struct StretchAlloc { + index: usize, + factor: f32, + min: f32, + max: f32, + frozen: bool, + measured: f32, + computed: f32, + violation: f32, + } + let mut stretch_items: SmallVec<[StretchAlloc; 16]> = SmallVec::new(); 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; + if items[i].stretch_main_factor > 0.0 { + stretch_items.push(StretchAlloc { + index: i, + factor: items[i].stretch_main_factor, + min: items[i].min_main, + max: items[i].max_main, + frozen: false, + measured: 0.0, + computed: 0.0, + violation: 0.0, + }); + } + } + + while !stretch_items.iter().all(|item| item.frozen) { + let mut total_violation = 0.0f32; + + for item in stretch_items.iter_mut().filter(|item| !item.frozen) { + let measured = if remaining_stretch_sum > 0.0 { + (item.factor * remaining_main / remaining_stretch_sum).round() + } else { + 0.0 + }; + let computed = measured.clamp(item.min, item.max); + + item.measured = measured; + item.computed = computed; + item.violation = computed - measured; + total_violation += item.violation; + } + + for item in stretch_items.iter_mut().filter(|item| !item.frozen) { + item.frozen = match total_violation { + total if total > 0.0 => item.violation > 0.0, + total if total < 0.0 => item.violation < 0.0, + _ => true, + }; + + if item.frozen { + remaining_stretch_sum -= item.factor; + remaining_main = (remaining_main - item.computed).max(0.0); + } } } + + for item in stretch_items.iter() { + let size = layout( + relative_children[item.index], + layout_type, + item.computed, + avail_cross, + cache, + tree, + store, + sublayout, + ); + items[item.index].main = size.main; + items[item.index].cross = size.cross; + } } } @@ -827,25 +890,42 @@ where } // Phase 5: Resolve cross-stretch children to fill their line's cross extent. + // This runs in two passes: + // 1) stretch against the current estimate, + // 2) stretch again against the settled line max. + // The second pass is required when all line items are cross-stretch and one + // item's min/max constraint determines the line cross-size. 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; + + let resolve_stretch_line = |target_cross: f32, + items: &mut SmallVec<[WrapItem; 32]>, + cache: &mut C, + sublayout: &mut ::SubLayout<'_>| { + for i in start..end { + if items[i].cross_is_stretch { + let child = relative_children[i]; + let clamped_cross = target_cross.clamp(items[i].min_cross, items[i].max_cross); + let size = layout(child, layout_type, items[i].main, clamped_cross, cache, tree, store, sublayout); + 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; + }; + + let compute_line_max = |items: &SmallVec<[WrapItem; 32]>| { + let mut max_cross = 0.0f32; + for i in start..end { + max_cross = max_cross.max(items[i].cross); + } + max_cross + }; + + resolve_stretch_line(line_cross[line_idx], &mut items, cache, sublayout); + let settled_cross = compute_line_max(&items); + resolve_stretch_line(settled_cross, &mut items, cache, sublayout); + line_cross[line_idx] = compute_line_max(&items); } // Phase 6: Determine the final cross size of the container. @@ -1271,8 +1351,8 @@ where let is_row_rtl = layout_type == LayoutType::Row && node.direction(store).unwrap_or_default() == Direction::RightToLeft; - let is_rtl = matches!(layout_type, LayoutType::Row | LayoutType::Column) - && node.direction(store).unwrap_or_default() == Direction::RightToLeft; + let is_column_rtl = + layout_type == LayoutType::Column && node.direction(store).unwrap_or_default() == Direction::RightToLeft; if is_row_rtl { relative_children.reverse(); @@ -1774,12 +1854,14 @@ where match child_position { PositionType::Absolute => { + // RTL mirrors horizontal offsets only. + // In rows, horizontal is the main axis; in columns, horizontal is the cross axis. let (child_main_before, child_main_after) = if is_row_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_cross_after) = if is_rtl && layout_type == LayoutType::Column { + let (child_cross_before, child_cross_after) = if is_column_rtl { (child.node.cross_after(store, layout_type), child.node.cross_before(store, layout_type)) } else { (child.node.cross_before(store, layout_type), child.node.cross_after(store, layout_type)) diff --git a/tests/wrap.rs b/tests/wrap.rs index e6e2d394..642bd8ca 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -167,6 +167,185 @@ fn wrap_row_with_stretch() { assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 200.0, posy: 0.0, width: 100.0, height: 50.0 })); } +#[test] +fn wrap_row_stretch_children_resolve_to_line_max_cross() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(260.0)); + world.set_height(root, Units::Pixels(220.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(10.0)); + world.set_vertical_gap(root, Units::Pixels(8.0)); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Stretch(1.0)); + world.set_height(node1, Units::Stretch(1.0)); + world.set_min_width(node1, Units::Pixels(80.0)); + world.set_min_height(node1, Units::Pixels(30.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Stretch(1.0)); + world.set_height(node2, Units::Stretch(1.0)); + world.set_min_width(node2, Units::Pixels(120.0)); + world.set_min_height(node2, Units::Pixels(70.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Stretch(1.0)); + world.set_height(node3, Units::Stretch(1.0)); + world.set_min_width(node3, Units::Pixels(80.0)); + world.set_min_height(node3, Units::Pixels(30.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // First line should resolve to node2's larger min-height, and node1 should + // stretch to that same line height. + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 0.0, posy: 0.0, width: 125.0, height: 70.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 135.0, posy: 0.0, width: 125.0, height: 70.0 })); + + // Second line starts after first line cross size plus vertical gap. + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 0.0, posy: 78.0, width: 260.0, height: 30.0 })); +} + +#[test] +fn wrap_row_stretch_mixed_min_cross_respects_parent_padding() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(280.0)); + world.set_height(root, Units::Pixels(240.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); + world.set_horizontal_gap(root, Units::Pixels(10.0)); + world.set_vertical_gap(root, Units::Pixels(8.0)); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Stretch(1.0)); + world.set_height(node1, Units::Stretch(1.0)); + world.set_min_width(node1, Units::Pixels(80.0)); + world.set_min_height(node1, Units::Pixels(30.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Stretch(1.0)); + world.set_height(node2, Units::Stretch(1.0)); + world.set_min_width(node2, Units::Pixels(120.0)); + world.set_min_height(node2, Units::Pixels(70.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Stretch(1.0)); + world.set_height(node3, Units::Stretch(1.0)); + world.set_min_width(node3, Units::Pixels(80.0)); + world.set_min_height(node3, Units::Pixels(30.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // Available width = 280 - left/right padding (20) = 260. + // Line 1 has node1 + gap + node2; line height settles to 70. + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 10.0, posy: 10.0, width: 125.0, height: 70.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 145.0, posy: 10.0, width: 125.0, height: 70.0 })); + + // Line 2 starts at padding_top + first_line_height + vertical_gap. + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 10.0, posy: 88.0, width: 260.0, height: 30.0 })); +} + +#[test] +fn wrap_column_stretch_mixed_min_cross_respects_parent_padding() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(240.0)); + world.set_height(root, Units::Pixels(280.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::Column); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_alignment(root, Alignment::TopLeft); + world.set_vertical_gap(root, Units::Pixels(10.0)); + world.set_horizontal_gap(root, Units::Pixels(8.0)); + + let node1 = world.add(Some(root)); + world.set_width(node1, Units::Stretch(1.0)); + world.set_height(node1, Units::Stretch(1.0)); + world.set_min_width(node1, Units::Pixels(30.0)); + world.set_min_height(node1, Units::Pixels(80.0)); + + let node2 = world.add(Some(root)); + world.set_width(node2, Units::Stretch(1.0)); + world.set_height(node2, Units::Stretch(1.0)); + world.set_min_width(node2, Units::Pixels(70.0)); + world.set_min_height(node2, Units::Pixels(120.0)); + + let node3 = world.add(Some(root)); + world.set_width(node3, Units::Stretch(1.0)); + world.set_height(node3, Units::Stretch(1.0)); + world.set_min_width(node3, Units::Pixels(30.0)); + world.set_min_height(node3, Units::Pixels(80.0)); + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // Available height = 280 - top/bottom padding (20) = 260. + // First column has node1 + gap + node2; column width settles to 70. + assert_eq!(world.cache.bounds(node1), Some(&Rect { posx: 10.0, posy: 10.0, width: 70.0, height: 125.0 })); + assert_eq!(world.cache.bounds(node2), Some(&Rect { posx: 10.0, posy: 145.0, width: 70.0, height: 125.0 })); + + // Second column starts at padding_left + first_column_width + horizontal_gap. + assert_eq!(world.cache.bounds(node3), Some(&Rect { posx: 88.0, posy: 10.0, width: 30.0, height: 260.0 })); +} + +#[test] +fn wrap_row_stretch_large_min_main_preserves_line_padding() { + let mut world = World::default(); + + let root = world.add(None); + world.set_width(root, Units::Pixels(872.0)); + world.set_height(root, Units::Pixels(934.0)); + world.set_layout_type(root, LayoutType::Row); + world.set_wrap(root, LayoutWrap::Wrap); + world.set_alignment(root, Alignment::TopLeft); + 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_horizontal_gap(root, Units::Pixels(10.0)); + world.set_vertical_gap(root, Units::Pixels(10.0)); + + let mins = [180.0, 180.0, 180.0, 300.0, 180.0, 180.0, 180.0]; + let mut nodes = Vec::new(); + for min in mins { + let node = world.add(Some(root)); + world.set_width(node, Units::Stretch(1.0)); + world.set_height(node, Units::Stretch(1.0)); + world.set_min_width(node, Units::Pixels(min)); + world.set_min_height(node, Units::Pixels(min)); + nodes.push(node); + } + + root.layout(&mut world.cache, &world.tree, &world.store, &mut ()); + + // First line: three equal stretch children. + assert_eq!(world.cache.bounds(nodes[0]), Some(&Rect { posx: 10.0, posy: 10.0, width: 277.0, height: 180.0 })); + assert_eq!(world.cache.bounds(nodes[1]), Some(&Rect { posx: 297.0, posy: 10.0, width: 277.0, height: 180.0 })); + assert_eq!(world.cache.bounds(nodes[2]), Some(&Rect { posx: 584.0, posy: 10.0, width: 277.0, height: 180.0 })); + + // Second line: larger min-width item should clamp to 300 and siblings re-resolve + // so the line still respects right padding. + assert_eq!(world.cache.bounds(nodes[3]), Some(&Rect { posx: 10.0, posy: 200.0, width: 300.0, height: 300.0 })); + assert_eq!(world.cache.bounds(nodes[4]), Some(&Rect { posx: 320.0, posy: 200.0, width: 266.0, height: 300.0 })); + assert_eq!(world.cache.bounds(nodes[5]), Some(&Rect { posx: 596.0, posy: 200.0, width: 266.0, height: 300.0 })); + + // Third line. + assert_eq!(world.cache.bounds(nodes[6]), Some(&Rect { posx: 10.0, posy: 510.0, width: 852.0, height: 180.0 })); +} + #[test] fn wrap_row_no_wrap_mode() { // Test that NoWrap mode (default) doesn't wrap items