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:
1 parent
58ce74dd7d
commit
55df32a33c
21 files changed
+465
-490
No files matched your search
+59
-71
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user