`along` said nothing about what it did. It is `Span::slot` now: the
stretch of the row between two distances from where the span starts
laying out, as a span of its own box, with the mirror for a negative
direction in one place. `far` is `row`, which is what the comment above
it already called it, and `shares` is `has_room` beside the
`any_leftover` it was folded into. `reached` now guards on the leftover
weight it divides by rather than on the numerator that happened to be
zero with it.
The pairs layout returns are named rather than positional: `Answer`
{size, holds} and `Drawn` {answer, drawing_holds} replace
`(Size, LayoutHolds)` and a three-tuple with two `LayoutHolds` in it,
which was the one shape the cold dump exists to catch. `try_reuse`
answers `bool` rather than `Option<()>`, and the four hand-written
copies of `move_idx != parent_move` are `ActiveData::is_region_node`.
`AXES` was declared in three modules; it is `Axis::BOTH`. `rel_min`,
`rel_max` and the unused `select_len` are gone -- `ZERO` and `FULL`
already said those. Three doc comments sat on `impl` blocks instead of
the single method inside them. `reposition` and `redepth` walked their
children by index, looking the parent up again per child; both take the
list and put it back. `Scroll`'s `fixed` and `fixed_len` are
`answer_px` and `answer_is_px`, which says which one is the length.
fmt, workspace clippy under `-D warnings` with and without
`layout-diagnostics`, and the workspace tests are clean. The cold dump
over 400 depth-5 trees is byte-identical to `6c84b6f`: 34,492 boxes,
no seed moved.
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>
`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.
The split box was named `region` and `placement` on 2026-09-17; `frame`
came back as a length and survived, `extent` did not. It stayed as the
name for both halves, distinguished only by prose: `draw_at` bound the
caller's `part` to a parameter called `extent`, and `ActiveData` held two
`UiRegion`s that `draw_at` wrote `part: extent` from.
The box a parent asks a widget in is now the region, and where its
drawing ends up is its placement. `Painter`'s four holds accumulators
become the one `LayoutHolds` they were assembled into, which also drops
the name mapping between them.
The cold dump of 400 depth-5 trees is byte-identical across the change.
The offer names are from the protocol before this one, where a widget was
drawn twice and the record had to say which drawing was the question. It
is asked once now, so offer_part is the part it was asked in, offer_place
the place it was asked at, and place where its drawing was put: part,
asked and placed. LayoutHolds::frame is a range on the window since the
frame became a length of one, and the frame's own entry is the frame_len
pin beside it, so it is window; Painter::frame_own goes with it.
answers_at had one caller and said less than the line that replaces it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There is one coordinate unit, the window. Every box in the tree is a
region in window units and a widget's frame is a length in the same
units, which is only what fractions resolve against, so the box need not
be the frame and padding can take from both without either becoming the
other. A region node's entry is a translation -- a rel 1 region anchored
where its box starts -- rather than a box, so nothing composes a frame
back up a chain and a node that moves is one entry write.
Padding is then an inset of both: its pixels come off the frame, so
rel(1.0) under it fills the padded widget rather than overflowing it,
and off the box, so what is drawn sits inside. A length a container
decides for a child's frame is a length of the window like everything
else here -- a row's slot, padding's frame less its pixels, or the box a
stack's sizing child decided, which arrives as Part::Sized -- because a
slot of a row is not a fraction of anything the row can name, the same
reason a node entry is a translation. A declaration is a fraction of
whichever of those reached it, and is the only one that also places the
box.
Frame validity is a pin beside the box's, not a range: a range of window
pixels cannot say which frame an answer is a fraction of, since two
frames are different lengths at the same window size. A widget pins its
frame by reading it or by being answered with it under a fractional
rule, and the pin composes up wherever a length of this frame is what
reached the child.
Also here, because the diagnosis needed them: the shrinker reports the
shrunk tree's own divergence with each level's frame, ask, box and size
warm against cold, and there is a size-resize case -- a change and then a
resize, the order that shows an answer kept as a fraction of the wrong
length, which every other case compares at the window it was made at.
Three defects the reports found, each pinned: a rule changed over two
pads relocated the column under them instead of dividing it again (seed
59, depth 5, resize-size), a share inside padding had the padding taken
off twice, and a root resolved its own rule twice.
fmt and clippy clean with and without layout-diagnostics, 121 suite, 20
core, 11 generated, the 400-seed depth-5 shrinker over all sixteen cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The review pass over the two commits before it:
- `LayoutHolds`'s comment said a pin does not compose into the parent. It
does, where the box it pinned is the parent's own box, which is the one
case where the parent's own length is what was pinned.
- `DrawInfo::offer` was stored beside the two fields it is computed from.
It is a method now, with the open question written where it is asked
rather than only in the handoff.
- `Painter::own` is `frame_own` beside `extent_own`, since a widget's own
box is the extent and the frame is what it is a part of.
- One expression for the length a rule gives a frame (`narrowed_by`) and
one for what a widget answered (`ActiveData::measured`), each of which
had two.
- The counter said "the placement it was pinned to" for what is now a
length; the deferral in `redraw` named the seeds that made it necessary
before the last commit rather than the ones that do now; three comments
claimed an inset that does not exist yet.
No behaviour change: 108 suite tests, 20 core, the 11 generated cases, the
same two shrinker seeds failing at 400/5, and `view` and `tabs` byte-identical
to the renders taken before it.
A widget is asked in two boxes rather than one. Its frame is what a fraction
it declares or reports is a fraction of, and it passes through a span, a
stack and a scroll unchanged, so `rel(0.5)` is half the same area however
many containers sit between: a frame is narrowed only by what is decided
above the widget -- a declared length, the root. Its extent is where the
drawing goes, given as a `Place` per axis: a part of the parent's own box,
measured in frame lengths from where that box starts, which the child either
fills or has its answer placed inside.
What that buys is that nothing under a container depends on where the
container sits. A container reads `extent_len` for the length it divides and
nothing about the start, so moving it re-places its children by re-adding
that start and draws nobody again; and a fraction is resolved once, against
the frame, rather than once per box it is composed through -- a stack sized
by a child that reports `rel(0.5)` no longer takes half of half.
`Place` replaces `DrawRegion`, `ExtentPlacement`, `widget_within`,
`measure_len`, `region()`, `placement()` and `box_of`. Primitives and masks
are written in the widget's own box's coordinates alone, so the drawing has
one reference rather than two. The placement pin goes with them: reading the
extent's length pins that length symbolically, and pins compose only where a
child's box is its parent's own.
Placing an answer waits for the end of the parent's draw or for the next ask
of that child in it, so a span child is one drawing and one move rather than
two moves.
`Pad` is transparent: its padding goes around what it pads and its child
keeps the outer frame, which is where `Outset` was going anyway. A fraction
under a pad is now a fraction of the frame rather than of the inset box.
Checked: fmt, clippy with -D warnings, 109 suite tests and 20 core tests in
debug, the 11 generated cases, and the shrinker at 400 trees of depth 5 over
all fifteen cases -- which still finds seed 108 under `reorder`, where a
wrapping text measured in one box and drawn in another settles differently
warm than cold. `redraw` therefore keeps the baseline's deferral for a box
that is not as long as the one the widget was measured in; the plan's step
6 is not done, and the next commit message or the handoff says why.
Keep the original measurement placement separate from the assigned slot.
Validate frame and extent lengths before reusing an answer or drawing, and
represent hint-only records as having no measured answer.
Retain primitive and mask coordinates with their frame/extent reference.
Forwarded children follow a reused wrapper's placement without rerunning
valid draw bodies. Keep the single Widget::draw API.
Restore the eight failing suite cases from the region/placement prototype,
with regressions for mixed coordinate references, a changed inherited
extent, the sizing-stack fraction, and an undrawn share becoming visible.
This remains experimental: nested container updates do substantially more
work than e44dea3 despite restoring the leaf and wrapper reuse guarantees.
Do not merge it as a performance improvement.