Read a child's answer in the asker's frame, and drop the root move entry

A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.

That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.

The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.

Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Fable 5.1 committed 2026-09-16 23:00:15 -04:00
1 parent 5f16617511
commit 5b7800264d
11 files changed
+182 -109

No files matched your search

+56 -20
View File
@@ -135,30 +135,34 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, None)
self.widget_at(id, region, [false; 2])
}
/// Draws a widget with an alignment chosen by its container rather than
/// the widget's property. Containers use this when the box they hand down
/// already expresses the size they report around the child.
pub fn widget_aligned<'s, W: ?Sized>(
/// Draws a widget in a box this widget chose from the widget's own
/// answer along the `decided` axes. On those the answer is not placed
/// inside the box again: it already is the box, and a fraction the
/// widget reported of its offer, taken of this box a second time, would
/// shrink it twice. A container uses this where it hands back exactly
/// what a child asked for -- a span placing a child at the length it
/// reported, a scroll giving its content the content's own length.
pub fn widget_decided<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align: RegionAlign,
decided: [bool; 2],
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, Some(align))
self.widget_at(id, region, decided)
}
fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align_override: Option<RegionAlign>,
decided: [bool; 2],
) -> DrawResult<'s, 'a, W> {
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id()));
let align = self.rsc.widgets().alignment(id.id());
// Composing `FULL` through a box is not quite the identity in f32,
// so a child with nothing declared keeps the box it would have had.
let local = match declared.iter().any(Option::is_some) {
@@ -200,7 +204,7 @@ impl<'a> Painter<'a> {
offer,
offered_px: self.px_within_offer(offer),
slot_wide: self.slot_wide,
align: align_override,
decided,
},
None,
self.rsc,
@@ -216,7 +220,7 @@ impl<'a> Painter<'a> {
DrawResult {
child: id,
painter: self,
size,
size: in_parent_frame(size, local, declared),
}
}
@@ -282,7 +286,7 @@ impl<'a> Painter<'a> {
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
Some(size.axis(axis))
Some(in_parent_frame(size, local, declared).axis(axis))
}
/// Whether this is the first box a child is asked about in during a draw
@@ -393,7 +397,13 @@ impl<'a> Painter<'a> {
/// near edge. A container that reports one child's size gives every child
/// this, so what it draws is inside what it says it occupies.
pub fn box_of(&self, size: Size) -> UiRegion {
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
placed_box(
UiRegion::FULL,
size,
RegionAlign::NEAR,
[None; 2],
[false; 2],
)
}
/// This widget's box in pixels. Reading it makes the drawing one that
@@ -517,6 +527,23 @@ impl PrimitiveLike for &TextureHandle {
}
}
/// A child's answer as lengths of the box it was asked from. A widget reports
/// a fraction of the box it was given, and the widget that gave it wants the
/// same length as a fraction of its own: one composition apart wherever the
/// offer was not the whole of the parent's extent, as a span's is after a
/// relative child. A declared axis is already the parent's: it resolved the
/// rule in its own box, and the rule is what the report says.
fn in_parent_frame(size: Size, local: UiRegion, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
let len = local.axis(axis).len();
*size.axis_mut(axis) = size.axis(axis).within_len(len);
}
}
size
}
/// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead.
@@ -537,12 +564,20 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
})
}
/// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. A share fills, because a
/// share is a length only to whoever divides one, and whoever did is the one
/// that handed down this box. A declared axis does too: `declared_box`
/// already placed it, in the parent's box, and the rule's length is what the
/// widget reports there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// The box a drawing occupies: the size the widget reported, on the side of
/// the box it was asked in that its alignment says. An axis reported as a
/// share fills, because a share is a length only to whoever divides one, and
/// whoever did is the one that handed down this box. A declared axis is
/// left alone too: `declared_box` already placed it, in the parent's box,
/// and the rule's length is what the widget reports there.
/// the box it was asked in that its alignment says, on every axis that is
/// not simply filled.
///
/// A reported fraction is a fraction of the box the widget drew in, where a
/// declared one is a fraction of the box its parent handed down -- a span
@@ -553,11 +588,12 @@ pub(crate) fn placed_box(
size: Size,
align: RegionAlign,
declared: [Option<LayoutLen>; 2],
decided: [bool; 2],
) -> UiRegion {
let mut placed = region;
for (axis, declared) in AXES.into_iter().zip(declared) {
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
let reported = size.axis(axis);
if reported.leftover != Weight::ZERO || declared.is_some() {
if fills(reported, declared, decided) {
continue;
}
let span = placed.axis_mut(axis);