From bf9438087a6aad44f57b04db8460552ad3be0325 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Mon, 14 Sep 2026 19:57:08 -0400 Subject: [PATCH] Say what the settle order is holding up `try_reuse` asks whether the widget in front of it is dirty and, if not, hands its parent the size it last reported. Nothing asks whether a dirty widget sits under it through the size dependencies -- which is the check `retained_size` makes, for exactly this reason, on the path that does not draw. What covers the gap is the order `redraw_updates` settles in: taking the deepest dirty widget first means that by the time a reader draws, what it reads has already drawn and propagated. Drawing in any other order returns a stale size. Measured rather than reasoned: picking whatever the dirty set yields first fails seed 2 of `tests/generated.rs` with 24 widgets wrong, a subtree keeping a 317 px width where a cold tree has 147, and the traces are identical until a `Span` reports 317 against 147 from the same child sizes -- it had reused a subtree holding a `SetSize` whose declared width had changed. So the coupling is real and was written down nowhere. Say it in both places, since a reader of either would otherwise conclude the order is about cost. Co-Authored-By: Claude Opus 5 --- core/src/ui/render_state.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 06bc266..376216c 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -388,6 +388,12 @@ impl UiRenderState { ) -> Option { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReuseAttempts); + // Only its own dirtiness, not anything dirty under it that could + // change the size this hands back. What makes that safe is the order + // `redraw_updates` settles in, and nothing else: by the time a reader + // draws, everything dirty below it has been drawn and has propagated. + // Draw in another order and this returns a stale size -- measured, on + // seed 2 of `tests/generated.rs`. if rsc.widgets().needs_redraw.contains(&id) { #[cfg(feature = "layout-diagnostics")] { @@ -587,8 +593,11 @@ impl UiRenderState { #[cfg(feature = "layout-diagnostics")] let _layout = diag::timer(TimerKind::IncrementalLayout); // A reader's answer is only valid after every dirty size it reads has - // settled. Equal-depth widgets are independent, so their order does - // not matter. Resize dirtiness already marks whole reader chains, so + // settled, and taking the deepest first is what arranges that -- + // `try_reuse` hands back a retained size without asking whether + // anything dirty sits under it, so this order is load-bearing for the + // answer and not only for the cost. Equal-depth widgets are + // independent, so their order does not matter. Resize dirtiness already marks whole reader chains, so // choosing their shallowest roots coalesces descendants that share a // reader and gives each changing box its final constraints first. while let Some(id) = {