Say layout's operations by name, and index a pair by its axis

Four rounds over the same idea: an expression that needed a comment to say
what it computed wanted to be a named operation.

The placement description is built by chaining off the value that says it.
`UiSpan::within_desc`/`shifted_desc` and `Len::as_desc` replace the
`PlaceDescAxis::` constructors, `PlaceDescAxis::axis` lifts one axis into a
pair with the whole box across it, and `PlaceDesc::per_axis` covers the case
where the two axes differ. `beside` is dropped: `from_axis` already said it.

Seven module-level functions become methods on the value each took first --
`Widgets::declared_lens`, `LayoutLen::fills`, `PlaceDesc::placement` and
`::rel_base_and_region`, `Size::within_box`, `UiRegion::at_origin` and
`::as_translation`.

`UiSpan::place` is the aligned-placement rule, which was written out three
times; `LayoutLen::without_leftover` is the sibling `apply_leftover` never
had, at six sites; `is_px` and `is_only_leftover` name field comparisons the
surrounding comments had to translate; `Holds::covers` was interval
containment spelled out by hand. A span's `shared` loses the two arguments
that did not vary across its loop.

`LayoutHolds` was four two-element arrays where every other pair here is a
struct of two per-axis values, so nothing it did could be written once.
It becomes `AxisHolds` on `x` and `y`, and `and`, `covers` and `contains`
lose their loops.

Every pair gets `Index<Axis>`/`IndexMut<Axis>` through one macro, and the
eighteen `axis`/`axis_mut` methods go. `const_index` keeps the accessors
usable in const context.

Cold layout is unchanged: `layout_dump` over 400 depth-5 trees is identical
to 58ce74d byte for byte, across all 34,492 boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-19 20:56:59 -04:00
1 parent 58ce74dd7d
commit 55df32a33c
21 files changed
+465 -490

No files matched your search

+4 -4
View File
@@ -118,9 +118,9 @@ pub struct Branch {
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut));
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
let measured = painter
.widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top))
.widget_at(&self.probe, top.axis(Axis::Y))
.len(Axis::X);
let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X);
@@ -134,8 +134,8 @@ impl Widget for Branch {
};
painter.window_holds(Axis::X, holds.through(len));
let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y)));
let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below);
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
let place = below.axis(Axis::Y);
match px > threshold {
true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, place),
+1 -1
View File
@@ -12,7 +12,7 @@ impl Widget for Image {
}
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(self.handle.size().axis(axis)))
Some(LayoutLen::px(self.handle.size()[axis]))
}
}
+5 -8
View File
@@ -16,7 +16,7 @@ impl Widget for Scroll {
let answer_len = painter
.widget_at(&self.inner, PlaceDesc::WHOLE.fills())
.len(self.axis);
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis);
let fixed = painter.to_px(answer_len.without_leftover(), self.axis);
self.container_len = container_len;
self.content_len = fixed.max(container_len);
@@ -24,14 +24,14 @@ impl Widget for Scroll {
self.amt = self.content_len - self.container_len;
}
self.update_amt();
let align = painter.alignment().axis(self.axis);
let align = painter.alignment()[self.axis];
// Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and
// the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length.
let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO;
let fixed_len = answer_len.is_px();
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end {
@@ -54,7 +54,7 @@ impl Widget for Scroll {
let content = match moved || self.content_len != self.container_len {
true => {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
PlaceDescAxis::shifted(UiSpan::new(start, start.offset(self.content_len)))
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
}
false => PlaceDescAxis::WHOLE,
};
@@ -62,10 +62,7 @@ impl Widget for Scroll {
// 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,
PlaceDesc::from_axis(self.axis, content.fills(), PlaceDescAxis::WHOLE.fills()),
);
painter.place_at(&self.inner, content.axis(self.axis).fills());
// 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
// more. The content's length is what it scrolls through, not what it
+17 -29
View File
@@ -18,10 +18,6 @@ impl Widget for Span {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
// 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 = PlaceDescAxis::WHOLE;
// A length for every child before their final slots are chosen: from
// 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
@@ -34,10 +30,11 @@ impl Widget for Span {
let len = match painter.size_hint(child, axis) {
Some(len) => len,
None => {
let room = PlaceDescAxis::shifted(along(cursor, far));
painter
.widget_at(child, PlaceDesc::from_axis(axis, room, across))
.len(axis)
// 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 room = along(cursor, far).shifted_desc().axis(axis);
painter.widget_at(child, room).len(axis)
}
};
cursor.px += len.px + self.gap;
@@ -58,7 +55,7 @@ impl Widget for Span {
// What is left for the shares to divide: the row less everything
// fixed, as a length of the rel base rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
let room = far - total.without_leftover();
// 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`
// itself, and answered back through the same expression, so the
@@ -94,24 +91,26 @@ impl Widget for Span {
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
let shared = |fixed: Len, taken: Weight| match taken == Weight::ZERO {
true => fixed,
false => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
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.
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
{
if len.is_only_leftover() && !shares {
painter.undraw(child);
fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
start = shared(fixed, taken);
continue;
}
let from = start;
if len.leftover > Weight::ZERO && shares {
taken += len.leftover;
}
fixed.px += len.px;
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
fixed += len.without_leftover();
start = shared(fixed, taken);
// 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
@@ -120,8 +119,7 @@ impl Widget for Span {
// 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.
let slot = along(from, start);
let slot_place = PlaceDescAxis::shifted(slot).fills();
let mut place = PlaceDesc::from_axis(axis, slot_place, across);
let mut place = slot.shifted_desc().fills().axis(axis);
if len.leftover > Weight::ZERO && shares {
place = place.rel_base(axis, slot.len());
}
@@ -131,14 +129,14 @@ impl Widget for Span {
// span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too;
// only fixed children are compared with one another.
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
if !used.is_px() {
ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px);
}
}
fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
start = shared(fixed, taken);
}
// Carried whole rather than collapsed to one share: a span that sizes
@@ -157,16 +155,6 @@ impl Widget for Span {
}
}
/// Where a row has reached: everything fixed before this point, which is a
/// sum and exact, plus the share of the room the weights so far are worth,
/// which is one rounding wherever it is asked for.
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
if taken == Weight::ZERO {
return fixed;
}
fixed + room.scale(Rel::ratio(taken, weight))
}
impl Span {
pub fn empty(dir: Dir) -> Self {
Self {
+4 -5
View File
@@ -31,14 +31,13 @@ impl Widget for Stack {
// 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 on = |axis| {
let len = size.axis(axis);
let place = PlaceDesc::per_axis(|axis| {
let len = size[axis];
match len.leftover == Weight::ZERO {
true => PlaceDescAxis::sized(Len::from_parts(len.rel, len.px)).fills(),
true => len.without_leftover().as_desc().fills(),
false => PlaceDescAxis::WHOLE,
}
};
let place = PlaceDesc::new(on(Axis::X), on(Axis::Y));
});
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;