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

+2 -2
View File
@@ -120,7 +120,7 @@ impl Widget for Branch {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.widget_at(&self.probe, [None; 2], [Place::Within(Part::WHOLE), top])
.len(Axis::X);
let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X);
@@ -135,7 +135,7 @@ impl Widget for Branch {
painter.window_holds(Axis::X, holds.through(len));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.region_len(Axis::Y))));
let place = [Place::Within(Part::All), below];
let place = [Place::Within(Part::WHOLE), below];
match px > threshold {
true => painter.widget_at(&self.wide, [None; 2], place),
false => painter.widget_at(&self.narrow, [None; 2], place),
+3 -9
View File
@@ -7,14 +7,8 @@ pub struct Offset {
impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size {
// The whole of this widget's box, moved: the frame passes through, so
// what the child declares or reports means the same as it would
// without the offset.
let moved = |len: Len, amt: Len| Place::Within(Part::From(UiSpan::new(amt, len + amt)));
let place = [
moved(painter.region_len(Axis::X), self.amt.x),
moved(painter.region_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, [None; 2], place).size()
painter
.widget_within(&self.inner, UiRegion::FULL.offset(self.amt))
.size()
}
}
+4 -21
View File
@@ -14,31 +14,14 @@ impl Widget for Pad {
// widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for.
//
// Padding is an inset of both: it comes off the frame, so `rel(1)`
// Padding is an inset of both: it comes off the rel base, so `rel(1)`
// under it fills this widget rather than overflowing it by the
// padding, and it comes off the box, so what is drawn sits inside.
// The two stay distinct -- the box can be narrower still, where a row
// asked this widget in the room left, and a text wraps at that.
let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead),
Len::from_parts(Rel::ONE, -trail),
)))
};
let place = [
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
// Read from this widget's own frame rather than written as a
// fraction of it: a frame is a length of the window like everything
// else here, and taking the padding off is the whole of what this
// widget does to it.
let narrow = [
(Axis::X, self.padding.left + self.padding.right),
(Axis::Y, self.padding.top + self.padding.bottom),
]
.map(|(axis, pixels)| Some(painter.frame_len(axis) - Len::from_parts(Rel::ZERO, pixels)));
let inner = painter.widget_at(&self.inner, narrow, place).size();
let inner = painter
.widget_within(&self.inner, self.padding.region())
.size();
Size {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
+6 -4
View File
@@ -14,7 +14,7 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis);
// Asked in the whole viewport, then put at the scrolled offset.
let answer_len = painter
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2])
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::WHOLE); 2])
.len(self.axis);
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis);
self.container_len = container_len;
@@ -56,15 +56,17 @@ impl Widget for Scroll {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
Part::From(UiSpan::new(start, start.offset(self.content_len)))
}
false => Part::All,
false => Part::WHOLE,
};
// The viewport is the inner's frame, so a fraction it declares or
// The viewport is the inner's rel base, 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 goes is the content
// box, scrolled: its drawing moved there, not made again there.
painter.place_at(
&self.inner,
self.axis.pair(Place::Fill(content), Place::Fill(Part::All)),
[None; 2],
self.axis
.pair(Place::Fill(content), Place::Fill(Part::WHOLE)),
);
// What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for
+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.
+2 -2
View File
@@ -23,7 +23,7 @@ impl Widget for Stack {
Some((i, child)) => {
painter.child_layer_at(i);
painter
.widget_at(child, [None; 2], [Place::Fill(Part::All); 2])
.widget_at(child, [None; 2], [Place::Fill(Part::WHOLE); 2])
.size()
}
None => Size::LEFTOVER,
@@ -37,7 +37,7 @@ impl Widget for Stack {
let len = size.axis(axis);
match len.leftover == Weight::ZERO {
true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))),
false => Place::Within(Part::All),
false => Place::Within(Part::WHOLE),
}
});
for (i, child) in self.children.iter().enumerate() {