From 9475a815daf0365cb7ae7b32af8a54de80ebe522 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 22:25:33 -0500 Subject: [PATCH] fix(layout): end the fill distribution loop when no child can freeze (#1926) Each iteration froze the children whose clamping passed a 1e-9 tolerance, but it compared the aggregate violation against the same tolerance: several clampings each under it could add up past it, so no child froze, the pending set never shrank and the loop never ended. An iteration that freezes nothing now settles the remaining children as clamped. --- crates/layout/src/lib.rs | 12 ++++++++++++ crates/layout/src/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 19de84afb..33ad233ff 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -629,14 +629,26 @@ fn distribute(items: &mut [Item], mut open: Vec, free: f64) { .collect(); let violation: f64 = sized.iter().map(|(_, want, v)| v - want).sum(); let frozen = |want: f64, v: f64| violation.abs() < 1e-9 || (violation > 0.0 && v > want + 1e-9) || (violation < 0.0 && v < want - 1e-9); + let mut progressed = false; for (i, want, v) in &sized { if frozen(*want, *v) && let Some(it) = items.get_mut(*i) { it.main = *v; free -= *v; + progressed = true; } } + if !progressed { + // Several clampings each under the tolerance added up past it, so none froze: settle + // them all as clamped rather than go round again with nothing changed. + for (i, _, v) in &sized { + if let Some(it) = items.get_mut(*i) { + it.main = *v; + } + } + break; + } free = free.max(0.0); open = sized.into_iter().filter(|(_, want, v)| !frozen(*want, *v)).map(|(i, _, _)| i).collect(); } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 0af9861b8..d48c1fb8d 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -447,3 +447,24 @@ fn grid_hostile_values() { assert!(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()); } } + +#[test] +fn fill_children_whose_tiny_clampings_add_up_past_the_tolerance_still_settle() { + // Three fill children sharing 300 px, each with a minimum 5e-10 over its 100 px share: no + // single clamping passes the 1e-9 tolerance, but together they do, so no child froze and the + // distribution loop went round forever without changing anything. + let mut d = Document::new("t"); + let p = d.first_page().unwrap(); + let f = d.create(p, NodeKind::Frame, "F", 0.0, 0.0, 300.0, 100.0).unwrap(); + d.get_mut(f).unwrap().layout = Some(AutoLayout { mode: LayoutMode::Horizontal, gap: 0.0, padding: [0.0; 4], ..Default::default() }); + let kids: Vec = (0..3).map(|i| d.create(f, NodeKind::Rectangle, "K", 0.0, 0.0, 10.0, 20.0 + i as f64).unwrap()).collect(); + for k in &kids { + let n = d.get_mut(*k).unwrap(); + n.child_layout.sizing_h = Sizing::Fill; + n.child_layout.min_w = Some(100.0 + 5e-10); + } + layout_frame(&mut d, f, &NoMeasure); + for k in &kids { + assert!((pos(&d, *k).2 - 100.0).abs() < 1e-6, "{:?}", pos(&d, *k)); + } +}