Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 109 additions & 27 deletions src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand All @@ -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 <N as Node>::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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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))
Expand Down
179 changes: 179 additions & 0 deletions tests/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading