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:
1 parent
6c84b6f2cb
commit
3da1c71870
9 files changed
+167
-150
No files matched your search
+82
-78
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user