Ask each child once and place its answer by re-expression

A widget draws in the box it is asked in and its answer is placed inside
that box by re-expressing the drawing; nothing is drawn again in a box an
answer chose. The offer machinery, whose job was to tell a measuring draw
from a placing one, goes with the placing draw. A span measures each child
from its cursor and moves fixed children to their slots with place_at; a
share child is asked once more in its decided slot with its frame narrowed
to it. A stack asks non-sizing children in the box its sizing child
decided. A scroll asks its content once and moves it to the scrolled
offset. A local redraw asks the retained question again and puts the
answer back where the parent placed it.

A symbolic length a child pinned composes through Part::Of exactly where
the part is the whole box less pixels, and pins the parent's own length
otherwise; dropping it let a pad reuse a drawing across a narrowed frame
of the same pixel length (shrinker seeds 60, 248 and 384 at depth 5).

Suite 114/114 including the two decided-box pins, fast oracle 11/11,
shrinker 400 seeds at depth 5 over all fifteen cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Fable 5.1 committed 2026-09-18 18:20:57 -04:00
1 parent 4328eac756
commit 3091fb86df
12 files changed
+353 -354

No files matched your search

+1 -1
View File
@@ -15,6 +15,6 @@ impl Widget for Offset {
moved(painter.extent_len(Axis::X), self.amt.x),
moved(painter.extent_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, UiRegion::FULL, place).size()
painter.widget_at(&self.inner, [None; 2], place).size()
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ impl Widget for Pad {
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
let inner = painter.widget_at(&self.inner, UiRegion::FULL, place).size();
let inner = painter.widget_at(&self.inner, [None; 2], place).size();
Size {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
+5 -7
View File
@@ -12,10 +12,9 @@ pub struct Scroll {
impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size {
let container_len = painter.px_len(self.axis);
// Measured in the whole viewport, then drawn at the scrolled offset.
let whole = UiRegion::FULL;
// Asked in the whole viewport, then put at the scrolled offset.
let answer_len = painter
.widget_at(&self.inner, whole, [Place::Fill(Part::All); 2])
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2])
.len(self.axis);
let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len);
self.container_len = container_len;
@@ -59,11 +58,10 @@ impl Widget for Scroll {
}
// The viewport is the inner's frame, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is drawn is the
// content box, scrolled.
painter.widget_at(
// content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there.
painter.place_at(
&self.inner,
whole,
self.axis
.pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)),
);
+32 -13
View File
@@ -25,19 +25,23 @@ impl Widget for Span {
// A length for every child before their final slots are chosen. The
// frame passes through unchanged, so `rel(0.5)` is half the area this
// span was given whatever else is in it and wherever this child sits
// among them; what it is drawn in is the room left from the cursor,
// because a text has to wrap at the width actually there.
// among them; what it is asked in is the room left from the cursor,
// because a text has to wrap at the width actually there. This is
// the one ask a fixed child gets: its slot is its answer, and the
// drawing is moved there once the shares are known.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
let mut sizes = Vec::with_capacity(self.children.len());
for child in &self.children {
let room = Place::Within(Part::From(along(cursor, far)));
let len = painter
.widget_at(child, UiRegion::FULL, axis.pair(room, across))
.len(axis);
let size = painter
.widget_at(child, [None; 2], axis.pair(room, across))
.size();
let len = size.axis(axis);
cursor.px += len.px + self.gap;
cursor.rel += len.rel;
lens.push(len);
sizes.push(size);
}
let lens: Vec<LayoutLen> = sizes.iter().map(|size| size.axis(axis)).collect();
let gaps = self
.gap
@@ -88,7 +92,8 @@ impl Widget for Span {
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) {
for (child, size) in self.children.iter().zip(&sizes) {
let len = size.axis(axis);
// 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.
@@ -106,12 +111,26 @@ impl Widget for Span {
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
// Along the row the span says where the child goes, and that slot
// is the drawing's box outright rather than something to place an
// answer inside again.
let slot = Place::Fill(Part::From(along(from, start)));
let placed = painter.widget_at(child, UiRegion::FULL, axis.pair(slot, across));
// is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere
// else: its slot narrows its frame, and the child is asked in
// it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so its drawing is put
// there as it is.
let slot = along(from, start);
let place = axis.pair(Place::Fill(Part::From(slot)), across);
let used = match len.leftover > Weight::ZERO && shares {
true => {
let mut narrow = [None; 2];
narrow[axis as usize] = Some(slot.len());
painter.widget_at(child, narrow, place).len(!axis)
}
false => {
painter.place_at(child, place);
size.axis(!axis)
}
};
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too;
+11 -4
View File
@@ -23,19 +23,26 @@ impl Widget for Stack {
Some((i, child)) => {
painter.child_layer_at(i);
painter
.widget_at(child, UiRegion::FULL, [Place::Fill(Part::All); 2])
.widget_at(child, [None; 2], [Place::Fill(Part::All); 2])
.size()
}
None => Size::LEFTOVER,
};
// Every other child gets the box the sizing child decided: the
// stack is that length, so that is the box they are asked in, and a
// fraction under them is a fraction of it. A share leaves the axis
// to whoever gave the stack its box. Where a child sits in a box
// bigger than itself is its own business.
let narrow = [Axis::X, Axis::Y].map(|axis| {
let len = size.axis(axis);
(len.leftover == Weight::ZERO).then(|| Len::from_parts(len.rel, len.px))
});
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i);
// Every other child is drawn in the stack's own box, and where it
// sits in one bigger than itself is its own business.
painter.widget(child);
painter.widget_at(child, narrow, [Place::Within(Part::All); 2]);
}
size
}