From c596bf12c618307bacfddec9fb2600ca8b923d56 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Tue, 15 Sep 2026 01:01:57 -0400 Subject: [PATCH] Measure a child in the length its parent declared, not the box it was offered `SetSize` drew its child in whatever box it had been given and then reported its declared length, so the child answered about a box it was never going to have -- and the answer on the *other* axis was taken under that. A wrapping text under `SetSize(x: 76px)` was measured in the whole 640 available, reported one line, and the parent sized itself to one line. The text was then drawn again at 76 and reported two, but by then its box was settled and nothing revisited it. A repaint put it right, which is why the first frame and the second disagreed. So the layout was not a function of the state, and "cold" was not a fixed point -- which means the warm-against-cold oracle has been measuring against a tree that had not settled, and some of what it reported as a retained-layout defect was the cold side being wrong. Nothing about retained state is involved in this: it reproduces in six widgets on a first frame. The declared length is what the child gets, so that is where it is measured. `apply_rest` carries `rel` and `rest` through unchanged, and a `px` length composes as an offset, so the child's box does not move again when this widget's own box shrinks to what it declared. `tests/unsettled.rs` passes, and the generated sweep now passes at depth 5 where it failed. Depth 6 and 7 still fail; there is more than one of these. Co-Authored-By: Claude Opus 5 --- src/widget/position/set_size.rs | 15 ++++- tests/trace_unsettled.rs | 97 +++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/trace_unsettled.rs diff --git a/src/widget/position/set_size.rs b/src/widget/position/set_size.rs index ffb4e52..8ec989e 100644 --- a/src/widget/position/set_size.rs +++ b/src/widget/position/set_size.rs @@ -8,7 +8,20 @@ pub struct SetSize { impl Widget for SetSize { fn draw(&mut self, painter: &mut Painter) -> Size { - let child = painter.widget(&self.inner).size(); + // A declared length is what the child gets, whatever box this widget + // was offered before its parent knew that. Measuring it anywhere else + // asks about a box it will not have, and the answer on the other axis + // is taken under that: a wrapping text measured in the whole width + // reports one line, and nothing revisits it once the real width + // arrives. + let mut region = UiRegion::FULL; + for (axis, len) in [(Axis::X, self.x), (Axis::Y, self.y)] { + if let Some(len) = len { + let span = region.axis_mut(axis); + span.end = span.start + len.apply_rest(); + } + } + let child = painter.widget_within(&self.inner, region).size(); Size { x: self.x.unwrap_or(child.x), y: self.y.unwrap_or(child.y), diff --git a/tests/trace_unsettled.rs b/tests/trace_unsettled.rs new file mode 100644 index 0000000..ea61ef6 --- /dev/null +++ b/tests/trace_unsettled.rs @@ -0,0 +1,97 @@ +//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is +//! actually drawn in on a first frame against a settled one. + +#![cfg(feature = "layout-diagnostics")] + +use iris::core::layout_diagnostics::{self as diag, TraceEvent}; +use iris::harness::Harness; +use iris::prelude::*; + +fn plant(h: &mut Harness) -> Vec { + let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); + let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); + let sized = SetSize { + inner: wrapped.add_strong(&mut h.rsc), + x: Some(Len::px(76.0)), + y: None, + } + .add(&mut h.rsc); + let aligned = Aligned { + inner: sized.add_strong(&mut h.rsc), + align: Align { + x: Some(AxisAlign::Pos), + y: Some(AxisAlign::Pos), + }, + } + .add(&mut h.rsc); + let stack = Stack { + children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], + size: StackSize::Child(0), + } + .add(&mut h.rsc); + let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); + h.state.root = Some(root.add_strong(&mut h.rsc)); + vec![ + plain.id(), + wrapped.id(), + sized.id(), + aligned.id(), + stack.id(), + root.id(), + ] +} + +fn dump(label: &str, report: &diag::Report, text: WidgetId) { + println!("--- {label} ---"); + for event in report.traces() { + match event { + TraceEvent::DrawRequest { + id, + region, + pixel_size, + .. + } if *id == text => { + println!( + " draw in {:.2}x{:.2} region {region:?}", + pixel_size.x, pixel_size.y + ) + } + TraceEvent::SizeReported { id, size } if *id == text => { + println!(" reported {size}") + } + TraceEvent::SizeRead { id, reader, size } if *id == text => { + println!(" size read by {reader:?}: {size}") + } + TraceEvent::Placed { id, parent, region } if *id == text => { + println!(" placed by {parent:?} at {region:?}") + } + TraceEvent::Reuse { id, outcome } if *id == text => println!(" reuse: {outcome:?}"), + _ => {} + } + } +} + +#[test] +#[ignore = "a diagnostic, not a check"] +fn what_box_the_text_is_drawn_in() { + diag::clear_traced_widgets(); + let _ = diag::take(); + let mut h = Harness::new((640, 900)); + let ids = plant(&mut h); + let text = ids[1]; + diag::trace_widget(text); + let _ = diag::take(); + + h.frame(); + dump("first frame", &diag::take(), text); + + for _ in 0..2 { + for &id in &ids { + h.rsc.widgets_mut().get_dyn_mut(id); + } + let _ = diag::take(); + h.frame(); + dump("repaint", &diag::take(), text); + } + diag::clear_traced_widgets(); +}