Say rel base, and give containers back a box to hand over

`frame` named a length, not a rectangle, which was the one word in the
layout vocabulary that lied about its own shape. It is `rel_base`: what a
fraction a widget declares or reports is a fraction of.

Three API changes with it, all for containers that do one simple thing:

- `widget_within(id, region)` returns, taking a box in the widget's own
  coordinates and deriving the child's rel base from it. `Offset` and `Pad`
  are one call each again. `Offset` also stops reading `region_len`, which
  pinned its drawing to a box length it does not care about.
- `place_at` takes the rel base, returns the answer, and asks the child
  where there is no answer to re-express. Which of the two happens is the
  painter's to work out, so `Span`'s second pass is one call and its
  `drawn_across` bookkeeping is gone.
- `Part::All` is a `Part::WHOLE` constant rather than a variant, since it
  was exactly `Of(UiSpan::FULL)` and bought a separate arm in two matches.
  Measured at 0.07% of instructions retired against 0.04% run-to-run noise.

Cold layout is byte-identical to `84dad21` over 400 depth-5 trees.
This commit is contained in:
iris-ai committed 2026-09-19 16:33:49 -04:00
1 parent a904cf4f36
commit aeb60e50f5
16 files changed
+246 -238

No files matched your search

+10 -22
View File
@@ -10,7 +10,7 @@ pub struct Span {
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis;
// The row: this span's own box, as a length of the frame its children
// The row: this span's own box, as a length of the rel base its children
// are laid out against. Its start is nothing's business -- a slot is
// a length from it -- so what this reads is the length alone.
let far = painter.region_len(axis);
@@ -21,29 +21,23 @@ impl Widget for Span {
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = Place::Within(Part::All);
let across = Place::Within(Part::WHOLE);
// A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The frame passes
// a hint where one says, and from drawing otherwise. The rel base 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 a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
let mut drawn_across = Vec::with_capacity(self.children.len());
for child in &self.children {
let len = match painter.size_hint(child, axis) {
Some(len) => {
drawn_across.push(None);
len
}
Some(len) => len,
None => {
let room = Place::Within(Part::From(along(cursor, far)));
let size = painter
painter
.widget_at(child, [None; 2], axis.pair(room, across))
.size();
drawn_across.push(Some(size.axis(!axis)));
size.axis(axis)
.len(axis)
}
};
cursor.px += len.px + self.gap;
@@ -63,7 +57,7 @@ impl Widget for Span {
);
// What is left for the shares to divide: the row less everything
// fixed, as a length of the frame rather than a number of pixels.
// fixed, as a length of the rel base rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
@@ -100,7 +94,7 @@ impl Widget for Span {
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
for ((child, &len), &across_len) in self.children.iter().zip(&lens).zip(&drawn_across) {
for (child, &len) in self.children.iter().zip(&lens) {
// 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.
@@ -121,7 +115,7 @@ impl Widget for Span {
// Along the row the span says where the child goes, and that slot
// 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
// else: its slot narrows its rel base, 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 a drawing made in the
// room is put there as it is, and one not made yet is made here.
@@ -131,13 +125,7 @@ impl Widget for Span {
if len.leftover > Weight::ZERO && shares {
narrow[axis as usize] = Some(slot.len());
}
let used = match (across_len, narrow[axis as usize]) {
(Some(across_len), None) => {
painter.place_at(child, place);
across_len
}
_ => painter.widget_at(child, narrow, place).len(!axis),
};
let used = painter.place_at(child, narrow, place).len(!axis);
if shrinks {
// Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points.