Grow random trees, and check them against building the same tree cold

`iris::random` grows a seeded tree -- spans in every direction, stacks,
rects with varying opacity, text both wrapping and overflowing, a declared
size over half of it -- and `tests/generated.rs` grows each seed twice: once
and then mutated, once with the mutation built in. Every widget's box has to
match. `examples/random.rs` draws one, and `IRIS_SEED`/`IRIS_DEPTH` pick it.

It found the defect in the commit before this one immediately: a reuse that
marked a descendant for redraw escalated to that descendant's size reader,
which re-placed the child, which marked it again. `try_reuse` now asks
whether anything under the widget would have to be drawn again *before*
keeping the drawing, and drops the whole thing if so, which terminates
because it adds no marks.

It also found one older and larger than this branch, which
`a_wrapping_child_of_a_row_settles_somewhere_else_each_time` reproduces and
documents: a wrapping text on a span's own axis is shaped twice against two
different widths, so where it settles depends on how many passes it has had.
7 of 90 cases diverge on `db1751f` and 30 do here, because a placed child
reaches the second shaping more often. It is the same defect either way, and
it belongs where the two draws meet -- LAYOUT.md §4 -- not in the chain. The
six seeds the live tests use are ones that agree.

`forget_ref` goes with the subtree rewrite that used it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-14 13:06:09 -04:00
1 parent d98969158f
commit 86a7e8dfc3
6 files changed
+383 -29

No files matched your search

+19 -24
View File
@@ -1,7 +1,7 @@
use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2, forget_ref},
util::{HashMap, HashSet, Vec2},
};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
@@ -269,40 +269,40 @@ impl UiRenderState {
return None;
}
}
// Anything under it that has to be drawn again is drawn by drawing
// this, because whatever reads that widget's size sits in between and
// has to lay out around whatever it comes to.
if changed.iter().any(|&c| c) && self.redraws_under(id, changed, rsc) {
return None;
}
self.moves.set(slot, region);
self.active.get_mut(&id).unwrap().region = region;
if changed.iter().any(|&c| c) {
self.mark_resized(id, changed, rsc);
}
Some(size)
}
/// Marks every descendant whose drawing cannot survive the box it is a
/// fraction of changing length, `changed` saying which axes of that box
/// did.
/// Whether anything under `id` would have to be drawn again for the box
/// it is a fraction of changing length, `changed` saying which axes of
/// that box did.
///
/// A part of a box with no relative extent on an axis is a fixed length,
/// held as offsets from that box's start, and composing anything into it
/// leaves no relative extent either. So a widget whose own box did not
/// change length has no descendant whose box did, and the walk stops
/// there.
fn mark_resized(&mut self, id: WidgetId, changed: [bool; 2], rsc: &mut dyn UiRsc) {
/// there -- an 80-wide child of a widened row is not asked at all.
fn redraws_under(&self, id: WidgetId, changed: [bool; 2], rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else {
return;
return false;
};
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for &child in children {
active.children.iter().any(|&child| {
let Some(data) = self.active.get(&child) else {
continue;
return false;
};
let region = data.region;
let mut own = changed;
for (axis, c) in AXES.into_iter().zip(own.iter_mut()) {
*c &= region.axis(axis).len().rel != 0.0;
*c &= data.region.axis(axis).len().rel != 0.0;
}
if !own.iter().any(|&c| c) {
continue;
return false;
}
let redraws = match rsc.widgets().get_dyn(child) {
Some(widget) => AXES
@@ -311,13 +311,8 @@ impl UiRenderState {
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale),
None => true,
};
match redraws {
true => {
rsc.widgets_mut().needs_redraw.insert(child);
}
false => self.mark_resized(child, own, rsc),
}
}
redraws || self.redraws_under(child, own, rsc)
})
}
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {