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

+3
View File
@@ -9,6 +9,9 @@ pub enum Axis {
}
impl Axis {
/// Both of them, for the layout code that asks the same question of each.
pub const BOTH: [Self; 2] = [Self::X, Self::Y];
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
-12
View File
@@ -163,14 +163,6 @@ impl Len {
Self::from_parts(Rel::ZERO, Px::from_f32(px))
}
pub const fn rel_min() -> Self {
Self::ZERO
}
pub const fn rel_max() -> Self {
Self::FULL
}
pub const fn max(&self, other: Self) -> Self {
Self {
rel: self.rel.max(other.rel),
@@ -213,10 +205,6 @@ impl Len {
})
}
pub fn select_len(&self, len: Len) -> Self {
len.within_len(*self)
}
pub const fn flip(&mut self) {
self.rel = Rel::ONE.sub(self.rel);
self.px = self.px.neg();
+16 -2
View File
@@ -27,7 +27,7 @@ pub struct ActiveData {
pub region: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
pub answer: Option<Answer>,
/// Asked more than once in its parent's last draw -- measured in one box
/// and then asked in the one the parent decided. The parent's layout
/// rests on the first answer and its drawing on the last, so only the
@@ -82,6 +82,20 @@ impl ActiveData {
/// all. Not `size`, which is what its last drawing reported: a drawing
/// re-expressed in the box that answer chose is not a second answer.
pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size)
self.answer.map(|answer| answer.size)
}
/// Whether it owns a region node rather than sharing the one it was drawn
/// under, which is what its two move indices being different says.
pub fn is_region_node(&self) -> bool {
self.move_idx != self.parent_move
}
}
/// What a widget answered when it was asked: the size it reported, and the
/// boxes and windows that answer holds for.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Answer {
pub size: Size,
pub holds: LayoutHolds,
}
+2 -3
View File
@@ -1,8 +1,6 @@
use crate::util::impl_axis_index;
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on along one axis: the window
/// lengths its reads hold for, the pixel lengths of its own box, and the
/// symbolic lengths of that box and of its rel base where either one is what
@@ -101,7 +99,8 @@ impl LayoutHolds {
}
pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
AXES.into_iter()
Axis::BOTH
.into_iter()
.all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
}
}
+24 -25
View File
@@ -10,7 +10,6 @@ use crate::{
},
ui::render_state::{DrawInfo, Placing},
};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// makes your surfaces look pretty
pub struct Painter<'a> {
@@ -155,8 +154,8 @@ impl<'a> Painter<'a> {
/// where that is this widget's own narrowed the way the region is. An
/// axis the region leaves whole is not read at all, so a wrapper that
/// only moves its child does not pin its drawing to a rel base.
fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
for axis in AXES {
fn resolve_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
for axis in Axis::BOTH {
if let Some(span) = place[axis].narrows_rel_base() {
let len = span.len();
let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis)));
@@ -181,7 +180,7 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
let place = self.resolve_rel_base(place.into());
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id());
@@ -198,7 +197,7 @@ impl<'a> Painter<'a> {
self.children.push(id.id());
}
let px = rel_base.to_px(self.window);
let (size, answer_holds, holds) = self.state.draw_inner(
let drawn = self.state.draw_inner(
id.id(),
DrawInfo {
layer: self.layer,
@@ -217,8 +216,8 @@ impl<'a> Painter<'a> {
None,
self.rsc,
);
let holds = self.in_parent(holds, region, place, declared);
let answer_holds = self.in_parent(answer_holds, region, place, declared);
let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
let answer_holds = self.in_parent(drawn.answer.holds, region, place, declared);
match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
Some((_, kept)) => *kept = holds,
None => self.under.push((id.id(), holds)),
@@ -226,7 +225,7 @@ impl<'a> Painter<'a> {
DrawResult {
child: id,
painter: self,
size,
size: drawn.answer.size,
answer_holds,
}
}
@@ -255,8 +254,8 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
let states_rel_base = AXES
let place = self.resolve_rel_base(place.into());
let states_rel_base = Axis::BOTH
.iter()
.any(|&axis| place[axis].stated_rel_base().is_some());
if states_rel_base || !self.children.contains(&id.id()) {
@@ -579,10 +578,10 @@ impl PrimitiveLike for &TextureHandle {
}
}
/// Moves what a child depends on into this widget's own terms: this
/// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// only what the child was asked with.
impl Painter<'_> {
/// Moves what a child depends on into this widget's own terms, taking
/// only what the child was asked with.
///
/// Window ranges are already about the one unit and combine directly.
/// A rel base pin becomes this widget's own rel base wherever a length of it
/// is what reached the child; where only pixels did, no length of this
@@ -602,7 +601,7 @@ impl Painter<'_> {
declared: Declared,
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
for axis in Axis::BOTH {
let declared = declared[axis];
let holds = holds[axis];
let result = &mut result[axis];
@@ -677,16 +676,16 @@ impl LayoutLen {
}
}
/// Where a widget's drawing goes inside the part its parent gave it: what
/// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it.
///
/// The length it reported is a length of its rel base, and the part is one too,
/// so this takes one from the other rather than composing it into the part.
/// That is what makes a fraction the same fraction wherever the part it is
/// placed in sits and however long it is -- the fraction is resolved once,
/// here, against the rel base it was reported of.
impl PlaceDesc {
/// Where a widget's drawing goes inside the part its parent gave it: what
/// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it.
///
/// The length it reported is a length of its rel base, and the part is one
/// too, so this takes one from the other rather than composing it into the
/// part. That is what makes a fraction the same fraction wherever the part
/// it is placed in sits and however long it is -- the fraction is resolved
/// once, here, against the rel base it was reported of.
pub(crate) fn placement(
self,
region: UiRegion,
@@ -695,7 +694,7 @@ impl PlaceDesc {
align: RegionAlign,
) -> UiRegion {
let mut placed = region;
for axis in AXES {
for axis in Axis::BOTH {
let reported = size[axis];
if reported.fills(declared[axis], self[axis].does_fill()) {
continue;
@@ -725,7 +724,7 @@ impl PlaceDesc {
let given = self.of(own, align);
let mut rel_base = parent_rel_base;
let mut region = given;
for axis in AXES {
for axis in Axis::BOTH {
let base = self[axis]
.stated_rel_base()
.unwrap_or_else(|| parent_rel_base[axis]);
+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];