Ask the root the way every other widget is asked

The root had a layout path of its own: `root_layout` read its declared
lengths against the window, while every other widget's box came of
`Painter::widget_at`, where a rule of the widget's own -- a share with pixels
or a fraction beside it -- is compared against the offer and can take the box
past it. So a share on the root was the window whatever it asked for, which
`docs/LAYOUT_LOG.md` recorded as a gap rather than fixing, and any later rule
that reads the offer would have had to be written twice.

There is one box nobody drew, and that is the whole of what the root is
asked in. `Placing::WINDOW` says it -- the full output, fractions of the full
output, no move entry and no mask -- and `Placing::ask` is then the one place
a box is decided, called by the painter, by a local redraw, and by the root's
first draw. The root's own path is what is left of it: a widget with no
parent keeps different bookkeeping, not a different layout.

Measured in a 400 px window, a probe under each of three parents, which now
agree on every row where two of them agreed before:

    rule                      as root   wrapped   in a span
    leftover(1)                   400       400         400
    px(50) + leftover(1)          400       400         400
    px(500) + leftover(1)         500       500         500   (was 400 as root)
    rel(0.5) + leftover(1)        400       400         400
    rel(2.0) + leftover(1)        800       800         800   (was 400 as root)
    px(500)                       500       500         500
    rel(0.5)                      200       200         200

The comparison is kept on the widget asked about rather than on the asker,
which is what makes the root need nothing of its own: a window range means
the same thing at either end of an ask, `in_parent` passes one up unchanged,
and the asker ends up holding it through the child's drawing exactly as it
did when `longer_than` narrowed the asker directly. The root has no asker, so
its own record is the only place that range can live -- and `resize` already
checks that record, so a share crossing its length is caught with no new
code. `a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing`
now runs at the root too: 500 at a 400 window, 900 at 900, 500 again at 400.

Two things this changes beyond the share. `DrawInfo::asked` is now the place
the parent offered rather than the place the ask came to, so a local redraw
re-decides the rule instead of re-reading the decision -- the two were the
same until a rule could move the box. And the root's `is_region_node` is
read, where the old path passed `false`: a region-node root now gets its
entry, whose translation is the identity, pinned by
`a_region_node_root_is_a_region_node`.

Format, clippy with and without layout-diagnostics, and the suite (136 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`2dba90b` across all 34,571 boxes, and the three seed scans pass: 400 at
depth 5 (61s), 1,000 at depth 6 (155s), 2,000 at depth 4 (291s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-20 13:51:19 -04:00
1 parent 2dba90bd0f
commit 0d0326769c
4 files changed
+295 -176

No files matched your search

+80 -63
View File
@@ -4,6 +4,7 @@ use crate::{
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,
ui::painter::Ask,
util::{HashMap, Vec2},
};
@@ -23,11 +24,14 @@ pub(super) struct DrawInfo {
/// The box the widget is asked in, in its parent region node's
/// coordinates.
pub region: UiRegion,
/// Where the widget is put, and where it was asked, as parts of the
/// parent's box. See [`PlaceDesc`]. The two are one ask's place until the
/// parent puts the answer somewhere else.
/// Where the widget is put, and what its parent offered it, as parts of
/// the parent's box. See [`PlaceDesc`]. The two are one place until a
/// rule of the widget's own takes it past the offer, or the parent puts
/// the answer somewhere else.
pub placed: PlaceDesc,
pub asked: PlaceDesc,
/// What the ask that gave it those two holds for. See [`Ask::holds`].
pub ask_holds: LayoutHolds,
/// Whether the parent already asked about this widget in this draw.
pub re_asked: bool,
}
@@ -43,7 +47,8 @@ pub(super) struct Drawn {
/// 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 {
pub id: WidgetId,
/// The widget whose box this is, and nothing for the window.
pub id: Option<WidgetId>,
pub region: UiRegion,
pub rel_base: UiVec2,
pub depth: usize,
@@ -51,6 +56,21 @@ pub(super) struct Placing {
pub mask: MaskIdx,
}
impl Placing {
/// The window, which is what the root is placed within. Nothing above the
/// root narrowed a box or chose where it goes, so it is asked in the whole
/// output and its fractions are of the whole output -- an ordinary ask,
/// from the one box nobody drew.
pub const WINDOW: Self = Self {
id: None,
region: UiRegion::FULL,
rel_base: UiVec2::FULL_SIZE,
depth: 0,
move_idx: MoveIdx::NONE,
mask: MaskIdx::NONE,
};
}
pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers,
@@ -124,20 +144,21 @@ impl UiRenderState {
}
}
/// The root is asked about in the output. Its own rules narrow both its
/// rel base and box; nothing above it chose a different one.
fn root_info(&self, rel_base: UiVec2, region: UiRegion) -> DrawInfo {
/// The root's first draw: the ask [`Placing::WINDOW`] answered, with the
/// bookkeeping a widget with no parent carries.
fn root_info(&self, ask: &Ask, region_node: bool) -> DrawInfo {
DrawInfo {
layer: 0,
parent: None,
depth: 1,
depth: Placing::WINDOW.depth + 1,
parent_move: MoveIdx::NONE,
region_node: false,
region_node,
mask: MaskIdx::NONE,
rel_base,
region,
placed: PlaceDesc::WHOLE,
rel_base: ask.rel_base,
region: ask.region,
placed: ask.place,
asked: PlaceDesc::WHOLE,
ask_holds: ask.holds,
re_asked: false,
}
}
@@ -184,24 +205,13 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
if let Some(id) = root {
let (rel_base, region) = Self::root_layout(id.id(), rsc.widgets());
let info = self.root_info(rel_base, region);
let ask =
Placing::WINDOW.ask(rsc.widgets(), self.output_size, id.id(), PlaceDesc::WHOLE);
let info = self.root_info(&ask, rsc.widgets().is_region_node(id.id()));
self.draw_inner(id.id(), info, None, rsc);
}
}
/// The root's rel base and box: the window, taken in by the root's own
/// 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) {
PlaceDesc::WHOLE.rel_base_and_region(
UiRegion::FULL,
UiVec2::FULL_SIZE,
widgets.declared_lens(id),
widgets.alignment(id),
)
}
pub(super) fn draw_inner(
&mut self,
id: WidgetId,
@@ -329,7 +339,10 @@ impl UiRenderState {
mask_slot,
children: Vec::new(),
size_deps: Vec::new(),
own: LayoutHolds::ANY,
// What the ask holds for is part of what the drawing holds for:
// a box the widget's own rule took past the offer was decided in
// this window, and at the root nobody else keeps that range.
own: info.ask_holds,
under: Vec::new(),
answer_under: LayoutHolds::ANY,
depth: info.depth,
@@ -462,6 +475,7 @@ impl UiRenderState {
region: UiRegion::FULL,
placed: PlaceDesc::WHOLE,
asked: PlaceDesc::WHOLE,
ask_holds: LayoutHolds::ANY,
re_asked: false,
},
rsc,
@@ -697,7 +711,7 @@ impl UiRenderState {
);
let info = DrawInfo {
layer: active.layer,
parent: Some(at.id),
parent: at.id,
depth: at.depth + 1,
parent_move: at.move_idx,
region_node: active.is_region_node(),
@@ -706,6 +720,8 @@ impl UiRenderState {
region,
placed: place,
asked: active.asked,
// Placing decides no box: this is the one the ask already gave.
ask_holds: LayoutHolds::ANY,
re_asked: active.re_asked,
};
self.relocate(child, placed, info, rsc);
@@ -734,7 +750,7 @@ impl UiRenderState {
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed);
}
let at = Placing {
id,
id: Some(id),
region: placed,
rel_base: info.rel_base,
depth: info.depth,
@@ -1066,29 +1082,19 @@ impl UiRenderState {
if !active.drawn {
return true;
}
// Nothing above the root resolved its rules or its alignment, so its
// box is its own to work out again against the output. Every other
// widget was given one.
let Some(parent) = active.parent else {
let (rel_base, region) = Self::root_layout(id, rsc.widgets());
let info = DrawInfo {
mask: active.parent_mask,
..self.root_info(rel_base, region)
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
self.draw_inner(id, info, old, rsc);
return true;
};
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed);
// The question its parent asked, asked again: the same place of the
// box the parent was asked in, which is the box the parent's own
// draw ran in and what its children's parts are of. Where the
// parent's answer put its own drawing is not a question anybody
// asked, and nothing is asked in it here either.
let parent_at = self.placing_of(parent, self.active[&parent].region);
let (rel_base, region) = Self::ask_again(active, &parent_at, active.asked);
// asked, and nothing is asked in it here either. The root's parent is
// the window, which no draw made and no answer can move.
let at = match active.parent {
Some(parent) => self.placing_of(parent, self.active[&parent].region),
None => Placing::WINDOW,
};
let ask = at.ask(rsc.widgets(), self.output_size, id, active.asked);
let (rel_base, region) = (ask.rel_base, ask.region);
let info = DrawInfo {
layer: active.layer,
parent: active.parent,
@@ -1098,8 +1104,9 @@ impl UiRenderState {
mask: active.parent_mask,
rel_base,
region,
placed: active.asked,
placed: ask.place,
asked: active.asked,
ask_holds: ask.holds,
re_asked: false,
};
#[cfg(feature = "layout-diagnostics")]
@@ -1127,21 +1134,31 @@ impl UiRenderState {
if active.holds.covers(was_holds) && was_holds.contains(window, rel_base, region) {
active.holds = was_holds;
}
if active.answer != was_answer || active.holds != was_holds {
// The parent retains both the answer and the drawing's validity;
// even an unchanged size can narrow the range safe for a resize.
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::SizeChanges);
diag::bump(Counter::ReaderEdges);
let changed = active.answer != was_answer || active.holds != was_holds;
// Nothing above the root retained either, so there is nobody to tell
// and nowhere else the drawing has to go back to.
if let Some(parent) = active.parent {
match changed {
// The parent retains both the answer and the drawing's
// validity; even an unchanged size can narrow the range safe
// for a resize.
true => {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::SizeChanges);
diag::bump(Counter::ReaderEdges);
}
self.mark(parent, rsc.widgets_mut());
}
// The answer stands, so where the parent put it stands: the
// fresh drawing goes back there -- the same place, of the box
// the parent's answer chose rather than the one it was asked
// in.
false => {
let at = self.placing_of(parent, self.active[&parent].placement);
self.place_in(id, &at, was_place, rsc);
}
}
self.mark(parent, rsc.widgets_mut());
} else {
// The answer stands, so where the parent put it stands: the
// fresh drawing goes back there -- the same place, of the box
// the parent's answer chose rather than the one it was asked in.
let at = self.placing_of(parent, self.active[&parent].placement);
self.place_in(id, &at, was_place, rsc);
}
true
}
@@ -1152,7 +1169,7 @@ impl UiRenderState {
fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing {
let active = &self.active[&id];
Placing {
id,
id: Some(id),
region,
rel_base: active.rel_base,
depth: active.depth,