Thread a box in pixels down the draw, one multiply from its parent's
A box in pixels was composed back up the move chain, on a grid fine enough that the walk rounded once, while a widget's offer was threaded down through its ancestors' offers. Two routes to one length, which is what `Holds::through` allowed for -- and the offer's route broke at a region node. `offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved that against the node's slot entry, which holds the box its parent *placed* the node in. Under a `Scroll` that is as long as the content rather than the viewport, so everything below was re-asked at a width its own answer had produced and the old answer confirmed itself: shrinker seed 220 on `reorder` left a widget 290px out. `ActiveData` now keeps a widget's box as lengths of its parent's box -- `given_len`, and `offer_len` for the box it was first asked about -- and `DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a time: the box its parent gave it, then the part of that box its own answer placed the drawing in, which `placed_lens` states once for both `placed_box` and the walk. `Painter::px_size` and `px_len` read that value, and `UiRenderState::asked_px` takes the same steps back up the parent chain where a local redraw starts part-way down the tree. Neither chain has a coordinate frame in it, so neither can break at a region node, and warm and cold reach every length by the same expression. Three things follow. `Holds::through` is the exact preimage of `px + floor(rel * box)` -- two divisions, no allowance, the whole of a box mapping back to itself. A local redraw asks in the box its parent gave it and only where that box is as long as the offer, which retires `redraw`'s third ask and the region-node exception beside it; `draw_inner` places the answer inside that box itself. And symbolic regions are left to the GPU, hit testing and remaps, where `Moves::resolve` is the only walk: `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region` and `slot_wide` are gone, 252 lines of `core/` net. `px` is deliberately not stored beside those lengths. A resize every widget's `Holds` admits redraws nothing, so a stored pixel length would be stale on every widget in the tree with nothing on it to say so, and refreshing it costs a walk down every reused subtree on the resize path. Instructions:u, medians of 21 runs, seed 1 at depth 8: | phase | before | after | | | --- | ---: | ---: | ---: | | `cold`, 200 frames | 313.1M | 312.9M | -0.04% | | `resize` | 408.1M | 405.6M | -0.61% | | `many` | 1,924M | 1,756M | -8.75% | | `scroll` | 357.3M | 323.4M | -9.49% | | `repaint` | 363.3M | 315.4M | -13.18% | `cold` and `resize` have all twenty-five work counters identical, so those two rows say the draw path costs the same threaded as composed. The other three do less work: `repaint` goes from 23 draw requests and 13 widget draws a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and 186 to 207 and 157. Primitive writes are unmoved in every phase. Verified: `view`, `minimal`, `random`, `tabs` and `text` render byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch replay before and after the gesture, and a live resize of `random` to 1280x800 is identical both to the old head's and to a cold render at that size. The oracle passes 100 seeds in release and 120 in debug -- the debug run is the one that exercises the `Holds` assertion -- and the fifteen shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`, which needs both halves of this to fail: the old chain with the old allowance passes it, and the old chain with the exact preimage does not. `AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the 400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the resize path re-expressing a part as a fraction of a box that changed length, not a length reached two ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b7800264d
commit
32542d0c0b
9 files changed
+399
-557
No files matched your search
+61
-57
@@ -1,9 +1,9 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter};
|
||||
use crate::{
|
||||
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget,
|
||||
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget,
|
||||
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
|
||||
WideRegion, WidgetId, Widgets,
|
||||
WidgetId, Widgets,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
||||
PrimitiveKind, TexturePrimitive,
|
||||
@@ -19,9 +19,11 @@ pub struct Painter<'a> {
|
||||
|
||||
/// This widget's box, in the coordinates of `move_idx`.
|
||||
pub(super) region: UiRegion,
|
||||
/// What this widget's slot composes to, so its own box and its children's
|
||||
/// are a step further rather than a walk back up the chain.
|
||||
pub(super) slot_wide: WideRegion,
|
||||
/// That box in pixels, which its children's are a length of: threaded
|
||||
/// down from the box this widget was given rather than composed back up
|
||||
/// the chain, so every length in layout is one multiply from its
|
||||
/// parent's and [`Holds::through`] inverts exactly that.
|
||||
pub(super) px: PxVec2,
|
||||
pub(super) mask: MaskIdx,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
@@ -29,10 +31,12 @@ pub struct Painter<'a> {
|
||||
/// The children asked about so far, so the first box each was asked in
|
||||
/// is the one recorded as its offer.
|
||||
pub(super) offered: Vec<WidgetId>,
|
||||
/// The box this widget was first asked about in, in pixels.
|
||||
/// The lengths of the box this widget was first asked about in, in
|
||||
/// pixels. Its children's offers are a fraction of it.
|
||||
pub(super) offered_px: PxVec2,
|
||||
/// Whether this draw is in that box, which makes the questions it asks
|
||||
/// the ones a cold layout asks and their answers the ones to keep.
|
||||
/// Whether this draw is in a box of those lengths, which makes the
|
||||
/// questions it asks the ones a cold layout asks and their answers the
|
||||
/// ones to keep.
|
||||
pub(super) at_offer: bool,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub(super) size_deps: Vec<WidgetId>,
|
||||
@@ -183,11 +187,20 @@ impl<'a> Painter<'a> {
|
||||
self.children.push(id.id());
|
||||
}
|
||||
let first_ask = self.offer(id.id());
|
||||
let offer = match first_ask {
|
||||
true => local,
|
||||
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
|
||||
let given_len = local.size();
|
||||
let offer_len = match first_ask {
|
||||
true => given_len,
|
||||
false => self
|
||||
.state
|
||||
.active
|
||||
.get(&id.id())
|
||||
.map_or(given_len, |a| a.offer_len),
|
||||
};
|
||||
let answers_offer = self.at_offer && local == offer;
|
||||
let px = given_len.to_px(self.px);
|
||||
let offered_px = offer_len.to_px(self.offered_px);
|
||||
// Whether this ask is the child's offer question, which is a question
|
||||
// about lengths: the same lengths somewhere else is the same question.
|
||||
let answers_offer = self.at_offer && px == offered_px;
|
||||
// The answer and what it holds for, both about the box asked in. The
|
||||
// child's record may say something else once its drawing has been
|
||||
// placed: a drawing made again in its placed box holds for that box.
|
||||
@@ -201,9 +214,10 @@ impl<'a> Painter<'a> {
|
||||
parent_move: self.move_idx,
|
||||
region_node,
|
||||
mask: self.mask,
|
||||
offer,
|
||||
offered_px: self.px_within_offer(offer),
|
||||
slot_wide: self.slot_wide,
|
||||
given_len,
|
||||
offer_len,
|
||||
px,
|
||||
offered_px,
|
||||
decided,
|
||||
},
|
||||
None,
|
||||
@@ -264,15 +278,14 @@ impl<'a> Painter<'a> {
|
||||
let declared = self.declared_lens(child);
|
||||
let align = self.rsc.widgets().alignment(child.id());
|
||||
let local = declared_box(region, declared, align);
|
||||
let within = local.within(&self.region);
|
||||
let first_ask = self.offer(child.id());
|
||||
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
||||
active.offer = local;
|
||||
active.offer_len = local.size();
|
||||
}
|
||||
if let Some(hint) = self.size_hint(child, axis) {
|
||||
return Some(hint);
|
||||
}
|
||||
let px = self.px_of(within);
|
||||
let px = local.size().to_px(self.px);
|
||||
let (size, holds) =
|
||||
self.state
|
||||
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
|
||||
@@ -300,22 +313,6 @@ impl<'a> Painter<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
/// The pixel size of a part of the box this widget was asked in.
|
||||
fn px_within_offer(&self, local: UiRegion) -> PxVec2 {
|
||||
let size = local.size();
|
||||
PxVec2::new(
|
||||
size.x.to_px(self.offered_px.x),
|
||||
size.y.to_px(self.offered_px.y),
|
||||
)
|
||||
}
|
||||
|
||||
/// A box stated as a part of this widget's slot, in pixels.
|
||||
fn px_of(&self, region: UiRegion) -> PxVec2 {
|
||||
self.slot_wide
|
||||
.select_size(®ion)
|
||||
.to_px(self.state.output_size())
|
||||
}
|
||||
|
||||
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
if !self.size_deps.contains(&child.id()) {
|
||||
self.size_deps.push(child.id());
|
||||
@@ -397,31 +394,25 @@ impl<'a> Painter<'a> {
|
||||
/// near edge. A container that reports one child's size gives every child
|
||||
/// this, so what it draws is inside what it says it occupies.
|
||||
pub fn box_of(&self, size: Size) -> UiRegion {
|
||||
placed_box(
|
||||
UiRegion::FULL,
|
||||
size,
|
||||
RegionAlign::NEAR,
|
||||
[None; 2],
|
||||
[false; 2],
|
||||
)
|
||||
let lens = placed_lens(size, [None; 2], [false; 2]);
|
||||
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
|
||||
}
|
||||
|
||||
/// This widget's box in pixels. Reading it makes the drawing one that
|
||||
/// holds for this box only, until `holds` says how far it goes.
|
||||
pub fn px_size(&mut self) -> PxVec2 {
|
||||
let px = self.px_of(self.region);
|
||||
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
|
||||
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) {
|
||||
if *own == Holds::ANY {
|
||||
*own = Holds::at(len);
|
||||
}
|
||||
}
|
||||
px
|
||||
self.px
|
||||
}
|
||||
|
||||
/// One axis of this widget's box in pixels. Prefer this to
|
||||
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
||||
pub fn px_len(&mut self, axis: Axis) -> Px {
|
||||
let len = self.px_of(self.region).axis(axis);
|
||||
let len = self.px.axis(axis);
|
||||
let own = &mut self.own[axis as usize];
|
||||
if *own == Holds::ANY {
|
||||
*own = Holds::at(len);
|
||||
@@ -436,7 +427,7 @@ impl<'a> Painter<'a> {
|
||||
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||
let holds = holds.into();
|
||||
debug_assert!(
|
||||
holds.contains(self.px_of(self.region).axis(axis)),
|
||||
holds.contains(self.px.axis(axis)),
|
||||
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
||||
self.label(),
|
||||
self.id
|
||||
@@ -575,29 +566,42 @@ pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: b
|
||||
reported.leftover != Weight::ZERO || declared.is_some() || decided
|
||||
}
|
||||
|
||||
/// The box a drawing occupies: the size the widget reported, on the side of
|
||||
/// the box it was asked in that its alignment says, on every axis that is
|
||||
/// not simply filled.
|
||||
/// What of the box it was given a widget's drawing occupies, as lengths of
|
||||
/// that box: the size it reported wherever that is a part to be placed, and
|
||||
/// the whole of the box wherever the answer fills it.
|
||||
///
|
||||
/// A reported fraction is a fraction of the box the widget drew in, where a
|
||||
/// declared one is a fraction of the box its parent handed down -- a span
|
||||
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
||||
/// fraction of. So this scales by the box rather than composing into it.
|
||||
pub(crate) fn placed_box(
|
||||
region: UiRegion,
|
||||
/// fraction of. So this is a length of the box rather than a length composed
|
||||
/// into it, and a box in pixels is this step from the given box's pixels.
|
||||
pub(crate) fn placed_lens(
|
||||
size: Size,
|
||||
align: RegionAlign,
|
||||
declared: [Option<LayoutLen>; 2],
|
||||
decided: [bool; 2],
|
||||
) -> UiRegion {
|
||||
let mut placed = region;
|
||||
) -> UiVec2 {
|
||||
let mut lens = UiVec2::FULL_SIZE;
|
||||
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
|
||||
let reported = size.axis(axis);
|
||||
if fills(reported, declared, decided) {
|
||||
if !fills(reported, declared, decided) {
|
||||
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
|
||||
}
|
||||
}
|
||||
lens
|
||||
}
|
||||
|
||||
/// Where that drawing sits: those lengths taken of the box the widget was
|
||||
/// asked in, on the side of it that the widget's alignment says.
|
||||
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
|
||||
let mut placed = region;
|
||||
for axis in AXES {
|
||||
// The whole of the box is already where it sits, and the arithmetic
|
||||
// below is the identity for it.
|
||||
if lens.axis(axis) == Len::FULL {
|
||||
continue;
|
||||
}
|
||||
let span = placed.axis_mut(axis);
|
||||
let len = span.len().scale(reported.rel) + Len::from_parts(Rel::ZERO, reported.px);
|
||||
let len = lens.axis(axis).within_len(span.len());
|
||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||
span.end = span.start + len;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user