Give a child a part of the container's extent rather than its raw box

`Pad` and `Stack` read `Painter::placement` to put their children inside
their own drawing, and reading it is what says the drawing holds for that
placement alone. So a pad or a stack anywhere in a row was drawn again --
with its whole subtree -- the moment an earlier sibling changed length,
however little else had moved.

`widget_within` now takes a `DrawRegion`, and `DrawRegion::Extent(part)`
gives the child a part of the extent without reading it. What is retained
is the part rather than the box it resolved to, so moving the extent
re-places the child through the same rule instead of redrawing the parent:
`inherited_children` becomes `extent_children`, carrying `Inherit` for the
wrapper case `Painter::widget` already had and `Within(part)` for the new
one.

The dependency that goes up is a range on the container's extent rather
than on its frame, since only the part's *length* reaches the child and
where the part sits is re-placed. A declared length is unchanged: it is a
length of the frame wherever the box it sits in came from. What still pins
the placement is a report with a fraction in it -- the same fraction of a
different extent is a different length -- and that pin is on the answer,
which `extent_frames_keep_fractional_reports_and_numeric_dependencies_valid`
fails without.

Three tests from the first attempt at this come with it, and the
diagnostics rig now says which of the three contracts refused a reuse,
which is what found the above.

Measured, seed 1 at depth 8, median frame: `many` 0.667 -> 0.613 ms and
`resize` 48 -> 32 us; seed 13's `many` 6.35 -> 5.15 ms. Green: fmt, clippy,
109 suite and 20 core tests, the oracle at 100 seeds, the shrinker at 400
trees of depth 5, 1000 seeds at depth 6, and 2000 seeds at depth 4 over all
fifteen cases. The five reference renders are byte-identical to `0e107f0`
on Venus, as are `tabs` resized to 900x1200 and `random` to 1280x800
against cold renders there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-17 19:15:17 -04:00
1 parent 0e107f0e89
commit e6ba570d07
7 files changed
+358 -57

No files matched your search

+32 -15
View File
@@ -288,7 +288,7 @@ impl UiRenderState {
&& let Some(old_parent) = self.active.get_mut(&old_parent)
{
old_parent.children.retain(|child| *child != id);
old_parent.inherited_children.retain(|child| *child != id);
old_parent.extent_children.retain(|(child, _)| *child != id);
}
(answer.0, answer.1, settled.1)
}
@@ -361,7 +361,7 @@ impl UiRenderState {
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
extent_children: Vec::new(),
children: Vec::new(),
offered: Vec::new(),
offered_px: info.offered_px,
@@ -398,7 +398,7 @@ impl UiRenderState {
textures,
primitives,
mask_region,
inherited_children,
extent_children,
extent_own,
answer_under,
children,
@@ -503,7 +503,7 @@ impl UiRenderState {
textures,
primitives,
mask_region,
inherited_children,
extent_children,
children,
size_deps,
declared: declared_lens(rsc.widgets(), id),
@@ -680,6 +680,24 @@ impl UiRenderState {
if !active.holds.contains(info.px, placement) {
#[cfg(feature = "layout-diagnostics")]
{
// Which of the three said no, so a frame that redraws more
// than it should says where to look. They overlap: a drawing
// can be outside two of them at once.
let holds = active.holds;
if holds.placement.is_some_and(|pinned| pinned != placement) {
diag::bump(Counter::OutsidePlacement);
}
for axis in AXES {
let n = axis as usize;
if !holds.frame[n].contains(info.px.axis(axis)) {
diag::bump(Counter::OutsideFrame);
}
if !holds.extent[n]
.contains(placement.axis(axis).len().to_px(info.px.axis(axis)))
{
diag::bump(Counter::OutsideExtent);
}
}
diag::bump(Counter::ReuseOutside);
diag::reuse(id, ReuseOutcome::Outside);
}
@@ -752,19 +770,18 @@ impl UiRenderState {
}
let parent_move = active.move_idx;
let mask = active.mask;
let children = active.inherited_children.len();
let children = active.extent_children.len();
for index in 0..children {
let child = self.active[&id].inherited_children[index];
let (child, extent) = self.active[&id].extent_children[index];
let (part, slot) = extent.resolve(placement);
let active = &self.active[&child];
let (child_local, chosen) = ask_box(
UiRegion::FULL,
active.declared,
active.own_align,
[Some(placement.x), Some(placement.y)],
);
let (child_local, chosen) = ask_box(part, active.declared, active.own_align, slot);
// What it took of that box is its own answer, which this move did
// not ask again: keep the placement it has on any axis this
// widget is not the one choosing.
let child_placement = UiRegion {
x: chosen[0].unwrap_or(UiSpan::FULL),
y: chosen[1].unwrap_or(UiSpan::FULL),
x: chosen[0].unwrap_or(active.placement.x),
y: chosen[1].unwrap_or(active.placement.y),
};
let child_info = DrawInfo {
layer: active.layer,
@@ -924,7 +941,7 @@ impl UiRenderState {
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
extent_children: Vec::new(),
children: Vec::new(),
size_deps: Vec::new(),
move_idx: info.parent_move,