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

+13 -7
View File
@@ -7,11 +7,7 @@ pub struct Pad {
impl Widget for Pad {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
let padding = match axis {
Axis::X => self.padding.left + self.padding.right,
Axis::Y => self.padding.top + self.padding.bottom,
};
requests.inset(&self.inner, axis, padding)
requests.inset(&self.inner, axis, self.padding.along(axis))
}
fn draw(&mut self, painter: &mut Painter) -> Size {
@@ -30,11 +26,11 @@ impl Widget for Pad {
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
Size {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
px: inner.x.px + self.padding.along(Axis::X),
..inner.x
},
y: LayoutLen {
px: inner.y.px + self.padding.top + self.padding.bottom,
px: inner.y.px + self.padding.along(Axis::Y),
..inner.y
},
}
@@ -65,6 +61,16 @@ impl Padding {
bottom: amt,
}
}
/// Both sides of one axis together, which is what this padding takes
/// of a length along it.
pub fn along(&self, axis: Axis) -> Px {
match axis {
Axis::X => self.left + self.right,
Axis::Y => self.top + self.bottom,
}
}
/// `region` less this padding on each side.
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left;
+40 -28
View File
@@ -14,11 +14,7 @@ impl Widget for Span {
// cross-axis length then contributes nothing to the drawn answer.
return None;
}
let mut total = RequestedLen::from(Len::from_parts(
Rel::ZERO,
self.gap
.mul_int(self.children.len().saturating_sub(1) as i32),
));
let mut total = RequestedLen::from(Len::from_parts(Rel::ZERO, self.gaps()));
for child in &self.children {
let child = requests.widget(child, axis)?;
total = requests.sum(total, child);
@@ -45,9 +41,7 @@ impl Span {
// the length alone.
let row = painter.region_len(axis);
self.collect(painter, row, lens, true);
let gaps = self
.gap
.mul_int(self.children.len().saturating_sub(1) as i32);
let gaps = self.gaps();
let fixed = lens
.iter()
.try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
@@ -65,8 +59,8 @@ impl Span {
if nonlinear {
painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
}
let allocated = nonlinear.then_some(&values);
let total = match &allocated {
let allocated = nonlinear.then(|| &values[..]);
let total = match allocated {
Some(allocated) => LayoutLen {
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
..LayoutLen::ZERO
@@ -106,23 +100,27 @@ impl Span {
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
let len = match &allocated {
Some(allocated) => LayoutLen {
px: allocated[index],
..LayoutLen::ZERO
},
None => request.linear().unwrap(),
// An allocated row already has a length for every child; without
// one the request is the length and the room is divided here.
// Either way a child asking for nothing but a part of what is
// left over, when nothing is, is not drawn at all -- one that
// also asked for pixels or a fraction keeps those and overflows.
let (len, shares, nothing_left) = match allocated {
Some(allocated) => {
let len = LayoutLen {
px: allocated[index],
..LayoutLen::ZERO
};
let shares = request.has_leftover();
(len, shares, shares && len.px == Px::ZERO)
}
None => {
let len = request.linear().unwrap();
let shares = len.leftover > Weight::ZERO && has_room;
(len, shares, len.is_only_leftover() && !has_room)
}
};
let shares = match &allocated {
Some(_) => request.has_leftover(),
None => len.leftover > Weight::ZERO && has_room,
};
// A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows.
if (len.is_only_leftover() && !has_room)
|| (allocated.is_some() && shares && len.px == Px::ZERO)
{
if nothing_left {
painter.undraw(child);
fixed.px += self.gap;
continue;
@@ -160,8 +158,15 @@ impl Span {
fixed.px += self.gap;
}
// Discovery carries nested requests to the allocating ancestor. The
// draw still returns an ordinary Size for callers measuring content.
// Where nothing was allocated the weight is carried whole rather
// than collapsed to one share, so nesting spans divides the same
// space rather than re-dividing a share of it: four `leftover(1)`
// children under two spans under one span get a quarter each, which
// one share per level does not give. Resolution happens at the
// nearest ancestor with a length, and the root always has one --
// or, where a comparison deferred the row, at the ancestor whose
// allocation discovery carried these requests to, and `total` is
// pixels by the time it gets here.
let ortho = match shrinks {
true => ortho,
false => LayoutLen::rel(1.0),
@@ -171,6 +176,13 @@ impl Span {
}
impl Span {
/// What the gaps between this span's children take, which is a length of
/// the row before anything is divided.
fn gaps(&self) -> Px {
self.gap
.mul_int(self.children.len().saturating_sub(1) as i32)
}
fn collect(
&self,
painter: &mut Painter,
+9 -6
View File
@@ -9,12 +9,15 @@ pub struct Stack {
impl Widget for Stack {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
match self.size {
StackSize::Default => Some(LayoutLen::LEFTOVER.into()),
StackSize::Child(i) => match self.children.get(i) {
Some(child) => requests.widget(child, axis),
None => Some(LayoutLen::LEFTOVER.into()),
},
let sizing = match self.size {
StackSize::Default => None,
StackSize::Child(i) => self.children.get(i),
};
// With nothing sizing it a stack is a share of the box it is given,
// which is what its draw answers too.
match sizing {
Some(child) => requests.widget(child, axis),
None => Some(LayoutLen::LEFTOVER.into()),
}
}