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
+164 -147

No files matched your search

+3
View File
@@ -9,6 +9,9 @@ pub enum Axis {
} }
impl 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, /// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector. /// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] { 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)) 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 { pub const fn max(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.max(other.rel), 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) { pub const fn flip(&mut self) {
self.rel = Rel::ONE.sub(self.rel); self.rel = Rel::ONE.sub(self.rel);
self.px = self.px.neg(); self.px = self.px.neg();
+16 -2
View File
@@ -27,7 +27,7 @@ pub struct ActiveData {
pub region: UiRegion, pub region: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or /// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet. /// 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 /// 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 /// 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 /// 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 /// 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. /// re-expressed in the box that answer chose is not a second answer.
pub fn measured(&self) -> Option<Size> { 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::util::impl_axis_index;
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2}; 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 /// 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 /// 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 /// 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 { 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())) .all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
} }
} }
+21 -22
View File
@@ -10,7 +10,6 @@ use crate::{
}, },
ui::render_state::{DrawInfo, Placing}, ui::render_state::{DrawInfo, Placing},
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// makes your surfaces look pretty /// makes your surfaces look pretty
pub struct Painter<'a> { 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 /// 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 /// 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. /// only moves its child does not pin its drawing to a rel base.
fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc { fn resolve_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
for axis in AXES { for axis in Axis::BOTH {
if let Some(span) = place[axis].narrows_rel_base() { if let Some(span) = place[axis].narrows_rel_base() {
let len = span.len(); let len = span.len();
let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis))); 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>, id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>, place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> { ) -> 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 region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
@@ -198,7 +197,7 @@ impl<'a> Painter<'a> {
self.children.push(id.id()); self.children.push(id.id());
} }
let px = rel_base.to_px(self.window); 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(), id.id(),
DrawInfo { DrawInfo {
layer: self.layer, layer: self.layer,
@@ -217,8 +216,8 @@ impl<'a> Painter<'a> {
None, None,
self.rsc, self.rsc,
); );
let holds = self.in_parent(holds, region, place, declared); let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
let answer_holds = self.in_parent(answer_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()) { match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
Some((_, kept)) => *kept = holds, Some((_, kept)) => *kept = holds,
None => self.under.push((id.id(), holds)), None => self.under.push((id.id(), holds)),
@@ -226,7 +225,7 @@ impl<'a> Painter<'a> {
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size, size: drawn.answer.size,
answer_holds, answer_holds,
} }
} }
@@ -255,8 +254,8 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>, place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into()); let place = self.resolve_rel_base(place.into());
let states_rel_base = AXES let states_rel_base = Axis::BOTH
.iter() .iter()
.any(|&axis| place[axis].stated_rel_base().is_some()); .any(|&axis| place[axis].stated_rel_base().is_some());
if states_rel_base || !self.children.contains(&id.id()) { 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<'_> { 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. /// 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 /// 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 /// is what reached the child; where only pixels did, no length of this
@@ -602,7 +601,7 @@ impl Painter<'_> {
declared: Declared, declared: Declared,
) -> LayoutHolds { ) -> LayoutHolds {
let mut result = LayoutHolds::ANY; let mut result = LayoutHolds::ANY;
for axis in AXES { for axis in Axis::BOTH {
let declared = declared[axis]; let declared = declared[axis];
let holds = holds[axis]; let holds = holds[axis];
let result = &mut result[axis]; let result = &mut result[axis];
@@ -677,16 +676,16 @@ impl LayoutLen {
} }
} }
impl PlaceDesc {
/// Where a widget's drawing goes inside the part its parent gave it: what /// 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 /// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it. /// part wherever the answer fills it.
/// ///
/// The length it reported is a length of its rel base, and the part is one too, /// The length it reported is a length of its rel base, and the part is one
/// so this takes one from the other rather than composing it into the part. /// too, so this takes one from the other rather than composing it into the
/// That is what makes a fraction the same fraction wherever the part it is /// part. That is what makes a fraction the same fraction wherever the part
/// placed in sits and however long it is -- the fraction is resolved once, /// it is placed in sits and however long it is -- the fraction is resolved
/// here, against the rel base it was reported of. /// once, here, against the rel base it was reported of.
impl PlaceDesc {
pub(crate) fn placement( pub(crate) fn placement(
self, self,
region: UiRegion, region: UiRegion,
@@ -695,7 +694,7 @@ impl PlaceDesc {
align: RegionAlign, align: RegionAlign,
) -> UiRegion { ) -> UiRegion {
let mut placed = region; let mut placed = region;
for axis in AXES { for axis in Axis::BOTH {
let reported = size[axis]; let reported = size[axis];
if reported.fills(declared[axis], self[axis].does_fill()) { if reported.fills(declared[axis], self[axis].does_fill()) {
continue; continue;
@@ -725,7 +724,7 @@ impl PlaceDesc {
let given = self.of(own, align); let given = self.of(own, align);
let mut rel_base = parent_rel_base; let mut rel_base = parent_rel_base;
let mut region = given; let mut region = given;
for axis in AXES { for axis in Axis::BOTH {
let base = self[axis] let base = self[axis]
.stated_rel_base() .stated_rel_base()
.unwrap_or_else(|| parent_rel_base[axis]); .unwrap_or_else(|| parent_rel_base[axis]);
+82 -78
View File
@@ -1,14 +1,12 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::{ use crate::{
ActiveData, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx,
Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion,
UiSpan, UiVec2, Weight, WidgetId, Widgets, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// Where a widget is drawn: what its parent decides about the draw besides /// Where a widget is drawn: what its parent decides about the draw besides
/// the boxes themselves. /// the boxes themselves.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -36,6 +34,14 @@ pub(super) struct DrawInfo {
pub px: PxVec2, 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 /// 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. /// drawing is in, and what else one ask of a child is decided from.
pub(super) struct Placing { pub(super) struct Placing {
@@ -113,7 +119,7 @@ impl UiRenderState {
// it will ask either again. // it will ask either again.
let answer = active let answer = active
.answer .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) answer && active.holds.contains(size, active.rel_base, active.region)
}); });
if !stands { if !stands {
@@ -207,7 +213,7 @@ impl UiRenderState {
info: DrawInfo, info: DrawInfo,
mut old: Option<ActiveData>, mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds, LayoutHolds) { ) -> Drawn {
let old_parent = old let old_parent = old
.as_ref() .as_ref()
.or_else(|| self.active.get(&id)) .or_else(|| self.active.get(&id))
@@ -233,9 +239,9 @@ impl UiRenderState {
.then(|| self.retained_answer(id, region, info)) .then(|| self.retained_answer(id, region, info))
.flatten() .flatten()
.and_then(|answer| { .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) self.try_reuse(id, region, placed, info, rsc)
.map(|()| answer) .then_some(answer)
}); });
let answer = reused.unwrap_or_else(|| { let answer = reused.unwrap_or_else(|| {
if old.is_none() { if old.is_none() {
@@ -245,7 +251,7 @@ impl UiRenderState {
// Where the drawing goes: the part its parent gave it, with the // Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left // answer placed inside that part on any axis the parent left
// open. // open.
let placed = info.placed.placement(region, answer.0, declared, align); let placed = info.placed.placement(region, answer.size, declared, align);
if placed != region { if placed != region {
self.relocate(id, placed, info, rsc); self.relocate(id, placed, info, rsc);
} }
@@ -273,7 +279,10 @@ impl UiRenderState {
{ {
old_parent.children.retain(|child| *child != id); 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`. /// Calls a widget's `draw` and keeps what it drew in `region`.
@@ -284,7 +293,7 @@ impl UiRenderState {
info: DrawInfo, info: DrawInfo,
old: Option<ActiveData>, old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) { ) -> Answer {
let rel_base = info.rel_base; let rel_base = info.rel_base;
let (move_idx, region, retired_move) = match info.region_node { let (move_idx, region, retired_move) = match info.region_node {
// A node entry is only a translation. Its local box keeps the // 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. // was offered reports the height it needs.
debug_assert!( debug_assert!(
mask == info.mask mask == info.mask
|| AXES || Axis::BOTH.into_iter().all(|axis| size.within_box(
.into_iter() region,
.all(|axis| size.within_box(region, self.output_size, axis)), self.output_size,
axis
)),
"'{}' ({id:?}) clips to {px:?} and reports {size}", "'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id), rsc.widgets().label(id),
); );
@@ -417,7 +428,7 @@ impl UiRenderState {
// that many pixels of this window -- the same pin a widget that read // that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing. // its rel base took for its drawing.
let mut own_holds = own; 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); let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
if fraction { if fraction {
own_holds[axis].rel_base = Some(info.rel_base[axis]); own_holds[axis].rel_base = Some(info.rel_base[axis]);
@@ -489,7 +500,10 @@ impl UiRenderState {
}; };
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, 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 /// 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 /// drawing ended up. Alignment is exactly that case: the first box is the
/// question and the smaller placed box holds the drawing. Whether 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. /// answer is stale at all is its caller's question, asked once there.
fn retained_answer( fn retained_answer(&self, id: WidgetId, region: UiRegion, info: DrawInfo) -> Option<Answer> {
&self,
id: WidgetId,
region: UiRegion,
info: DrawInfo,
) -> Option<(Size, LayoutHolds)> {
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
let has_region_node = active.move_idx != active.parent_move;
if !active.drawn if !active.drawn
|| has_region_node != info.region_node || active.is_region_node() != info.region_node
|| active.parent_move != info.parent_move || active.parent_move != info.parent_move
{ {
return None; return None;
} }
let answer = active.answer?; let answer = active.answer?;
answer answer
.1 .holds
.contains(self.output_size, info.rel_base, region) .contains(self.output_size, info.rel_base, region)
.then_some(answer) .then_some(answer)
} }
@@ -546,7 +554,7 @@ impl UiRenderState {
placed: UiRegion, placed: UiRegion,
info: DrawInfo, info: DrawInfo,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Option<()> { ) -> bool {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts); diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) { if rsc.widgets().needs_redraw.contains(&id) {
@@ -555,19 +563,20 @@ impl UiRenderState {
diag::bump(Counter::ReuseDirty); diag::bump(Counter::ReuseDirty);
diag::reuse(id, ReuseOutcome::Dirty); 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 { if !active.drawn {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::reuse(id, ReuseOutcome::Undrawn); diag::reuse(id, ReuseOutcome::Undrawn);
return None; return false;
} }
let has_region_node = active.move_idx != active.parent_move; if active.is_region_node() != info.region_node {
if has_region_node != info.region_node {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseWrongNode); diag::bump(Counter::ReuseWrongNode);
return None; return false;
} }
// Drawn on another layer: the drawing sits in that layer's list and // 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 // paints at its moment, which no amount of geometry says. A container
@@ -580,10 +589,10 @@ impl UiRenderState {
diag::bump(Counter::ReuseWrongLayer); diag::bump(Counter::ReuseWrongLayer);
diag::reuse(id, ReuseOutcome::WrongLayer); diag::reuse(id, ReuseOutcome::WrongLayer);
} }
return None; return false;
} }
if active.parent_mask != info.mask { if active.parent_mask != info.mask {
return None; return false;
} }
// Drawn somewhere else in the tree: its box is in coordinates it no // Drawn somewhere else in the tree: its box is in coordinates it no
// longer sits in, and its slot names the wrong parent. // longer sits in, and its slot names the wrong parent.
@@ -593,7 +602,7 @@ impl UiRenderState {
diag::bump(Counter::ReuseWrongParent); diag::bump(Counter::ReuseWrongParent);
diag::reuse(id, ReuseOutcome::WrongParent); diag::reuse(id, ReuseOutcome::WrongParent);
} }
return None; return false;
} }
// In pixels, because the box is a fraction of the window and that // 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 // 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 // Which of the three said no, so a rel base that redraws more
// than it should says where to look. They overlap: a drawing // than it should says where to look. They overlap: a drawing
// can be outside two of them at once. // can be outside two of them at once.
for axis in AXES { for axis in Axis::BOTH {
let holds = active.holds[axis]; let holds = active.holds[axis];
let len = region[axis].len(); let len = region[axis].len();
let window = self.output_size[axis]; let window = self.output_size[axis];
@@ -628,10 +637,10 @@ impl UiRenderState {
diag::bump(Counter::ReuseOutside); diag::bump(Counter::ReuseOutside);
diag::reuse(id, ReuseOutcome::Outside); diag::reuse(id, ReuseOutcome::Outside);
} }
return None; return false;
} }
self.relocate(id, placed, info, rsc); self.relocate(id, placed, info, rsc);
Some(()) true
} }
/// Puts a retained drawing where its parent now has it, without drawing: /// Puts a retained drawing where its parent now has it, without drawing:
@@ -644,14 +653,14 @@ impl UiRenderState {
"'{}' ({id:?}) placed while marked to draw", "'{}' ({id:?}) placed while marked to draw",
rsc.widgets().label(id) rsc.widgets().label(id)
); );
let has_region_node = active.move_idx != active.parent_move; let is_region_node = active.is_region_node();
let local = match has_region_node { let local = match is_region_node {
true => placed.at_origin(), true => placed.at_origin(),
false => placed, false => placed,
}; };
let moved = active.placement != local; let moved = active.placement != local;
let slot = active.move_idx; let slot = active.move_idx;
if has_region_node { if is_region_node {
self.moves.set(slot, placed.as_translation()); self.moves.set(slot, placed.as_translation());
} }
if moved { if moved {
@@ -663,23 +672,13 @@ impl UiRenderState {
active.placed = info.placed; active.placed = info.placed;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
match (moved, has_region_node) { let (counter, outcome) = match (moved, is_region_node) {
(true, true) => diag::bump(Counter::ReuseMoved), (true, true) => (Counter::ReuseMoved, ReuseOutcome::Moved),
(true, false) => diag::bump(Counter::ReuseRemapped), (true, false) => (Counter::ReuseRemapped, ReuseOutcome::Remapped),
(false, _) => diag::bump(Counter::ReuseExact), (false, _) => (Counter::ReuseExact, ReuseOutcome::Exact),
} };
diag::reuse( diag::bump(counter);
id, diag::reuse(id, outcome);
if moved {
if has_region_node {
ReuseOutcome::Moved
} else {
ReuseOutcome::Remapped
}
} else {
ReuseOutcome::Exact
},
);
} }
} }
@@ -712,7 +711,7 @@ impl UiRenderState {
parent: Some(at.id), parent: Some(at.id),
depth: at.depth + 1, depth: at.depth + 1,
parent_move: at.move_idx, parent_move: at.move_idx,
region_node: active.move_idx != active.parent_move, region_node: active.is_region_node(),
mask: at.mask, mask: at.mask,
rel_base, rel_base,
region, region,
@@ -755,16 +754,18 @@ impl UiRenderState {
move_idx: active.move_idx, move_idx: active.move_idx,
mask: active.mask, mask: active.mask,
}; };
let children = active.children.len(); // Taken out and put back so that placing a child can borrow the state
for index in 0..children { // it needs; nothing on that path reads this widget's own child list.
let child = self.active[&id].children[index]; let children = std::mem::take(&mut self.active.get_mut(&id).unwrap().children);
for &child in &children {
self.place_child(child, &at, rsc); 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 /// A reused subtree keeps its shape, so each widget in it keeps its depth
/// same amount -- and where the top of it did not move, none of it did, /// under the top -- and where the top's own depth did not change, none of
/// which is what makes this free in the ordinary case. /// them did, which is what makes this free in the ordinary case.
fn redepth(&mut self, id: WidgetId, depth: usize) { fn redepth(&mut self, id: WidgetId, depth: usize) {
let Some(active) = self.active.get_mut(&id) else { let Some(active) = self.active.get_mut(&id) else {
return; return;
@@ -773,18 +774,21 @@ impl UiRenderState {
return; return;
} }
active.depth = depth; active.depth = depth;
let children = active.children.len(); // Taken out and put back so the walk can borrow the state it needs;
for index in 0..children { // it only ever goes further down, so it reads no list but its own.
let child = self.active[&id].children[index]; let children = std::mem::take(&mut active.children);
for &child in &children {
self.redepth(child, depth + 1); self.redepth(child, depth + 1);
} }
self.active.get_mut(&id).unwrap().children = children;
} }
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool { fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
let Some(widget) = rsc.widgets().get_dyn(id) else { let Some(widget) = rsc.widgets().get_dyn(id) else {
return true; return true;
}; };
AXES.into_iter() Axis::BOTH
.into_iter()
.all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis])) .all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis]))
} }
@@ -1118,13 +1122,13 @@ impl UiRenderState {
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc); 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(); let active = self.active.get_mut(&id).unwrap();
// A wider contract does not invalidate the guarantee the parent kept. // A wider contract does not invalidate the guarantee the parent kept.
// Retain that guarantee so widening and narrowing back do not churn it. // Retain that guarantee so widening and narrowing back do not churn it.
if let Some((size, holds)) = was_answer if let Some(was) = was_answer
&& answer.0 == size && drawn.answer.size == was.size
&& answer.1.covers(holds) && drawn.answer.holds.covers(was.holds)
{ {
active.answer = was_answer; 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 { 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 { fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = self[axis]; let len = self[axis];
let window = window[axis]; let window = window[axis];
+6 -6
View File
@@ -16,9 +16,9 @@ impl Widget for Scroll {
let answer_len = painter let answer_len = painter
.widget_at(&self.inner, PlaceDesc::WHOLE.fills()) .widget_at(&self.inner, PlaceDesc::WHOLE.fills())
.len(self.axis); .len(self.axis);
let fixed = painter.to_px(answer_len.without_leftover(), self.axis); let answer_px = painter.to_px(answer_len.without_leftover(), self.axis);
self.container_len = container_len; self.container_len = container_len;
self.content_len = fixed.max(container_len); self.content_len = answer_px.max(container_len);
if self.snap_end { if self.snap_end {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
@@ -31,10 +31,10 @@ impl Widget for Scroll {
// the drawing holds for that length alone. One scrolled part way sits // 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 // where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length. // the end, it moves with every length.
let fixed_len = answer_len.is_px(); let answer_is_px = answer_len.is_px();
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { if answer_is_px && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, fixed..=Px::MAX); painter.holds(self.axis, answer_px..=Px::MAX);
} else if fixed_len && !self.snap_end { } else if answer_is_px && !self.snap_end {
let left = self.content_len - self.amt; let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left); painter.holds(self.axis, Px::MIN..=left);
} }
+33 -23
View File
@@ -10,21 +10,18 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The row: this span's own box, as a length of the rel base its children // The row this span lays its children out along, as a length of the
// are laid out against. Its start is nothing's business -- a slot is // rel base they are laid out against. Where it starts is nothing's
// a length from it -- so what this reads is the length alone. // business -- a slot is a length from there -- so what this reads is
let far = painter.region_len(axis); // the length alone.
let along = |from: Len, to: Len| match self.dir.sign { let row = painter.region_len(axis);
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
// A length for every child before their final slots are chosen: from // 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 // 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 // through unchanged, so `rel(0.5)` is half the area this span was
// given whatever else is in it and wherever this child sits among // given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the // them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there. // cursor, because a text has to wrap at the width actually there.
let mut cursor = Len::rel_min(); let mut cursor = Len::ZERO;
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let len = match painter.size_hint(child, axis) { let len = match painter.size_hint(child, axis) {
@@ -33,7 +30,7 @@ impl Widget for Span {
// Across itself the child sits where its own alignment // Across itself the child sits where its own alignment
// says, in the whole of the row: a span is what contains // says, in the whole of the row: a span is what contains
// its children there, and nothing divides that axis. // its children there, and nothing divides that axis.
let room = along(cursor, far).shifted_desc().on_axis(axis); let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
painter.widget_at(child, room).len(axis) painter.widget_at(child, room).len(axis)
} }
}; };
@@ -55,7 +52,7 @@ impl Widget for Span {
// What is left for the shares to divide: the row less everything // 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. // fixed, as a length of the rel base rather than a number of pixels.
let room = far - total.without_leftover(); let room = row - total.without_leftover();
// Whether anything is left over is a question in pixels: `rel(0.5)` // 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` // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the // itself, and answered back through the same expression, so the
@@ -65,10 +62,10 @@ impl Widget for Span {
// sign of `room.rel`, which `through` already reads. What the // sign of `room.rel`, which `through` already reads. What the
// generated oracle checks is the consequence, since which children // generated oracle checks is the consequence, since which children
// exist at all turns on this. // exist at all turns on this.
let mut shares = false; let any_leftover = total.leftover > Weight::ZERO;
if total.leftover > Weight::ZERO { let has_room = any_leftover && painter.to_px(room, axis) > Px::ZERO;
shares = painter.to_px(room, axis) > Px::ZERO; if any_leftover {
let holds = match shares { let holds = match has_room {
true => Holds::from(Px::STEP..=Px::MAX), true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO), false => Holds::from(Px::MIN..=Px::ZERO),
}; };
@@ -87,24 +84,26 @@ impl Widget for Span {
// than stepped from the last child: the share of the room is // than stepped from the last child: the share of the room is
// rounded, and taking each end from the one before it would carry // rounded, and taking each end from the one before it would carry
// every rounding along the row. // every rounding along the row.
let mut fixed = Len::rel_min(); let mut fixed = Len::ZERO;
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
let reached = |fixed: Len, taken: Weight| match taken == Weight::ZERO { // Nothing divides the room where no child asked for any of it, and a
true => fixed, // ratio of a whole of nothing has no answer.
false => fixed + room.scale(Rel::ratio(taken, total.leftover)), let reached = |fixed: Len, taken: Weight| match any_leftover {
false => fixed,
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
}; };
for (child, &len) in self.children.iter().zip(&lens) { for (child, &len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over, // 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 // when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows. // pixels or a fraction keeps those and overflows.
if len.is_only_leftover() && !shares { if len.is_only_leftover() && !has_room {
painter.undraw(child); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let from = reached(fixed, taken); let from = reached(fixed, taken);
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && has_room {
taken += len.leftover; taken += len.leftover;
} }
fixed += len.without_leftover(); fixed += len.without_leftover();
@@ -116,9 +115,9 @@ impl Widget for Span {
// it, since a text wraps at the width it is actually given. A // it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so a drawing made in the // 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. // room is put there as it is, and one not made yet is made here.
let slot = along(from, to); let slot = self.slot(row, from, to);
let mut place = slot.shifted_desc().fills().on_axis(axis); let mut place = slot.shifted_desc().fills().on_axis(axis);
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && has_room {
place = place.rel_base(axis, slot.len()); place = place.rel_base(axis, slot.len());
} }
let used = painter.place_at(child, place).len(!axis); let used = painter.place_at(child, place).len(!axis);
@@ -152,6 +151,17 @@ impl Widget for Span {
} }
impl Span { impl Span {
/// The stretch of the row between two distances from where this span
/// starts laying children out, as a span of its own box. A negative
/// direction lays out from the far end, so the same two distances mirror
/// in a row `row` long.
fn slot(&self, row: Len, from: Len, to: Len) -> UiSpan {
match self.dir.sign {
Sign::Pos => from.to(to),
Sign::Neg => (row - to).to(row - from),
}
}
pub fn empty(dir: Dir) -> Self { pub fn empty(dir: Dir) -> Self {
Self { Self {
children: Vec::new(), children: Vec::new(),
+1 -1
View File
@@ -125,7 +125,7 @@ fn moving_an_ordinary_subtree_remaps_its_mask() {
let active = &h.render.active[&masked.id()]; let active = &h.render.active[&masked.id()];
assert_eq!( assert_eq!(
h.rsc.ui().masks[active.mask.idx()].region, h.rsc.ui().masks[active.mask.idx()].region,
UiRegion::new(UiSpan::new(Len::px(150.0), Len::rel_max()), UiSpan::FULL,) UiRegion::new(UiSpan::new(Len::px(150.0), Len::FULL), UiSpan::FULL,)
); );
assert_corners!(h, inner, (150, 0), (400, 200)); assert_corners!(h, inner, (150, 0), (400, 200));
} }