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

+59 -71
View File
@@ -1,6 +1,5 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_lens, placement, rel_base_and_region};
use crate::{
ActiveData, Axis, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Moves,
Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan,
@@ -194,11 +193,10 @@ impl UiRenderState {
/// rules. Nothing above it narrowed anything or chose where it goes, so
/// its declaration is the whole of what decides either.
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
rel_base_and_region(
PlaceDesc::WHOLE.rel_base_and_region(
UiRegion::FULL,
UiVec2::FULL_SIZE,
PlaceDesc::WHOLE,
declared_lens(widgets, id),
widgets.declared_lens(id),
widgets.alignment(id),
)
}
@@ -221,7 +219,7 @@ impl UiRenderState {
diag::draw_request(id, info.parent, region, info.px, info.region_node);
}
let align = rsc.widgets().alignment(id);
let declared = declared_lens(rsc.widgets(), id);
let declared = rsc.widgets().declared_lens(id);
// Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
@@ -235,7 +233,7 @@ impl UiRenderState {
.then(|| self.retained_answer(id, region, info))
.flatten()
.and_then(|answer| {
let placed = placement(region, answer.0, declared, info.placed, align);
let placed = info.placed.placement(region, answer.0, declared, align);
self.try_reuse(id, region, placed, info, rsc)
.map(|()| answer)
});
@@ -247,7 +245,7 @@ impl UiRenderState {
// Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left
// open.
let placed = placement(region, answer.0, declared, info.placed, align);
let placed = info.placed.placement(region, answer.0, declared, align);
if placed != region {
self.relocate(id, placed, info, rsc);
}
@@ -292,8 +290,8 @@ impl UiRenderState {
// A node entry is only a translation. Its local box keeps the
// same window-unit length as the box in its parent's node.
true => (
self.move_slot(id, info.parent_move, translation(region)),
local_region(region),
self.move_slot(id, info.parent_move, region.as_translation()),
region.at_origin(),
None,
),
// Keep the old entry alive until every descendant has migrated.
@@ -377,14 +375,14 @@ impl UiRenderState {
// resolved into the rel base when the child was asked, and resolving it
// again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules.axis(axis).exact() {
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.rel_base.axis(axis).rel,
px: info.rel_base.axis(axis).px,
rel: info.rel_base[axis].rel,
px: info.rel_base[axis].px,
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.rel_base.axis(axis)),
Some(len) => len.within_len(info.rel_base[axis]),
};
let size = Size {
x: ruled(Axis::X, size.x),
@@ -399,7 +397,7 @@ impl UiRenderState {
mask == info.mask
|| AXES
.into_iter()
.all(|axis| within_box(size, region, self.output_size, axis)),
.all(|axis| size.within_box(region, self.output_size, axis)),
"'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id),
);
@@ -418,17 +416,13 @@ impl UiRenderState {
// rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing.
let rel_base = AXES.map(|axis| {
let fraction = rules
.axis(axis)
.exact()
.is_some_and(|len| len.rel != Rel::ZERO);
match fraction {
true => Some(info.rel_base.axis(axis)),
false => own.rel_base[axis as usize],
let mut own_holds = own;
for axis in AXES {
let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
if fraction {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
});
let own_holds = LayoutHolds { rel_base, ..own };
}
let answer_holds = own_holds.and(answer_under);
let holds = under
.into_iter()
@@ -485,7 +479,7 @@ impl UiRenderState {
mask_region,
children,
size_deps,
declared: declared_lens(rsc.widgets(), id),
declared: rsc.widgets().declared_lens(id),
own_align: rsc.widgets().alignment(id),
move_idx,
parent_move: info.parent_move,
@@ -613,21 +607,21 @@ impl UiRenderState {
// Which of the three said no, so a rel base that redraws more
// than it should says where to look. They overlap: a drawing
// can be outside two of them at once.
let holds = active.holds;
for axis in AXES {
let n = axis as usize;
if holds.region_len[n].is_some_and(|pinned| pinned != region.axis(axis).len()) {
let holds = active.holds[axis];
let len = region[axis].len();
let window = self.output_size[axis];
if holds.region_len.is_some_and(|pinned| pinned != len) {
diag::bump(Counter::OutsidePinnedLen);
}
if !holds.window[n].contains(self.output_size.axis(axis))
|| holds.rel_base[n]
.is_some_and(|pinned| pinned != info.rel_base.axis(axis))
if !holds.window.contains(window)
|| holds
.rel_base
.is_some_and(|pinned| pinned != info.rel_base[axis])
{
diag::bump(Counter::OutsideRelBase);
}
if !holds.region[n]
.contains(region.axis(axis).len().to_px(self.output_size.axis(axis)))
{
if !holds.region.contains(len.to_px(window)) {
diag::bump(Counter::OutsideRegion);
}
}
@@ -652,13 +646,13 @@ impl UiRenderState {
);
let has_region_node = active.move_idx != active.parent_move;
let local = match has_region_node {
true => local_region(placed),
true => placed.at_origin(),
false => placed,
};
let moved = active.placement != local;
let slot = active.move_idx;
if has_region_node {
self.moves.set(slot, translation(placed));
self.moves.set(slot, placed.as_translation());
}
if moved {
self.reposition(id, local, info, rsc);
@@ -707,11 +701,10 @@ impl UiRenderState {
) {
let active = &self.active[&child];
let (rel_base, region) = Self::ask_again(active, at, place);
let placed = placement(
let placed = place.placement(
region,
active.measured().unwrap_or(active.size),
active.declared,
place,
active.own_align,
);
let info = DrawInfo {
@@ -736,13 +729,7 @@ impl UiRenderState {
/// it declared are its own record's, so both are resolved against that
/// parent's rel base again exactly as the first ask resolved them.
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
rel_base_and_region(
at.region,
at.rel_base,
place,
active.declared,
active.own_align,
)
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
}
/// Re-places everything inside a widget whose own box moved. Every child
@@ -797,11 +784,8 @@ impl UiRenderState {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
};
AXES.into_iter().all(|axis| {
widget
.size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis))
})
AXES.into_iter()
.all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis]))
}
/// Takes a widget's record out and frees what it drew.
@@ -1074,7 +1058,7 @@ impl UiRenderState {
// to draw -- with the mark left on, so the parent draws it rather
// than keeping it. So is a widget the parent asked twice: its
// layout rests on an answer this widget cannot give again alone.
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
let declared_changed = rsc.widgets().declared_lens(id) != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent
&& (declared_changed
@@ -1189,29 +1173,33 @@ impl UiRenderState {
/// Both are lengths of the window, so the comparison is in its pixels. A
/// share is a length only to whoever divides one, so it is not a claim about
/// this box and cannot exceed it.
fn within_box(size: Size, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = size.axis(axis);
let window = window.axis(axis);
len.leftover != Weight::ZERO
|| Len::from_parts(len.rel, len.px).to_px(window) <= region.axis(axis).len().to_px(window)
impl Size {
fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = self[axis];
let window = window[axis];
len.leftover != Weight::ZERO
|| len.without_leftover().to_px(window) <= region[axis].len().to_px(window)
}
}
/// A box in a fresh region node keeps its window-unit length and starts at
/// that node's origin.
fn local_region(region: UiRegion) -> UiRegion {
let size = region.size();
UiRegion::new(
UiSpan::new(Len::ZERO, size.x),
UiSpan::new(Len::ZERO, size.y),
)
}
impl UiRegion {
/// A box in a fresh region node keeps its window-unit length and starts
/// at that node's origin.
fn at_origin(self) -> UiRegion {
let size = self.size();
UiRegion::new(
UiSpan::new(Len::ZERO, size.x),
UiSpan::new(Len::ZERO, size.y),
)
}
/// A region node changes only the origin. A full relative span anchored at
/// the box start composes as that translation in both the CPU and shader.
fn translation(region: UiRegion) -> UiRegion {
UiRegion {
x: UiSpan::new(region.x.start, region.x.start + Len::FULL),
y: UiSpan::new(region.y.start, region.y.start + Len::FULL),
/// A region node changes only the origin. A full relative span anchored at
/// the box start composes as that translation in both the CPU and shader.
fn as_translation(self) -> UiRegion {
UiRegion {
x: UiSpan::new(self.x.start, self.x.start + Len::FULL),
y: UiSpan::new(self.y.start, self.y.start + Len::FULL),
}
}
}