WIP: a widget's region stays put and its placement moves in it

The protocol split: `region` is the box a parent gives a widget -- what a
fraction it declares or reports is a fraction of, and the coordinates
every region it writes composes within -- and it is the same box on the
ask that measures and the ask that places. `placement` is what of that
region the drawing takes, chosen by the parent per axis or by the
widget's own answer and alignment.

That is what stops a fraction being resolved twice: the placing ask no
longer hands the widget its own answer as its box, so nothing under it
re-resolves against a box that came from its own report. `reports_of`
and `decided` are gone, folded into the two regions; `box_of` is gone;
`declared_box` becomes `ask_box`, which gives a rule the region's length
and takes the position from the placement.

84 of 92 suite tests pass. Five text and region-node cases still diverge
warm against cold, and three count a second widget draw where a span's
measuring ask and its placing ask give different placements.
This commit is contained in:
iris-ai committed 2026-09-17 13:53:14 -04:00
1 parent e44dea34b4
commit 5fcace1bfa
11 files changed
+346 -206

No files matched your search

+88 -51
View File
@@ -1,6 +1,6 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, placed_box, placed_lens};
use crate::ui::painter::{ask_box, declared_lens, placed_box, placed_lens};
use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
@@ -30,10 +30,27 @@ pub(super) struct DrawInfo {
/// parent's own, which is where every pixel length in layout comes from.
pub px: PxVec2,
pub offered_px: PxVec2,
/// The axes along which the parent chose this box from the widget's own
/// answer, so the answer is not placed inside it again. See
/// [`Painter::widget_at`].
pub decided: [bool; 2],
/// What of that region the parent chose to put the drawing in, per axis.
/// `None` leaves the axis to the widget's own answer and its alignment.
/// See [`Painter::widget_at`].
pub placement: [Option<UiSpan>; 2],
}
impl DrawInfo {
/// The axes the parent chose the placement on, which are the axes the
/// answer is not placed inside its region again.
fn decided(&self) -> [bool; 2] {
self.placement.map(|span| span.is_some())
}
/// The placement to draw in before the answer is known: what the parent
/// chose, and the whole region on any axis it left open.
fn offered_placement(&self) -> UiRegion {
UiRegion {
x: self.placement[0].unwrap_or(UiSpan::FULL),
y: self.placement[1].unwrap_or(UiSpan::FULL),
}
}
}
pub struct UiRenderState {
@@ -127,7 +144,7 @@ impl UiRenderState {
offer_len: UiVec2::FULL_SIZE,
px,
offered_px: px,
decided: [false; 2],
placement: [None; 2],
}
}
@@ -181,11 +198,13 @@ impl UiRenderState {
}
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
declared_box(
ask_box(
UiRegion::FULL,
declared_lens(widgets, id),
widgets.alignment(id),
[None; 2],
)
.0
}
pub(super) fn draw_inner(
@@ -217,41 +236,41 @@ impl UiRenderState {
if old.is_none() {
old = self.remove(id, false, rsc);
}
self.draw_at(id, region, info, old.take(), rsc)
self.draw_at(id, region, info.offered_placement(), info, old.take(), rsc)
});
let declared = declared_lens(rsc.widgets(), id);
// The second, final ask is in a box chosen from the answer on both
// axes, which is also what makes it terminate.
let lens = placed_lens(answer.0, declared, info.decided);
let placed = placed_box(region, lens, align);
let placed_info = DrawInfo {
px: lens.to_px(info.px),
decided: [true; 2],
..info
// Where the drawing goes, in the region's own coordinates: what the
// parent chose, and on any axis it left open, what the answer took of
// the region placed by the widget's alignment. The region itself does
// not change, so nothing under it resolves a fraction a second time.
let lens = placed_lens(answer.0, declared, info.decided());
let own = placed_box(UiRegion::FULL, lens, align);
let placement = UiRegion {
x: info.placement[0].unwrap_or(own.x),
y: info.placement[1].unwrap_or(own.y),
};
self.place(id, placed, placed_info, rsc);
self.place(id, region, placement, info, rsc);
// The answer is only reusable while both parts of the operation are:
// what the widget reported in the box it was asked in, and what it
// drew in the box its report selected. Express the latter's contract
// back in terms of the box asked in before handing it to the parent.
// what the widget reported, and what it drew once placed. Both are
// ranges of the region's own lengths, since that is the box neither
// ask changes.
let drawing_holds = self.active[&id].holds;
let mut settled = answer;
for axis in AXES {
settled.1[axis as usize] =
settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis)));
settled.1[axis as usize] = settled.1[axis as usize].and(drawing_holds[axis as usize]);
}
let active = self.active.get_mut(&id).unwrap();
// Whoever asked owns how the box was reached: the box it stated, and
// what of that box the answer then took. A local redraw asks the
// same question again from these.
active.given = region;
active.region = region;
active.given_len = info.given_len;
active.offer_len = info.offer_len;
active.answer = settled;
active.decided = info.decided;
active.decided = info.decided();
active.own_align = align;
// A subtree can be reused whole under a different parent -- same box,
// same layer, same region node -- and nothing in the drawing says it
@@ -268,17 +287,33 @@ impl UiRenderState {
settled
}
/// Draws a widget in the final box its answer chose, reusing the drawing
/// already there where its retained contract holds for that box. The
/// symbolic box can be unchanged while the box it sits in changed pixel
/// length, so what reuse checks is the box in pixels.
fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
if self.try_reuse(id, placed, info, rsc).is_none() {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, placed, info, old, rsc);
/// Puts the drawing where the answer says it goes. A drawing that never
/// read its placement is the same drawing wherever it is put, so all that
/// changes is what box the widget is recorded as occupying; one that read
/// it is drawn again, and only where the placement it read has moved.
fn place(
&mut self,
id: WidgetId,
region: UiRegion,
placement: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) {
let active = &self.active[&id];
// A drawing that never read its placement is the same drawing
// wherever it is put, so only what the widget is recorded as
// occupying changes; one that read it stands only for the placement
// it read. Either way the drawing still has to be where the region
// now is, which is what a retained answer on its own does not do.
let stands = !active.reads_placement || active.placement == placement;
if stands && self.try_reuse(id, region, info, rsc).is_some() {
self.active.get_mut(&id).unwrap().placement = placement;
return;
}
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, region, placement, info, old, rsc);
}
/// Calls a widget's `draw` and keeps what it drew in `region`.
@@ -286,6 +321,7 @@ impl UiRenderState {
&mut self,
id: WidgetId,
region: UiRegion,
placement: UiRegion,
info: DrawInfo,
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
@@ -317,6 +353,8 @@ impl UiRenderState {
let mut painter = Painter {
state: self,
region: local,
placement,
reads_placement: false,
px,
mask: info.mask,
layer: info.layer,
@@ -351,6 +389,8 @@ impl UiRenderState {
state: _,
rsc: _,
region: _,
placement: _,
reads_placement,
px: _,
mask,
textures,
@@ -425,7 +465,7 @@ impl UiRenderState {
offer_len: UiVec2::FULL_SIZE,
px,
offered_px: px,
decided: [false; 2],
placement: [None; 2],
},
rsc,
);
@@ -436,10 +476,8 @@ impl UiRenderState {
let active = ActiveData {
id,
region,
// The box a placing ask draws in is a part of the one its parent
// gave, which `draw_inner` writes back over these once the
// placement is done.
given: region,
placement,
reads_placement,
given_len: info.given_len,
offer_len: info.offer_len,
// Whoever asked writes the answer, if this was the asking.
@@ -454,7 +492,7 @@ impl UiRenderState {
children,
size_deps,
declared: declared_lens(rsc.widgets(), id),
decided: info.decided,
decided: info.decided(),
own_align: rsc.widgets().alignment(id),
move_idx,
parent_move: info.parent_move,
@@ -540,11 +578,7 @@ impl UiRenderState {
// Nothing above the root: the window is where a fraction becomes
// pixels, which is also the whole of the box the root is given.
let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) {
Some(parent) => {
let (given, offer) = self.asked_px(parent.id);
let lens = placed_lens(parent.answer.0, parent.declared, parent.decided);
(lens.to_px(given), offer)
}
Some(parent) => self.asked_px(parent.id),
None => (self.output_size, self.output_size),
};
let px = active.given_len.to_px(parent_px);
@@ -639,7 +673,6 @@ impl UiRenderState {
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.given = region;
active.given_len = info.given_len;
active.offer_len = info.offer_len;
#[cfg(feature = "layout-diagnostics")]
@@ -694,7 +727,6 @@ impl UiRenderState {
rsc: &mut dyn UiRsc,
) {
let active = self.active.get_mut(&id).unwrap();
active.given = remap.apply(active.given);
if active.move_idx != parent_move {
let region = remap.apply(active.region);
active.region = region;
@@ -792,7 +824,8 @@ impl UiRenderState {
ActiveData {
id,
region: UiRegion::FULL,
given: UiRegion::FULL,
placement: UiRegion::FULL,
reads_placement: false,
given_len: UiVec2::FULL_SIZE,
offer_len: UiVec2::FULL_SIZE,
answer: (size, [Holds::ANY; 2]),
@@ -951,8 +984,9 @@ impl UiRenderState {
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?;
active.drawn.then(|| {
let placed = active.placement.within(&active.region);
self.moves
.resolve(active.parent_move, active.region)
.resolve(active.parent_move, placed)
.to_px(self.output_size)
})
}
@@ -1031,9 +1065,12 @@ impl UiRenderState {
offer_len: active.offer_len,
px: given_px,
offered_px,
decided: active.decided,
// The same question its parent asked: the axes its parent chose
// the placement on, put back where they were.
placement: AXES
.map(|axis| active.decided[axis as usize].then(|| *active.placement.axis(axis))),
};
let (given, was_answer) = (active.given, active.answer);
let (given, was_answer) = (active.region, active.answer);
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);