Name the values layout carries, and say what a span's slot is

`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.
This commit is contained in:
iris-ai committed 2026-09-19 23:24:55 -04:00
1 parent 6c84b6f2cb
commit 3da1c71870
9 files changed
+167 -150

No files matched your search

+82 -78
View File
@@ -1,14 +1,12 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::{
ActiveData, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc,
UiSpan, UiVec2, Weight, WidgetId, Widgets,
ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx,
MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion,
UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
util::{HashMap, Vec2},
};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// Where a widget is drawn: what its parent decides about the draw besides
/// the boxes themselves.
#[derive(Clone, Copy)]
@@ -36,6 +34,14 @@ pub(super) struct DrawInfo {
pub px: PxVec2,
}
/// What one draw of a widget came to: the answer it gave, and the boxes and
/// windows the drawing that gave it holds for. The two are separate ranges --
/// a drawing can be invalid where its answer still stands.
pub(super) struct Drawn {
pub answer: Answer,
pub drawing_holds: LayoutHolds,
}
/// What a widget's children are placed in: its own box, the coordinates its
/// drawing is in, and what else one ask of a child is decided from.
pub(super) struct Placing {
@@ -113,7 +119,7 @@ impl UiRenderState {
// it will ask either again.
let answer = active
.answer
.is_some_and(|(_, holds)| holds.contains(size, active.rel_base, active.region));
.is_some_and(|answer| answer.holds.contains(size, active.rel_base, active.region));
answer && active.holds.contains(size, active.rel_base, active.region)
});
if !stands {
@@ -207,7 +213,7 @@ impl UiRenderState {
info: DrawInfo,
mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds, LayoutHolds) {
) -> Drawn {
let old_parent = old
.as_ref()
.or_else(|| self.active.get(&id))
@@ -233,9 +239,9 @@ impl UiRenderState {
.then(|| self.retained_answer(id, region, info))
.flatten()
.and_then(|answer| {
let placed = info.placed.placement(region, answer.0, declared, align);
let placed = info.placed.placement(region, answer.size, declared, align);
self.try_reuse(id, region, placed, info, rsc)
.map(|()| answer)
.then_some(answer)
});
let answer = reused.unwrap_or_else(|| {
if old.is_none() {
@@ -245,7 +251,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 = info.placed.placement(region, answer.0, declared, align);
let placed = info.placed.placement(region, answer.size, declared, align);
if placed != region {
self.relocate(id, placed, info, rsc);
}
@@ -273,7 +279,10 @@ impl UiRenderState {
{
old_parent.children.retain(|child| *child != id);
}
(answer.0, answer.1, drawing_holds)
Drawn {
answer,
drawing_holds,
}
}
/// Calls a widget's `draw` and keeps what it drew in `region`.
@@ -284,7 +293,7 @@ impl UiRenderState {
info: DrawInfo,
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) {
) -> Answer {
let rel_base = info.rel_base;
let (move_idx, region, retired_move) = match info.region_node {
// A node entry is only a translation. Its local box keeps the
@@ -395,9 +404,11 @@ impl UiRenderState {
// was offered reports the height it needs.
debug_assert!(
mask == info.mask
|| AXES
.into_iter()
.all(|axis| size.within_box(region, self.output_size, axis)),
|| Axis::BOTH.into_iter().all(|axis| size.within_box(
region,
self.output_size,
axis
)),
"'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id),
);
@@ -417,7 +428,7 @@ impl UiRenderState {
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing.
let mut own_holds = own;
for axis in AXES {
for axis in Axis::BOTH {
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]);
@@ -489,7 +500,10 @@ impl UiRenderState {
};
rsc.on_draw(&active);
self.active.insert(id, active);
(size, answer_holds)
Answer {
size,
holds: answer_holds,
}
}
/// Keeps a region node's entry across redraws because descendants retain
@@ -516,23 +530,17 @@ impl UiRenderState {
/// drawing ended up. Alignment is exactly that case: the first box is the
/// question and the smaller placed box holds the drawing. Whether the
/// answer is stale at all is its caller's question, asked once there.
fn retained_answer(
&self,
id: WidgetId,
region: UiRegion,
info: DrawInfo,
) -> Option<(Size, LayoutHolds)> {
fn retained_answer(&self, id: WidgetId, region: UiRegion, info: DrawInfo) -> Option<Answer> {
let active = self.active.get(&id)?;
let has_region_node = active.move_idx != active.parent_move;
if !active.drawn
|| has_region_node != info.region_node
|| active.is_region_node() != info.region_node
|| active.parent_move != info.parent_move
{
return None;
}
let answer = active.answer?;
answer
.1
.holds
.contains(self.output_size, info.rel_base, region)
.then_some(answer)
}
@@ -546,7 +554,7 @@ impl UiRenderState {
placed: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) -> Option<()> {
) -> bool {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) {
@@ -555,19 +563,20 @@ impl UiRenderState {
diag::bump(Counter::ReuseDirty);
diag::reuse(id, ReuseOutcome::Dirty);
}
return None;
return false;
}
let active = self.active.get(&id)?;
let Some(active) = self.active.get(&id) else {
return false;
};
if !active.drawn {
#[cfg(feature = "layout-diagnostics")]
diag::reuse(id, ReuseOutcome::Undrawn);
return None;
return false;
}
let has_region_node = active.move_idx != active.parent_move;
if has_region_node != info.region_node {
if active.is_region_node() != info.region_node {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseWrongNode);
return None;
return false;
}
// Drawn on another layer: the drawing sits in that layer's list and
// paints at its moment, which no amount of geometry says. A container
@@ -580,10 +589,10 @@ impl UiRenderState {
diag::bump(Counter::ReuseWrongLayer);
diag::reuse(id, ReuseOutcome::WrongLayer);
}
return None;
return false;
}
if active.parent_mask != info.mask {
return None;
return false;
}
// Drawn somewhere else in the tree: its box is in coordinates it no
// longer sits in, and its slot names the wrong parent.
@@ -593,7 +602,7 @@ impl UiRenderState {
diag::bump(Counter::ReuseWrongParent);
diag::reuse(id, ReuseOutcome::WrongParent);
}
return None;
return false;
}
// In pixels, because the box is a fraction of the window and that
// may be what changed -- an unchanged fraction of a window half the
@@ -607,7 +616,7 @@ 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.
for axis in AXES {
for axis in Axis::BOTH {
let holds = active.holds[axis];
let len = region[axis].len();
let window = self.output_size[axis];
@@ -628,10 +637,10 @@ impl UiRenderState {
diag::bump(Counter::ReuseOutside);
diag::reuse(id, ReuseOutcome::Outside);
}
return None;
return false;
}
self.relocate(id, placed, info, rsc);
Some(())
true
}
/// Puts a retained drawing where its parent now has it, without drawing:
@@ -644,14 +653,14 @@ impl UiRenderState {
"'{}' ({id:?}) placed while marked to draw",
rsc.widgets().label(id)
);
let has_region_node = active.move_idx != active.parent_move;
let local = match has_region_node {
let is_region_node = active.is_region_node();
let local = match is_region_node {
true => placed.at_origin(),
false => placed,
};
let moved = active.placement != local;
let slot = active.move_idx;
if has_region_node {
if is_region_node {
self.moves.set(slot, placed.as_translation());
}
if moved {
@@ -663,23 +672,13 @@ impl UiRenderState {
active.placed = info.placed;
#[cfg(feature = "layout-diagnostics")]
{
match (moved, has_region_node) {
(true, true) => diag::bump(Counter::ReuseMoved),
(true, false) => diag::bump(Counter::ReuseRemapped),
(false, _) => diag::bump(Counter::ReuseExact),
}
diag::reuse(
id,
if moved {
if has_region_node {
ReuseOutcome::Moved
} else {
ReuseOutcome::Remapped
}
} else {
ReuseOutcome::Exact
},
);
let (counter, outcome) = match (moved, is_region_node) {
(true, true) => (Counter::ReuseMoved, ReuseOutcome::Moved),
(true, false) => (Counter::ReuseRemapped, ReuseOutcome::Remapped),
(false, _) => (Counter::ReuseExact, ReuseOutcome::Exact),
};
diag::bump(counter);
diag::reuse(id, outcome);
}
}
@@ -712,7 +711,7 @@ impl UiRenderState {
parent: Some(at.id),
depth: at.depth + 1,
parent_move: at.move_idx,
region_node: active.move_idx != active.parent_move,
region_node: active.is_region_node(),
mask: at.mask,
rel_base,
region,
@@ -755,16 +754,18 @@ impl UiRenderState {
move_idx: active.move_idx,
mask: active.mask,
};
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
// Taken out and put back so that placing a child can borrow the state
// it needs; nothing on that path reads this widget's own child list.
let children = std::mem::take(&mut self.active.get_mut(&id).unwrap().children);
for &child in &children {
self.place_child(child, &at, rsc);
}
self.active.get_mut(&id).unwrap().children = children;
}
/// A reused subtree keeps its shape, so every widget in it moves by the
/// same amount -- and where the top of it did not move, none of it did,
/// which is what makes this free in the ordinary case.
/// A reused subtree keeps its shape, so each widget in it keeps its depth
/// under the top -- and where the top's own depth did not change, none of
/// them did, which is what makes this free in the ordinary case.
fn redepth(&mut self, id: WidgetId, depth: usize) {
let Some(active) = self.active.get_mut(&id) else {
return;
@@ -773,18 +774,21 @@ impl UiRenderState {
return;
}
active.depth = depth;
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
// Taken out and put back so the walk can borrow the state it needs;
// it only ever goes further down, so it reads no list but its own.
let children = std::mem::take(&mut active.children);
for &child in &children {
self.redepth(child, depth + 1);
}
self.active.get_mut(&id).unwrap().children = children;
}
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
};
AXES.into_iter()
Axis::BOTH
.into_iter()
.all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis]))
}
@@ -1118,13 +1122,13 @@ impl UiRenderState {
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
let answer = self.draw_inner(id, info, old, rsc);
let drawn = self.draw_inner(id, info, old, rsc);
let active = self.active.get_mut(&id).unwrap();
// A wider contract does not invalidate the guarantee the parent kept.
// Retain that guarantee so widening and narrowing back do not churn it.
if let Some((size, holds)) = was_answer
&& answer.0 == size
&& answer.1.covers(holds)
if let Some(was) = was_answer
&& drawn.answer.size == was.size
&& drawn.answer.holds.covers(was.holds)
{
active.answer = was_answer;
}
@@ -1169,11 +1173,11 @@ impl UiRenderState {
}
}
/// Whether what a widget reports along `axis` is inside the box it drew in.
/// 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.
impl Size {
/// Whether what a widget reports along `axis` is inside the box it drew
/// in. 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(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = self[axis];
let window = window[axis];