Compare commits

...
Author SHA1 Message Date
iris-aiandClaude Fable 5.1 62a16b5608 Re-ask a dirty widget at its offer locally instead of deferring to its parent
A local redraw refused any widget whose given box was not as long as its
offer and marked its parent instead. Under the frame/extent protocol that
is nearly every widget beneath a self-sized container: a span hands its
children its own placement across itself, which is `FULL` while the span
is measured and its answer once it is placed, so the children's offer and
given frames differ on every such axis. A `many` frame at seed 13, depth
8 escalated 43 marks along chains up to seven levels and redrew 508 of
583 active widgets where e44dea3 redraws 159.

Retain the offer's frame beside the given one and ask the offer question
locally: the offer frame composed where the given one is, at the offer's
lengths and placement, then place at the given box where the two differ.
Seed 13 `many` goes from 4.73 ms to 1.28 ms against e44dea3's 0.90, and
294 distinct widgets a frame; size, scroll and repaint are unchanged.

Not sound yet: the suite, the debug oracle and the shrinker at 400 trees
of depth 5 pass, but the oracle at 1000 seeds of depth 6 diverges on seed
532 under reorder and seed 398 under every-size. Both reduce to a
self-sized container whose answer changes under a local redraw; the
reduced plans are in docs/HANDOFF.md of ai-app-2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 20:51:36 -04:00
iris-aiandClaude Opus 5 34cafb6edc Read the marks rather than the queue to decide the walk is done
Review of the two commits above. The queue was the walk's only record of
what was left, so a mark that reached `needs_redraw` without going through
`mark` -- an `on_undraw` handler is the reachable one -- would have waited
for the next frame. The set is read again once the queue drains, which is
what the scan it replaced did for free. `pop_last` takes the deepest entry
in one step rather than reading and then removing it.

The rest is comments: nine lines shorter, and the arm that takes an
ordinary ask said only what it does for a declared length.

Unchanged by all of it: 109 suite and 20 core tests, the four fuzzer runs
(100 seeds, 400 trees at depth 5, 1000 at depth 6, 2000 at depth 4), the
five reference renders and the resized `tabs`, and every counter on the
diagnostics rig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:31:29 -04:00
iris-aiandClaude Opus 5 3bf22935ce Take the deepest dirty widget from an ordered queue, not by scanning
The walk found the next widget to settle with `max_by_key` over the whole
`needs_redraw` set, and `depth` is a hash lookup, so a frame did a lookup
per marked widget per pop -- 131 depth reads for nine marks at seed 1
depth 8, 1,314 for 34, and 14,611 for 145. The set is scanned once now and
kept in a `BTreeSet` keyed by depth, and every mark made while the walk
runs goes through `mark`, which puts itself in place. The same three
counts become 57, 160 and 436.

Two things the scan gave for free are paid for explicitly: a widget that
was settled inside an ancestor's draw, or deferred to one, is dropped when
its entry comes up, and an entry whose widget has since changed depth --
a subtree that moved under a new parent -- is re-queued at the depth it now
has. What is drawn does not change: widget draws are identical at every
load measured.

Median frame at seed 1, depth 8: 0.955 -> 0.843 ms with 145 marks, 0.668 ->
0.666 with 34, and seed 13's default load 5.19 -> 4.86 ms. Ties between
equal depths now break by widget id rather than by hash order, which makes
the walk deterministic; nothing in the order within one depth was ever
relied on, since a widget at the same depth as another cannot contain it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:16:40 -04:00
iris-aiandClaude Opus 5 e6ba570d07 Give a child a part of the container's extent rather than its raw box
`Pad` and `Stack` read `Painter::placement` to put their children inside
their own drawing, and reading it is what says the drawing holds for that
placement alone. So a pad or a stack anywhere in a row was drawn again --
with its whole subtree -- the moment an earlier sibling changed length,
however little else had moved.

`widget_within` now takes a `DrawRegion`, and `DrawRegion::Extent(part)`
gives the child a part of the extent without reading it. What is retained
is the part rather than the box it resolved to, so moving the extent
re-places the child through the same rule instead of redrawing the parent:
`inherited_children` becomes `extent_children`, carrying `Inherit` for the
wrapper case `Painter::widget` already had and `Within(part)` for the new
one.

The dependency that goes up is a range on the container's extent rather
than on its frame, since only the part's *length* reaches the child and
where the part sits is re-placed. A declared length is unchanged: it is a
length of the frame wherever the box it sits in came from. What still pins
the placement is a report with a fraction in it -- the same fraction of a
different extent is a different length -- and that pin is on the answer,
which `extent_frames_keep_fractional_reports_and_numeric_dependencies_valid`
fails without.

Three tests from the first attempt at this come with it, and the
diagnostics rig now says which of the three contracts refused a reuse,
which is what found the above.

Measured, seed 1 at depth 8, median frame: `many` 0.667 -> 0.613 ms and
`resize` 48 -> 32 us; seed 13's `many` 6.35 -> 5.15 ms. Green: fmt, clippy,
109 suite and 20 core tests, the oracle at 100 seeds, the shrinker at 400
trees of depth 5, 1000 seeds at depth 6, and 2000 seeds at depth 4 over all
fifteen cases. The five reference renders are byte-identical to `0e107f0`
on Venus, as are `tabs` resized to 900x1200 and `random` to 1280x800
against cold renders there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:15:17 -04:00
iris-ai 0e107f0e89 Keep valid layout guarantees when a redraw widens their range 2026-09-17 17:24:53 -04:00
iris-ai f860f716e6 Separate measured-answer dependencies from retained drawing validity 2026-09-17 17:10:55 -04:00
iris-ai c44bd198ee Retain child frames relative to the container extent 2026-09-17 16:40:54 -04:00
iris-ai a7307d95fd Resolve a text draw's glyph origin once 2026-09-17 16:17:47 -04:00
9 changed files with 722 additions and 144 deletions

No files matched your search

+7 -1
View File
@@ -54,10 +54,13 @@ pub(crate) enum Counter {
TextShapes,
TextBreaks,
GlyphPlacements,
OutsidePlacement,
OutsideFrame,
OutsideExtent,
}
impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1;
const COUNT: usize = Self::OutsideExtent as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"updates",
@@ -89,6 +92,9 @@ impl Counter {
"text shapes",
"text line breaks",
"glyph placements",
"reuse outside: the placement it was pinned to",
"reuse outside: a frame length",
"reuse outside: an extent length",
];
}
+27 -1
View File
@@ -18,6 +18,9 @@ pub struct ActiveData {
/// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion,
/// The frame it was first asked in, in the same coordinates: the offer's
/// frame, which its parent's placing draw may since have narrowed.
pub offer_region: UiRegion,
/// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
@@ -41,7 +44,11 @@ pub struct ActiveData {
pub textures: Vec<TextureHandle>,
pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>,
pub inherited_children: Vec<WidgetId>,
/// The children whose box is a part of this widget's extent rather than
/// of its frame, and which part each was given. Moving the extent
/// re-places them through that part, so the drawing need not depend on
/// where it sits.
pub(crate) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
@@ -87,3 +94,22 @@ impl ActiveData {
})
}
}
/// What of a container's extent a child was given: the whole of it, for a
/// wrapper whose box is its child's, or a part of it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum ExtentPlacement {
Inherit,
Within(UiRegion),
}
impl ExtentPlacement {
/// The child's frame in the container's frame coordinates, and the slot
/// the container chose within it.
pub fn resolve(self, extent: UiRegion) -> (UiRegion, [Option<crate::UiSpan>; 2]) {
match self {
Self::Inherit => (UiRegion::FULL, [Some(extent.x), Some(extent.y)]),
Self::Within(part) => (part.within(&extent), [None; 2]),
}
}
}
+30
View File
@@ -15,6 +15,36 @@ impl LayoutHolds {
placement: None,
};
pub fn and(self, other: Self) -> Self {
debug_assert!(
self.placement.is_none()
|| other.placement.is_none()
|| self.placement == other.placement
);
Self {
frame: [
self.frame[0].and(other.frame[0]),
self.frame[1].and(other.frame[1]),
],
extent: [
self.extent[0].and(other.extent[0]),
self.extent[1].and(other.extent[1]),
],
placement: self.placement.or(other.placement),
}
}
pub fn covers(self, other: Self) -> bool {
self.placement
.is_none_or(|placement| other.placement == Some(placement))
&& [0, 1].into_iter().all(|n| {
self.frame[n].lo <= other.frame[n].lo
&& self.frame[n].hi >= other.frame[n].hi
&& self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi
})
}
pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool {
self.placement.is_none_or(|old| old == placement)
&& [Axis::X, Axis::Y].into_iter().all(|axis| {
+127 -42
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, DrawRegion, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText,
RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
Axis, DrawRegion, ExtentPlacement, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
@@ -40,9 +40,10 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<DrawRegion>,
pub(super) inherited_children: Vec<WidgetId>,
pub(super) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub(super) extent_own: [Holds; 2],
pub(super) extent_under: [Holds; 2],
/// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer.
@@ -59,8 +60,8 @@ pub struct Painter<'a> {
/// What this draw itself read of its box in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2],
/// What the children it asked about and drew keep it to.
pub(super) under: [Holds; 2],
/// Dependencies of every child drawing, including unmeasured overlays.
pub(super) under: LayoutHolds,
/// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx,
@@ -80,6 +81,21 @@ impl<'a> Painter<'a> {
/// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: DrawRegion) {
self.write_resolved(
kind,
primitive,
region,
region.resolve(self.region, self.placement),
);
}
fn write_resolved<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: DrawRegion,
resolved: UiRegion,
) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write(
@@ -88,7 +104,7 @@ impl<'a> Painter<'a> {
kind,
id: self.id,
primitive,
region: region.resolve(self.region, self.placement),
region: resolved,
mask_idx: self.mask,
move_idx: self.move_idx,
},
@@ -139,7 +155,13 @@ impl<'a> Painter<'a> {
/// around one child wants, since its box is the child's.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
let own = self.placement;
self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true, false)
self.widget_at_inner(
id,
UiRegion::FULL,
[Some(own.x), Some(own.y)],
Some(ExtentPlacement::Inherit),
false,
)
}
/// What a widget's rules declare its lengths to be, which whoever draws
@@ -155,20 +177,36 @@ impl<'a> Painter<'a> {
/// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.inherited_children.retain(|child| *child != id.id());
self.extent_children.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// Draws a widget somewhere within this one. `region` is in this widget's
/// own region, and is the child's own region: what its declared lengths
/// and its report are fractions of. Where its drawing sits inside that is
/// its own answer placed by its alignment.
/// Draws a child in `region`, relative to this widget's frame. The child
/// resolves declared lengths and reports against that frame, then places
/// its drawing by its own alignment.
///
/// `DrawRegion::Extent` gives a part of where this widget's drawing sits
/// instead, for a container whose children belong inside that rather than
/// inside the box it was offered. The part is what is kept, so moving the
/// extent re-places the child rather than drawing this widget again.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
region: impl Into<DrawRegion>,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, [None; 2])
match region.into() {
DrawRegion::Frame(region) => self.widget_at(id, region, [None; 2]),
DrawRegion::Extent(part) => {
let within = part.within(&self.placement);
self.widget_at_inner(
id,
within,
[None; 2],
Some(ExtentPlacement::Within(part)),
false,
)
}
}
}
/// Draws a widget in `region`, saying where in it the drawing goes.
@@ -191,7 +229,7 @@ impl<'a> Painter<'a> {
region: UiRegion,
placement: [Option<UiSpan>; 2],
) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, false, false)
self.widget_at_inner(id, region, placement, None, false)
}
fn widget_at_inner<'s, W: ?Sized>(
@@ -199,15 +237,12 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
inherited: bool,
extent: Option<ExtentPlacement>,
measuring: bool,
) -> DrawResult<'s, 'a, W> {
if inherited {
if !self.inherited_children.contains(&id.id()) {
self.inherited_children.push(id.id());
}
} else {
self.inherited_children.retain(|child| *child != id.id());
self.extent_children.retain(|(child, _)| *child != id.id());
if let Some(extent) = extent {
self.extent_children.push((id.id(), extent));
}
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
@@ -236,6 +271,14 @@ impl<'a> Painter<'a> {
.get(&id.id())
.map_or(given_len, |a| a.offer_len),
};
let offer_region = match first_ask {
true => local,
false => self
.state
.active
.get(&id.id())
.map_or(local, |a| a.offer_region),
};
let offer_placement = if first_ask {
placement
} else {
@@ -249,7 +292,7 @@ impl<'a> Painter<'a> {
// 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.
let (size, holds) = self.state.draw_inner(
let (size, answer_holds, holds) = self.state.draw_inner(
id.id(),
within,
DrawInfo {
@@ -260,6 +303,7 @@ impl<'a> Painter<'a> {
region_node,
mask: self.mask,
given_region: local,
offer_region,
offer_len,
offer_placement,
px,
@@ -270,28 +314,62 @@ impl<'a> Painter<'a> {
measuring,
self.rsc,
);
// Whatever the child's answer holds for keeps this one to the boxes
// that give the child a length inside it.
let in_parent = |holds: LayoutHolds| {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
let frame = holds.frame[n].through(local.axis(axis).len());
self.under[n] = self.under[n].and(frame);
if inherited && declared[n].is_none() {
self.extent_under[n] = self.extent_under[n].and(holds.extent[n]);
self.reads_placement |= holds.placement.is_some();
} else {
let extent = placement[n].unwrap_or(UiSpan::FULL).len();
self.under[n] = self.under[n].and(
let chosen = placement[n].unwrap_or(UiSpan::FULL).len();
match extent {
// Its box is this widget's own, so what its drawing holds
// for is what this widget's extent holds for.
Some(ExtentPlacement::Inherit) if declared[n].is_none() => {
result.frame[n] = holds.frame[n].through(local.axis(axis).len());
result.extent[n] = holds.extent[n];
if holds.placement.is_some() {
result.placement = Some(self.placement);
}
}
// Its box is a part of this widget's extent, so what it
// holds for is a range on that extent and none of it a
// range on the frame. Only the part's length reaches it,
// which is what lets the extent move without a redraw.
Some(ExtentPlacement::Within(part)) if declared[n].is_none() => {
result.extent[n] = holds.frame[n]
.and(holds.extent[n].through(chosen))
.through(part.axis(axis).len());
}
// Its box is a length of this widget's frame: an
// ordinary ask, or a declared length, which is that
// length wherever the box it sits in came from.
_ => {
result.frame[n] = holds.frame[n].through(local.axis(axis).len()).and(
holds.extent[n]
.through(extent)
.through(chosen)
.through(local.axis(axis).len()),
);
}
}
}
result
};
self.under = self.under.and(in_parent(holds));
let mut answer_holds = in_parent(answer_holds);
// What it reports is a fraction of the box it was given, which is a
// part of this widget's extent -- so the same fraction is a different
// length once that extent is, and pixels are not. The answer only:
// the drawing this holds is re-placed rather than made again.
if matches!(extent, Some(ExtentPlacement::Within(_)))
&& AXES.into_iter().any(|axis| {
declared[axis as usize].is_none() && size.axis(axis).rel != crate::Rel::ZERO
})
{
answer_holds.placement = Some(self.placement);
}
DrawResult {
child: id,
painter: self,
size: in_parent_frame(size, local.size(), declared),
answer_holds,
}
}
@@ -348,7 +426,7 @@ impl<'a> Painter<'a> {
.retained_size(child.id(), px, placement, self.move_idx, self.rsc.widgets());
let Some((size, holds)) = retained else {
return self
.widget_at_inner(child, region, offered, false, true)
.widget_at_inner(child, region, offered, None, true)
.len(axis);
};
#[cfg(feature = "layout-diagnostics")]
@@ -358,6 +436,7 @@ impl<'a> Painter<'a> {
self.offered.push(child.id());
let active = self.state.active.get_mut(&child.id()).unwrap();
active.offer_len = local.size();
active.offer_region = local;
active.offer_placement = placement;
}
let placement = UiRegion {
@@ -365,7 +444,7 @@ impl<'a> Painter<'a> {
y: placement[1].unwrap_or(UiSpan::FULL),
};
let holds = holds.in_frame(placement);
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
for (axis, under) in AXES.into_iter().zip(self.answer_under.frame.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
in_parent_frame(size, local.size(), declared).axis(axis)
@@ -404,9 +483,12 @@ impl<'a> Painter<'a> {
// TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into<DrawRegion>) {
let origin = origin.into();
// Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs the frame/extent composition.
let resolved = origin.resolve(self.region, self.placement);
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() {
let region = origin.map(|mut region| {
let place = |mut region: UiRegion| {
region.x.end = region.x.start;
region.y.end = region.y.start;
let mut region = region.offset(UiVec2::from_px(glyph.offset));
@@ -417,8 +499,8 @@ impl<'a> Painter<'a> {
region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y);
region
});
self.write(
};
self.write_resolved(
kind,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
@@ -427,7 +509,8 @@ impl<'a> Painter<'a> {
color: text.color,
flags: glyph.entry.flags(),
},
region,
origin.map(place),
place(resolved),
);
}
}
@@ -585,6 +668,7 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>,
size: Size,
answer_holds: LayoutHolds,
}
impl<W: ?Sized> DrawResult<'_, '_, W> {
@@ -595,6 +679,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on(self.child);
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
self.size
}
+143 -88
View File
@@ -24,6 +24,8 @@ pub(super) struct DrawInfo {
pub given_region: UiRegion,
/// The original offer's lengths relative to the parent's own offer.
pub offer_len: UiVec2,
/// The offer's frame in the parent widget's coordinates.
pub offer_region: UiRegion,
pub offer_placement: [Option<UiSpan>; 2],
/// This ask's box in pixels, and the offer's: one multiply from the
/// parent's own, which is where every pixel length in layout comes from.
@@ -66,16 +68,12 @@ pub struct UiRenderState {
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot.
slots: HashMap<WidgetId, MoveIdx>,
/// Answers invalidated by a declared-length change below them. These are
/// replaced even when retained placement means the redraw is not at the
/// old offer.
answer_invalid: crate::util::HashSet<WidgetId>,
/// Whether this frame contains a declared-length change, so any dirty
/// dependent replaces its answer too.
replace_answers: bool,
/// Widgets waiting for an ancestor to draw them, so the walk down the
/// depths does not pick one up again at its own depth.
deferred: crate::util::HashSet<WidgetId>,
/// What the walk has left to settle, deepest last. Ordered rather than
/// searched for, so finding the next one is not a pass over the marks.
pending: std::collections::BTreeSet<(usize, WidgetId)>,
pub moves: Moves,
}
@@ -87,9 +85,8 @@ impl UiRenderState {
output_size: PxVec2::ZERO,
old_root: None,
slots: Default::default(),
answer_invalid: Default::default(),
replace_answers: false,
deferred: Default::default(),
pending: Default::default(),
moves: Default::default(),
resized: false,
}
@@ -103,9 +100,8 @@ impl UiRenderState {
/// retained entry at all.
///
/// The root is the only widget a resize marks, and only where the new
/// output falls outside what its answer holds for: that range is the
/// intersection of everything under it, so admitting the new output says
/// the whole tree still stands. Where it does not, the ordinary walk
/// output invalidates its answer or its drawing. The latter includes
/// children whose size it never read. Where either fails, the ordinary walk
/// draws the root, and each widget's own range decides how far down the
/// new length reaches.
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
@@ -116,10 +112,10 @@ impl UiRenderState {
self.output_size = size;
self.resized = true;
let Some(root) = self.old_root else { return };
let stands = self
.active
.get(&root)
.is_some_and(|active| active.answers_at(active.given_region.size().to_px(size)));
let stands = self.active.get(&root).is_some_and(|active| {
let px = active.given_region.size().to_px(size);
active.answers_at(px) && active.holds.contains(px, active.placement)
});
if !stands {
widgets.needs_redraw.insert(root);
}
@@ -140,6 +136,7 @@ impl UiRenderState {
region_node: false,
mask: MaskIdx::NONE,
given_region: region,
offer_region: region,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px,
@@ -182,7 +179,6 @@ impl UiRenderState {
if rsc.widgets().has_updates() {
self.redraw_updates(rsc);
}
self.replace_answers = false;
self.free(rsc);
}
@@ -215,7 +211,7 @@ impl UiRenderState {
mut old: Option<ActiveData>,
measuring: bool,
rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) {
) -> (Size, LayoutHolds, LayoutHolds) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::DrawRequests);
@@ -226,8 +222,7 @@ impl UiRenderState {
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
let stale = rsc.widgets().needs_redraw.contains(&id);
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
let retained = match replace_answer || stale {
let retained = match stale {
true => None,
false => self
.retained_answer(id, info)
@@ -282,9 +277,10 @@ impl UiRenderState {
// same question again from these.
active.region = region;
active.given_region = info.given_region;
active.offer_region = info.offer_region;
active.offer_len = info.offer_len;
if info.placement == info.offer_placement && info.px == info.offered_px {
active.answer = Some(settled);
active.answer = Some(answer);
active.offer_placement = info.offer_placement;
}
active.decided = info.decided();
@@ -300,9 +296,9 @@ impl UiRenderState {
&& let Some(old_parent) = self.active.get_mut(&old_parent)
{
old_parent.children.retain(|child| *child != id);
old_parent.inherited_children.retain(|child| *child != id);
old_parent.extent_children.retain(|(child, _)| *child != id);
}
settled
(answer.0, answer.1, settled.1)
}
/// Recompose retained geometry when the evaluation still holds at this extent.
@@ -373,16 +369,16 @@ impl UiRenderState {
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
extent_children: Vec::new(),
children: Vec::new(),
offered: Vec::new(),
offered_px: info.offered_px,
at_offer,
size_deps: Vec::new(),
own: [Holds::ANY; 2],
under: [Holds::ANY; 2],
under: LayoutHolds::ANY,
extent_own: [Holds::ANY; 2],
extent_under: [Holds::ANY; 2],
answer_under: LayoutHolds::ANY,
depth: info.depth,
move_idx,
rsc,
@@ -410,9 +406,9 @@ impl UiRenderState {
textures,
primitives,
mask_region,
inherited_children,
extent_children,
extent_own,
extent_under,
answer_under,
children,
offered: _,
offered_px: _,
@@ -450,14 +446,13 @@ impl UiRenderState {
"'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id),
);
let holds = LayoutHolds {
frame: [own[0].and(under[0]), own[1].and(under[1])],
extent: [
extent_own[0].and(extent_under[0]),
extent_own[1].and(extent_under[1]),
],
let own_holds = LayoutHolds {
frame: own,
extent: extent_own,
placement: reads_placement.then_some(placement),
};
let answer_holds = own_holds.and(answer_under);
let holds = answer_holds.and(under);
debug_assert!(
holds.contains(px, placement),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
@@ -487,6 +482,7 @@ impl UiRenderState {
region_node: false,
mask,
given_region: UiRegion::FULL,
offer_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px,
@@ -504,6 +500,7 @@ impl UiRenderState {
region,
placement,
given_region: info.given_region,
offer_region: info.offer_region,
offer_len: info.offer_len,
offer_placement: info.offer_placement,
// Whoever asked writes the answer, if this was the asking.
@@ -516,7 +513,7 @@ impl UiRenderState {
textures,
primitives,
mask_region,
inherited_children,
extent_children,
children,
size_deps,
declared: declared_lens(rsc.widgets(), id),
@@ -530,7 +527,7 @@ impl UiRenderState {
};
rsc.on_draw(&active);
self.active.insert(id, active);
(size, holds)
(size, answer_holds)
}
/// Keeps a region node's entry across redraws because descendants retain
@@ -693,6 +690,24 @@ impl UiRenderState {
if !active.holds.contains(info.px, placement) {
#[cfg(feature = "layout-diagnostics")]
{
// Which of the three said no, so a frame 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;
if holds.placement.is_some_and(|pinned| pinned != placement) {
diag::bump(Counter::OutsidePlacement);
}
for axis in AXES {
let n = axis as usize;
if !holds.frame[n].contains(info.px.axis(axis)) {
diag::bump(Counter::OutsideFrame);
}
if !holds.extent[n]
.contains(placement.axis(axis).len().to_px(info.px.axis(axis)))
{
diag::bump(Counter::OutsideExtent);
}
}
diag::bump(Counter::ReuseOutside);
diag::reuse(id, ReuseOutcome::Outside);
}
@@ -715,6 +730,7 @@ impl UiRenderState {
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.given_region = info.given_region;
active.offer_region = info.offer_region;
active.offer_len = info.offer_len;
#[cfg(feature = "layout-diagnostics")]
{
@@ -765,19 +781,18 @@ impl UiRenderState {
}
let parent_move = active.move_idx;
let mask = active.mask;
let children = active.inherited_children.len();
let children = active.extent_children.len();
for index in 0..children {
let child = self.active[&id].inherited_children[index];
let (child, extent) = self.active[&id].extent_children[index];
let (part, slot) = extent.resolve(placement);
let active = &self.active[&child];
let (child_local, chosen) = ask_box(
UiRegion::FULL,
active.declared,
active.own_align,
[Some(placement.x), Some(placement.y)],
);
let (child_local, chosen) = ask_box(part, active.declared, active.own_align, slot);
// What it took of that box is its own answer, which this move did
// not ask again: keep the placement it has on any axis this
// widget is not the one choosing.
let child_placement = UiRegion {
x: chosen[0].unwrap_or(UiSpan::FULL),
y: chosen[1].unwrap_or(UiSpan::FULL),
x: chosen[0].unwrap_or(active.placement.x),
y: chosen[1].unwrap_or(active.placement.y),
};
let child_info = DrawInfo {
layer: active.layer,
@@ -787,6 +802,7 @@ impl UiRenderState {
region_node: active.move_idx != active.parent_move,
mask,
given_region: child_local,
offer_region: active.offer_region,
offer_len: active.offer_len,
offer_placement: active.offer_placement,
px: child_local.size().to_px(info.px),
@@ -926,6 +942,7 @@ impl UiRenderState {
region: UiRegion::FULL,
placement: UiRegion::FULL,
given_region: UiRegion::FULL,
offer_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
answer: None,
@@ -937,7 +954,7 @@ impl UiRenderState {
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
extent_children: Vec::new(),
children: Vec::new(),
size_deps: Vec::new(),
move_idx: info.parent_move,
@@ -959,8 +976,6 @@ impl UiRenderState {
}
}
self.slots.clear();
self.answer_invalid.clear();
self.replace_answers = false;
self.moves.clear();
self.layers.clear();
rsc.widgets_mut().needs_redraw.clear();
@@ -974,7 +989,6 @@ impl UiRenderState {
rsc.on_remove(id);
self.remove(id, true, rsc);
self.drop_slot(id);
self.answer_invalid.remove(&id);
}
rsc.ui_mut().textures.free();
}
@@ -993,24 +1007,52 @@ impl UiRenderState {
// something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved.
// The queue is that set, ordered: a mark made while the walk runs
// queues itself through `mark`. What ends the walk is still the set
// being spent, not the queue, so a mark that reached it another way
// cannot be left for the next frame.
loop {
let next = rsc
.widgets()
.needs_redraw
.iter()
.copied()
.filter(|id| !self.deferred.contains(id))
.max_by_key(|&id| self.depth(id));
let Some(id) = next else { break };
for &id in rsc.widgets().needs_redraw.iter() {
if !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
}
}
if self.pending.is_empty() {
break;
}
while let Some((depth, id)) = self.pending.pop_last() {
// Settled inside an ancestor's draw, or deferred to one,
// since the mark that queued it.
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
continue;
}
// A subtree that changed hands takes its descendants' depths
// with it, so an entry queued before that move names the
// depth it had under the parent it left.
let now = self.depth(id);
if now != depth {
self.pending.insert((now, id));
continue;
}
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops);
if !self.redraw(id, rsc) {
self.deferred.insert(id);
}
}
}
self.deferred.clear();
}
/// Marks a widget for the walk to settle, and queues it at its depth.
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
}
}
fn depth(&self, id: WidgetId) -> usize {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::DepthReads);
@@ -1111,20 +1153,11 @@ impl UiRenderState {
if let Some(parent) = active.parent
&& (declared_changed || alignment_changed || !active.drawn || active.answer.is_none())
{
if declared_changed {
self.replace_answers = true;
let mut at = Some(id);
while let Some(next) = at {
self.answer_invalid.insert(next);
rsc.widgets_mut().needs_redraw.insert(next);
at = self.active[&next].parent;
}
}
// Both stay marked: the parent because it has this to draw, and
// this because the parent must draw it rather than keep what it
// has. The mark comes off in `draw_at`, where the parent draws.
rsc.widgets_mut().needs_redraw.insert(id);
rsc.widgets_mut().needs_redraw.insert(parent);
self.mark(id, rsc.widgets_mut());
self.mark(parent, rsc.widgets_mut());
return false;
}
if !active.drawn {
@@ -1146,16 +1179,6 @@ impl UiRenderState {
return true;
};
let (given_px, offered_px) = self.asked_px(id);
// Asked again in the box its parent gave it, which is the question
// its parent asked only while that box is as long as the offer. Any
// other box is a different question, so the parent asks it, with the
// mark left on. Lengths and not whole boxes: what a drawing depends
// on is its lengths, so the same lengths elsewhere is one question.
if given_px != offered_px {
rsc.widgets_mut().needs_redraw.insert(id);
rsc.widgets_mut().needs_redraw.insert(parent);
return false;
}
let info = DrawInfo {
layer: active.layer,
parent: active.parent,
@@ -1164,6 +1187,7 @@ impl UiRenderState {
region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask,
given_region: active.given_region,
offer_region: active.offer_region,
offer_len: active.offer_len,
offer_placement: active.offer_placement,
px: given_px,
@@ -1173,30 +1197,61 @@ impl UiRenderState {
placement: AXES
.map(|axis| active.decided[axis as usize].then(|| *active.placement.axis(axis))),
};
let (given, was_answer) = (active.region, active.answer);
let (given, was_answer, was_holds) = (active.region, active.answer, active.holds);
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
// Refresh the original measurement before restoring the assigned slot.
// Its lengths may differ even though the fraction reference is unchanged.
// Asked again where its parent asked: the offer's frame, composed
// where the given one is, at the offer's lengths and placement. That
// is the question its answer came from, whatever box the parent then
// chose from the answer -- which is often a different frame, since a
// span hands its children its own placement across itself. The
// parent draws in its own frame, or in `FULL` where it is a region
// node.
let parent_frame = match self.active.get(&parent) {
Some(p) if p.move_idx == p.parent_move => p.region,
_ => UiRegion::FULL,
};
let offer_frame = match info.offer_region == UiRegion::FULL {
true => parent_frame,
false => info.offer_region.within(&parent_frame),
};
let offered = DrawInfo {
placement: info.offer_placement,
given_region: info.offer_region,
px: offered_px,
..info
};
let answer = self.draw_inner(id, given, offered, old, false, rsc);
if info.placement != offered.placement {
// Where the given differs from the offer, the first draw is only the
// measurement and the second puts the drawing where the parent did.
let placed_apart =
info.placement != offered.placement || info.px != offered.px || given != offer_frame;
let answer = self.draw_inner(id, offer_frame, offered, old, placed_apart, rsc);
if placed_apart {
self.draw_inner(id, given, info, None, false, rsc);
}
if Some(answer) != was_answer {
// Its parent chose its box knowing the old answer, so it lays out
// again and chooses the box the new one asks for.
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)
{
active.answer = was_answer;
}
if active.holds.covers(was_holds) && was_holds.contains(given_px, active.placement) {
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);
}
rsc.widgets_mut().needs_redraw.insert(parent);
self.mark(parent, rsc.widgets_mut());
}
true
}
+1 -1
View File
@@ -1,4 +1,4 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SlotId {
idx: u32,
genr: u32,
+1 -1
View File
@@ -13,7 +13,7 @@ impl Widget for Pad {
// it; where the box is bigger -- a share of a row, a rule over this
// widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for.
let inside = self.padding.region_of(painter.placement());
let inside = DrawRegion::Extent(self.padding.region());
let inner = painter.widget_within(&self.inner, inside).size();
Size {
x: LayoutLen {
+2 -10
View File
@@ -13,8 +13,6 @@ impl Widget for Stack {
StackSize::Default => None,
StackSize::Child(i) => Some(i),
};
// This stack's own box, which is `FULL` until its answer is known.
let placement = painter.placement();
// Whichever child sizes the stack keeps the stack's whole region as
// its own -- the stack is the length that child asked for, so taking
// the fraction of the stack's box again would take it twice -- and is
@@ -25,13 +23,7 @@ impl Widget for Stack {
// drawing belongs to the layer it was made on.
Some((i, child)) => {
painter.child_layer_at(i);
painter
.widget_at(
child,
UiRegion::FULL,
[Some(placement.x), Some(placement.y)],
)
.size()
painter.widget(child).size()
}
None => Size::LEFTOVER,
};
@@ -43,7 +35,7 @@ impl Widget for Stack {
// Every other child has the stack's own box for its region, since
// the stack is what contains it, and where it sits in one bigger
// than itself is its own business.
painter.widget_within(child, placement);
painter.widget_within(child, DrawRegion::Extent(UiRegion::FULL));
}
size
}
+384
View File
@@ -951,3 +951,387 @@ fn a_span_does_not_place_its_measurement_before_assigning_the_childs_slot() {
vec![h.region(&leaf.id()).unwrap()]
);
}
#[test]
fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
struct Glyphs {
buffer: TextBuffer,
draws: Rc<Cell<usize>>,
}
impl Widget for Glyphs {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
let text = painter.render_text(&mut self.buffer, &TextAttrs::default(), None);
let origin = UiRegion::new(
UiSpan::new(Len::rel(0.23) + Len::px(-7.125), Len::FULL),
UiSpan::new(Len::rel(0.37) + Len::px(3.25), Len::FULL),
);
painter.glyphs(text, DrawRegion::Frame(origin));
painter.glyphs(text, DrawRegion::Extent(origin));
Size::LEFTOVER
}
}
struct Frame {
child: StrongWidget,
region: UiRegion,
extent: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
self.region,
[Some(self.extent.x), Some(self.extent.y)],
);
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((403, 211));
let draws = Rc::new(Cell::new(0));
let text = Glyphs {
buffer: TextBuffer::new("Glyphs: gj AV\nsecond line"),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(text, node);
let root = Frame {
child: text.add_strong(&mut h.rsc),
region: UiRegion::FULL,
extent: UiRegion::FULL,
}
.add(&mut h.rsc);
h.set_root(root);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let before = draws.get();
h.rsc[root].region.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
h.rsc[root].extent = UiRegion::new(
UiSpan::new(Len::rel(start), Len::rel(end)),
UiSpan::new(Len::px(7.25), Len::rel(end)),
);
h.frame();
assert_eq!(draws.get(), before);
let retained = primitive_bounds(&h, text.id());
assert!(!retained.is_empty());
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
h.frame();
assert!(draws.get() > before);
assert_eq!(retained, primitive_bounds(&h, text.id()));
}
}
}
#[test]
fn resizing_does_not_remeasure_a_fixed_stack_for_its_unmeasured_overlay() {
let mut h = Harness::new((400, 200));
let (sizing, _) = counted(&mut h, Size::from((100, 80)), false);
let (overlay, draws) = counted(&mut h, Size::LEFTOVER, true);
h.set_root((sizing, overlay).stack().size(StackSize::Child(0)));
let settled = draws.get();
h.resize((800, 300));
h.frame();
assert_eq!(draws.get(), settled);
assert_corners!(h, overlay, (350, 110), (450, 190));
}
struct Unmeasured {
child: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Unmeasured {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.child);
Size::LEFTOVER
}
}
#[test]
fn a_declared_size_change_stops_at_an_independent_parent() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::RED).width(100).add(&mut h.rsc);
let parent = Unmeasured {
child: leaf.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add_strong(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
h.set_root(Unmeasured {
child: parent,
draws: draws.clone(),
});
let settled = draws.get();
h.set_len(leaf, Axis::X, 150);
h.frame();
assert_corners!(h, leaf, (125, 0), (275, 200));
assert_eq!(draws.get(), settled);
}
#[test]
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((leaf,).stack());
let settled = draws.get();
h.resize((800, 200));
h.frame();
assert!(draws.get() > settled);
assert_corners!(h, leaf, (300, 90), (500, 110));
}
#[test]
fn changed_drawing_dependencies_reach_ancestors_without_a_size_change() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
h.set_root(((leaf,).stack(),).stack());
h.rsc[leaf].reads_box = true;
h.frame();
let settled = draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(draws.get(), settled + 1);
assert_corners!(h, leaf, (0, 0), (800, 200));
}
#[test]
fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
let mut h = Harness::new((400, 200));
let (leaf, leaf_draws) = counted(&mut h, Size::LEFTOVER, true);
let draws = Rc::new(Cell::new(0));
let child = leaf.add_strong(&mut h.rsc);
h.set_root(Unmeasured {
child,
draws: draws.clone(),
});
let settled = draws.get();
for reads_box in [false, true, false, true] {
h.rsc[leaf].reads_box = reads_box;
h.frame();
assert_eq!(draws.get(), settled);
}
let settled = leaf_draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(leaf_draws.get(), settled + 1);
}
#[test]
fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
struct Observed<W> {
widget: W,
draws: Rc<Cell<usize>>,
}
impl<W: Widget> Widget for Observed<W> {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.widget.draw(painter)
}
}
struct Frame {
child: StrongWidget,
extent: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
);
Size::LEFTOVER
}
}
for node in [false, true] {
let plant = |h: &mut Harness, extent| {
let draws = Rc::new(Cell::new(0));
let leaf = rect(Color::BLUE).masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node);
let fixed = rect(Color::RED).width(31).height(19).add(&mut h.rsc);
let stack = Observed {
widget: Stack {
children: vec![leaf.add_strong(&mut h.rsc), fixed.add_strong(&mut h.rsc)],
size: StackSize::Default,
},
draws: draws.clone(),
}
.add_strong(&mut h.rsc);
let pad = Observed {
widget: Pad {
inner: stack,
padding: Padding::uniform(7).with_left(13),
},
draws: draws.clone(),
}
.add_strong(&mut h.rsc);
let root = Frame { child: pad, extent }.add(&mut h.rsc);
h.set_root(root);
(root, leaf, fixed, draws)
};
let mut warm = Harness::new((403, 211));
let (root, leaf, fixed, draws) = plant(&mut warm, UiRegion::FULL);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let extent = UiRegion::new(
UiSpan::new(Len::rel(start) + Len::px(3.125), Len::rel(end)),
UiSpan::new(Len::px(11.25), Len::rel(end)),
);
let before = draws.get();
warm.rsc[root].extent = extent;
warm.frame();
assert_eq!(draws.get(), before);
let mut cold = Harness::new((403, 211));
let (_, other, other_fixed, _) = plant(&mut cold, extent);
for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] {
assert_eq!(warm.region(&a), cold.region(&b));
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
}
let mask = |h: &Harness, id: WidgetId| {
let active = &h.render.active[&id];
let mask = &h.rsc.ui().masks[active.mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
}
}
}
#[test]
fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
struct Measured;
impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size {
let width = painter.px_len(Axis::X);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
}
}
struct Frame {
child: StrongWidget,
start: f32,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[
Some(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
)),
Some(UiSpan::FULL),
],
);
Size::LEFTOVER
}
}
let mut h = Harness::new((400, 200));
let leaf = Measured.add(&mut h.rsc);
let stack = (leaf,).stack().add_strong(&mut h.rsc);
let root = Frame {
child: stack,
start: 0.0,
}
.add(&mut h.rsc);
h.set_root(root);
assert_corners!(h, leaf, (60, 80), (140, 120));
h.rsc[root].start = 30.0;
h.frame();
assert_corners!(h, leaf, (90, 80), (170, 120));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf).unwrap()]
);
}
#[test]
fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
struct Container {
child: StrongWidget,
region: UiRegion,
}
impl Widget for Container {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter
.widget_within(&self.child, DrawRegion::Extent(self.region))
.size()
}
}
struct Frame {
child: StrongWidget,
extent: UiRegion,
answer: Rc<Cell<Size>>,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.answer.set(
painter
.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
)
.size(),
);
Size::LEFTOVER
}
}
for fractional in [false, true] {
for region in [
UiRegion::FULL,
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
] {
let plant = |h: &mut Harness, extent| {
let size = if fractional {
Size {
x: rel(0.5),
y: LayoutLen::px(27),
}
} else {
Size::from((80, 27))
};
let (leaf, _) = counted(h, size, !fractional);
let child = Container {
child: leaf.add_strong(&mut h.rsc),
region,
}
.add_strong(&mut h.rsc);
let answer = Rc::new(Cell::new(Size::ZERO));
let root = Frame {
child,
extent,
answer: answer.clone(),
}
.add(&mut h.rsc);
h.set_root(root);
(root, leaf, answer)
};
let mut warm = Harness::new((403, 211));
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
for width in [191.125, 297.25, 83.75] {
let extent =
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
warm.rsc[root].extent = extent;
warm.frame();
let mut cold = Harness::new((403, 211));
let (_, other, other_answer) = plant(&mut cold, extent);
assert_eq!(answer.get(), other_answer.get());
assert_eq!(warm.region(&leaf), cold.region(&other));
}
}
}
}