Hold a bound's length where it is decided, and say each thing once

A quality sweep over the deferred request system, which no earlier round
has reviewed.

`Bound::outside` said which end a length fell outside and left the caller
to look that end up through `Bound::at`, which `expect`s an end the value
it is given does not promise: only the pairing of the two calls kept
`at(Shorter)` off a bound with no floor. It already had the length in
hand, so it returns that, and `Outside` and `at` go with the state that
could panic.

`measured_request` pinned the rel base for any bound at all, so a measured
share under a cap in pixels was invalidated by a change to a base its
answer cannot depend on. That question is `Bound::has_fraction` now, which
is also the one `Placing::ask` and `SizeRule::has_fraction` were each
writing out over a bare array.

The rest is one name where there were several spellings: `Span::gaps`,
`Padding::along`, `Plan::drop_bounds` behind one `IRIS_UNBOUNDED` in both
rigs that had grown their own, and `Stack::size_request` resolving its
sizing child the way its draw already does. `Span`'s placement loop asked
three times whether the row was allocated, twice to decide one child's
length; one match answers all three, so the allocated and plain rules are
read side by side.

The buffers `draw_at` now reuses for their capacity are empty only because
every path to it drains them in `remove`; a `debug_assert` says so, since
a drawing over primitives left in one would record them twice.

Comments: `with_requests` named discovery as the hazard where it is a
child drawn mid-row, `Painter::allocate` documented the window it holds
for rather than what it does, `minimum_request` had none, and the note
saying a span carries its children's weight whole -- which is still what
the unallocated path does, and still the surprising part -- had been
replaced by one about the other path.
This commit is contained in:
iris-ai committed 2026-09-20 18:24:37 -04:00
1 parent 0e838e9dd1
commit 4cb6f6882a
12 files changed
+192 -160

No files matched your search

+15 -8
View File
@@ -349,6 +349,14 @@ impl UiRenderState {
),
None => Default::default(),
};
// Every one of these is a buffer this widget's last draw filled and
// `remove` emptied, kept for its capacity alone. A drawing whose
// primitives were still in it would record them twice.
debug_assert!(
textures.is_empty() && primitives.is_empty() && request_deps.is_empty(),
"'{}' ({id:?}) was drawn again over what its last draw left",
rsc.widgets().label(id)
);
let children = std::mem::take(&mut scratch.children);
let size_deps = std::mem::take(&mut scratch.size_deps);
let under = std::mem::take(&mut scratch.under);
@@ -468,11 +476,10 @@ impl UiRenderState {
if answer.leftover != Weight::ZERO {
continue;
}
let (outside, kept) =
info.bounds[axis].outside(answer.without_leftover(), window[axis]);
let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]);
bounded[axis].window = kept;
if let Some(outside) = outside {
size[axis] = info.bounds[axis].at(outside).into();
if let Some(held) = held {
size[axis] = held.into();
}
}
// A widget that clipped its contents to its box drew nothing outside
@@ -1031,10 +1038,10 @@ impl UiRenderState {
// something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved.
// A mark made while the walk runs queues itself through `mark`.
// What ends the walk is still the set
// being spent, not the queue, so a mark that reached it another way
// cannot be left for the next frame.
// A mark made while the walk runs queues itself through `mark`. What
// ends the walk is the marks being spent rather than the queue being
// empty, so a mark that reached the queue twice, or that was settled
// another way, costs a pop and nothing else.
loop {
for &id in rsc.widgets().needs_redraw.iter() {
if !self.deferred.contains(&id) {