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, TextShapes,
TextBreaks, TextBreaks,
GlyphPlacements, GlyphPlacements,
OutsidePlacement,
OutsideFrame,
OutsideExtent,
} }
impl Counter { impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1; const COUNT: usize = Self::OutsideExtent as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"updates", "updates",
@@ -89,6 +92,9 @@ impl Counter {
"text shapes", "text shapes",
"text line breaks", "text line breaks",
"glyph placements", "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 /// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain. /// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion, 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 /// 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 /// 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 /// was given was decided knowing its answer, so this is the question
@@ -41,7 +44,11 @@ pub struct ActiveData {
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<RetainedPrimitive>, pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>, 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>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>, 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, 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 { pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool {
self.placement.is_none_or(|old| old == placement) self.placement.is_none_or(|old| old == placement)
&& [Axis::X, Axis::Y].into_iter().all(|axis| { && [Axis::X, Axis::Y].into_iter().all(|axis| {
+127 -42
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, DrawRegion, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Axis, DrawRegion, ExtentPlacement, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign,
RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive, TexturePrimitive,
@@ -40,9 +40,10 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<DrawRegion>, 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_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>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked in /// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer. /// 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 /// 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. /// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2], pub(super) own: [Holds; 2],
/// What the children it asked about and drew keep it to. /// Dependencies of every child drawing, including unmeasured overlays.
pub(super) under: [Holds; 2], pub(super) under: LayoutHolds,
/// The movable region this widget's primitives are positioned through: /// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's. /// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx, pub(super) move_idx: MoveIdx,
@@ -80,6 +81,21 @@ impl<'a> Painter<'a> {
/// Takes the kind, for a caller writing many of one primitive. /// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: DrawRegion) { 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")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites); diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
@@ -88,7 +104,7 @@ impl<'a> Painter<'a> {
kind, kind,
id: self.id, id: self.id,
primitive, primitive,
region: region.resolve(self.region, self.placement), region: resolved,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx, move_idx: self.move_idx,
}, },
@@ -139,7 +155,13 @@ impl<'a> Painter<'a> {
/// around one child wants, since its box is the child's. /// 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> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
let own = self.placement; 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 /// 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. /// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) { pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id()); 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); self.state.undraw_rec(id.id(), self.rsc);
} }
/// Draws a widget somewhere within this one. `region` is in this widget's /// Draws a child in `region`, relative to this widget's frame. The child
/// own region, and is the child's own region: what its declared lengths /// resolves declared lengths and reports against that frame, then places
/// and its report are fractions of. Where its drawing sits inside that is /// its drawing by its own alignment.
/// its own answer placed by its 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>( pub fn widget_within<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: impl Into<DrawRegion>,
) -> DrawResult<'s, 'a, W> { ) -> 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. /// Draws a widget in `region`, saying where in it the drawing goes.
@@ -191,7 +229,7 @@ impl<'a> Painter<'a> {
region: UiRegion, region: UiRegion,
placement: [Option<UiSpan>; 2], placement: [Option<UiSpan>; 2],
) -> DrawResult<'s, 'a, W> { ) -> 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>( fn widget_at_inner<'s, W: ?Sized>(
@@ -199,15 +237,12 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
placement: [Option<UiSpan>; 2], placement: [Option<UiSpan>; 2],
inherited: bool, extent: Option<ExtentPlacement>,
measuring: bool, measuring: bool,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
if inherited { self.extent_children.retain(|(child, _)| *child != id.id());
if !self.inherited_children.contains(&id.id()) { if let Some(extent) = extent {
self.inherited_children.push(id.id()); self.extent_children.push((id.id(), extent));
}
} else {
self.inherited_children.retain(|child| *child != id.id());
} }
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
@@ -236,6 +271,14 @@ impl<'a> Painter<'a> {
.get(&id.id()) .get(&id.id())
.map_or(given_len, |a| a.offer_len), .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 { let offer_placement = if first_ask {
placement placement
} else { } else {
@@ -249,7 +292,7 @@ impl<'a> Painter<'a> {
// The answer and what it holds for, both about the box asked in. The // 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 // 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. // 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(), id.id(),
within, within,
DrawInfo { DrawInfo {
@@ -260,6 +303,7 @@ impl<'a> Painter<'a> {
region_node, region_node,
mask: self.mask, mask: self.mask,
given_region: local, given_region: local,
offer_region,
offer_len, offer_len,
offer_placement, offer_placement,
px, px,
@@ -270,28 +314,62 @@ impl<'a> Painter<'a> {
measuring, measuring,
self.rsc, self.rsc,
); );
// Whatever the child's answer holds for keeps this one to the boxes let in_parent = |holds: LayoutHolds| {
// that give the child a length inside it. let mut result = LayoutHolds::ANY;
for axis in AXES { for axis in AXES {
let n = axis as usize; let n = axis as usize;
let frame = holds.frame[n].through(local.axis(axis).len()); let chosen = placement[n].unwrap_or(UiSpan::FULL).len();
self.under[n] = self.under[n].and(frame); match extent {
if inherited && declared[n].is_none() { // Its box is this widget's own, so what its drawing holds
self.extent_under[n] = self.extent_under[n].and(holds.extent[n]); // for is what this widget's extent holds for.
self.reads_placement |= holds.placement.is_some(); Some(ExtentPlacement::Inherit) if declared[n].is_none() => {
} else { result.frame[n] = holds.frame[n].through(local.axis(axis).len());
let extent = placement[n].unwrap_or(UiSpan::FULL).len(); result.extent[n] = holds.extent[n];
self.under[n] = self.under[n].and( 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] holds.extent[n]
.through(extent) .through(chosen)
.through(local.axis(axis).len()), .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 { DrawResult {
child: id, child: id,
painter: self, painter: self,
size: in_parent_frame(size, local.size(), declared), 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()); .retained_size(child.id(), px, placement, self.move_idx, self.rsc.widgets());
let Some((size, holds)) = retained else { let Some((size, holds)) = retained else {
return self return self
.widget_at_inner(child, region, offered, false, true) .widget_at_inner(child, region, offered, None, true)
.len(axis); .len(axis);
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -358,6 +436,7 @@ impl<'a> Painter<'a> {
self.offered.push(child.id()); self.offered.push(child.id());
let active = self.state.active.get_mut(&child.id()).unwrap(); let active = self.state.active.get_mut(&child.id()).unwrap();
active.offer_len = local.size(); active.offer_len = local.size();
active.offer_region = local;
active.offer_placement = placement; active.offer_placement = placement;
} }
let placement = UiRegion { let placement = UiRegion {
@@ -365,7 +444,7 @@ impl<'a> Painter<'a> {
y: placement[1].unwrap_or(UiSpan::FULL), y: placement[1].unwrap_or(UiSpan::FULL),
}; };
let holds = holds.in_frame(placement); 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())); *under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
in_parent_frame(size, local.size(), declared).axis(axis) 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. // TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into<DrawRegion>) { pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into<DrawRegion>) {
let origin = origin.into(); 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>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let region = origin.map(|mut region| { let place = |mut region: UiRegion| {
region.x.end = region.x.start; region.x.end = region.x.start;
region.y.end = region.y.start; region.y.end = region.y.start;
let mut region = region.offset(UiVec2::from_px(glyph.offset)); 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.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y); region.y.end = region.y.start.offset(size.y);
region region
}); };
self.write( self.write_resolved(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
uv_min: glyph.entry.uv_min, uv_min: glyph.entry.uv_min,
@@ -427,7 +509,8 @@ impl<'a> Painter<'a> {
color: text.color, color: text.color,
flags: glyph.entry.flags(), 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>, painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>, child: &'p StrongWidget<W>,
size: Size, size: Size,
answer_holds: LayoutHolds,
} }
impl<W: ?Sized> DrawResult<'_, '_, W> { 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); diag::size_read(self.child.id(), self.painter.id, self.size);
} }
self.painter.depend_on(self.child); self.painter.depend_on(self.child);
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
self.size self.size
} }
+143 -88
View File
@@ -24,6 +24,8 @@ pub(super) struct DrawInfo {
pub given_region: UiRegion, pub given_region: UiRegion,
/// The original offer's lengths relative to the parent's own offer. /// The original offer's lengths relative to the parent's own offer.
pub offer_len: UiVec2, pub offer_len: UiVec2,
/// The offer's frame in the parent widget's coordinates.
pub offer_region: UiRegion,
pub offer_placement: [Option<UiSpan>; 2], pub offer_placement: [Option<UiSpan>; 2],
/// This ask's box in pixels, and the offer's: one multiply from the /// 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. /// 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 /// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot. /// replaces that while its children go on pointing at the slot.
slots: HashMap<WidgetId, MoveIdx>, 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 /// Widgets waiting for an ancestor to draw them, so the walk down the
/// depths does not pick one up again at its own depth. /// depths does not pick one up again at its own depth.
deferred: crate::util::HashSet<WidgetId>, 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, pub moves: Moves,
} }
@@ -87,9 +85,8 @@ impl UiRenderState {
output_size: PxVec2::ZERO, output_size: PxVec2::ZERO,
old_root: None, old_root: None,
slots: Default::default(), slots: Default::default(),
answer_invalid: Default::default(),
replace_answers: false,
deferred: Default::default(), deferred: Default::default(),
pending: Default::default(),
moves: Default::default(), moves: Default::default(),
resized: false, resized: false,
} }
@@ -103,9 +100,8 @@ impl UiRenderState {
/// retained entry at all. /// retained entry at all.
/// ///
/// The root is the only widget a resize marks, and only where the new /// 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 /// output invalidates its answer or its drawing. The latter includes
/// intersection of everything under it, so admitting the new output says /// children whose size it never read. Where either fails, the ordinary walk
/// the whole tree still stands. Where it does not, the ordinary walk
/// draws the root, and each widget's own range decides how far down the /// draws the root, and each widget's own range decides how far down the
/// new length reaches. /// new length reaches.
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) { pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
@@ -116,10 +112,10 @@ impl UiRenderState {
self.output_size = size; self.output_size = size;
self.resized = true; self.resized = true;
let Some(root) = self.old_root else { return }; let Some(root) = self.old_root else { return };
let stands = self let stands = self.active.get(&root).is_some_and(|active| {
.active let px = active.given_region.size().to_px(size);
.get(&root) active.answers_at(px) && active.holds.contains(px, active.placement)
.is_some_and(|active| active.answers_at(active.given_region.size().to_px(size))); });
if !stands { if !stands {
widgets.needs_redraw.insert(root); widgets.needs_redraw.insert(root);
} }
@@ -140,6 +136,7 @@ impl UiRenderState {
region_node: false, region_node: false,
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
given_region: region, given_region: region,
offer_region: region,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2], offer_placement: [None; 2],
px, px,
@@ -182,7 +179,6 @@ impl UiRenderState {
if rsc.widgets().has_updates() { if rsc.widgets().has_updates() {
self.redraw_updates(rsc); self.redraw_updates(rsc);
} }
self.replace_answers = false;
self.free(rsc); self.free(rsc);
} }
@@ -215,7 +211,7 @@ impl UiRenderState {
mut old: Option<ActiveData>, mut old: Option<ActiveData>,
measuring: bool, measuring: bool,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) { ) -> (Size, LayoutHolds, LayoutHolds) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::DrawRequests); diag::bump(Counter::DrawRequests);
@@ -226,8 +222,7 @@ impl UiRenderState {
// one bottom-up walk, so anything deeper has settled or deferred to // one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked. // its own parent, and a deferred one leaves that parent marked.
let stale = rsc.widgets().needs_redraw.contains(&id); let stale = rsc.widgets().needs_redraw.contains(&id);
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale); let retained = match stale {
let retained = match replace_answer || stale {
true => None, true => None,
false => self false => self
.retained_answer(id, info) .retained_answer(id, info)
@@ -282,9 +277,10 @@ impl UiRenderState {
// same question again from these. // same question again from these.
active.region = region; active.region = region;
active.given_region = info.given_region; active.given_region = info.given_region;
active.offer_region = info.offer_region;
active.offer_len = info.offer_len; active.offer_len = info.offer_len;
if info.placement == info.offer_placement && info.px == info.offered_px { 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.offer_placement = info.offer_placement;
} }
active.decided = info.decided(); active.decided = info.decided();
@@ -300,9 +296,9 @@ impl UiRenderState {
&& let Some(old_parent) = self.active.get_mut(&old_parent) && let Some(old_parent) = self.active.get_mut(&old_parent)
{ {
old_parent.children.retain(|child| *child != id); 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. /// Recompose retained geometry when the evaluation still holds at this extent.
@@ -373,16 +369,16 @@ impl UiRenderState {
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None, mask_region: None,
inherited_children: Vec::new(), extent_children: Vec::new(),
children: Vec::new(), children: Vec::new(),
offered: Vec::new(), offered: Vec::new(),
offered_px: info.offered_px, offered_px: info.offered_px,
at_offer, at_offer,
size_deps: Vec::new(), size_deps: Vec::new(),
own: [Holds::ANY; 2], own: [Holds::ANY; 2],
under: [Holds::ANY; 2], under: LayoutHolds::ANY,
extent_own: [Holds::ANY; 2], extent_own: [Holds::ANY; 2],
extent_under: [Holds::ANY; 2], answer_under: LayoutHolds::ANY,
depth: info.depth, depth: info.depth,
move_idx, move_idx,
rsc, rsc,
@@ -410,9 +406,9 @@ impl UiRenderState {
textures, textures,
primitives, primitives,
mask_region, mask_region,
inherited_children, extent_children,
extent_own, extent_own,
extent_under, answer_under,
children, children,
offered: _, offered: _,
offered_px: _, offered_px: _,
@@ -450,14 +446,13 @@ impl UiRenderState {
"'{}' ({id:?}) clips to {px:?} and reports {size}", "'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id), rsc.widgets().label(id),
); );
let holds = LayoutHolds { let own_holds = LayoutHolds {
frame: [own[0].and(under[0]), own[1].and(under[1])], frame: own,
extent: [ extent: extent_own,
extent_own[0].and(extent_under[0]),
extent_own[1].and(extent_under[1]),
],
placement: reads_placement.then_some(placement), placement: reads_placement.then_some(placement),
}; };
let answer_holds = own_holds.and(answer_under);
let holds = answer_holds.and(under);
debug_assert!( debug_assert!(
holds.contains(px, placement), holds.contains(px, placement),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
@@ -487,6 +482,7 @@ impl UiRenderState {
region_node: false, region_node: false,
mask, mask,
given_region: UiRegion::FULL, given_region: UiRegion::FULL,
offer_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2], offer_placement: [None; 2],
px, px,
@@ -504,6 +500,7 @@ impl UiRenderState {
region, region,
placement, placement,
given_region: info.given_region, given_region: info.given_region,
offer_region: info.offer_region,
offer_len: info.offer_len, offer_len: info.offer_len,
offer_placement: info.offer_placement, offer_placement: info.offer_placement,
// Whoever asked writes the answer, if this was the asking. // Whoever asked writes the answer, if this was the asking.
@@ -516,7 +513,7 @@ impl UiRenderState {
textures, textures,
primitives, primitives,
mask_region, mask_region,
inherited_children, extent_children,
children, children,
size_deps, size_deps,
declared: declared_lens(rsc.widgets(), id), declared: declared_lens(rsc.widgets(), id),
@@ -530,7 +527,7 @@ impl UiRenderState {
}; };
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, active); self.active.insert(id, active);
(size, holds) (size, answer_holds)
} }
/// Keeps a region node's entry across redraws because descendants retain /// Keeps a region node's entry across redraws because descendants retain
@@ -693,6 +690,24 @@ impl UiRenderState {
if !active.holds.contains(info.px, placement) { if !active.holds.contains(info.px, placement) {
#[cfg(feature = "layout-diagnostics")] #[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::bump(Counter::ReuseOutside);
diag::reuse(id, ReuseOutcome::Outside); diag::reuse(id, ReuseOutcome::Outside);
} }
@@ -715,6 +730,7 @@ impl UiRenderState {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.region = region; active.region = region;
active.given_region = info.given_region; active.given_region = info.given_region;
active.offer_region = info.offer_region;
active.offer_len = info.offer_len; active.offer_len = info.offer_len;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
@@ -765,19 +781,18 @@ impl UiRenderState {
} }
let parent_move = active.move_idx; let parent_move = active.move_idx;
let mask = active.mask; let mask = active.mask;
let children = active.inherited_children.len(); let children = active.extent_children.len();
for index in 0..children { 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 active = &self.active[&child];
let (child_local, chosen) = ask_box( let (child_local, chosen) = ask_box(part, active.declared, active.own_align, slot);
UiRegion::FULL, // What it took of that box is its own answer, which this move did
active.declared, // not ask again: keep the placement it has on any axis this
active.own_align, // widget is not the one choosing.
[Some(placement.x), Some(placement.y)],
);
let child_placement = UiRegion { let child_placement = UiRegion {
x: chosen[0].unwrap_or(UiSpan::FULL), x: chosen[0].unwrap_or(active.placement.x),
y: chosen[1].unwrap_or(UiSpan::FULL), y: chosen[1].unwrap_or(active.placement.y),
}; };
let child_info = DrawInfo { let child_info = DrawInfo {
layer: active.layer, layer: active.layer,
@@ -787,6 +802,7 @@ impl UiRenderState {
region_node: active.move_idx != active.parent_move, region_node: active.move_idx != active.parent_move,
mask, mask,
given_region: child_local, given_region: child_local,
offer_region: active.offer_region,
offer_len: active.offer_len, offer_len: active.offer_len,
offer_placement: active.offer_placement, offer_placement: active.offer_placement,
px: child_local.size().to_px(info.px), px: child_local.size().to_px(info.px),
@@ -926,6 +942,7 @@ impl UiRenderState {
region: UiRegion::FULL, region: UiRegion::FULL,
placement: UiRegion::FULL, placement: UiRegion::FULL,
given_region: UiRegion::FULL, given_region: UiRegion::FULL,
offer_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2], offer_placement: [None; 2],
answer: None, answer: None,
@@ -937,7 +954,7 @@ impl UiRenderState {
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None, mask_region: None,
inherited_children: Vec::new(), extent_children: Vec::new(),
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
move_idx: info.parent_move, move_idx: info.parent_move,
@@ -959,8 +976,6 @@ impl UiRenderState {
} }
} }
self.slots.clear(); self.slots.clear();
self.answer_invalid.clear();
self.replace_answers = false;
self.moves.clear(); self.moves.clear();
self.layers.clear(); self.layers.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
@@ -974,7 +989,6 @@ impl UiRenderState {
rsc.on_remove(id); rsc.on_remove(id);
self.remove(id, true, rsc); self.remove(id, true, rsc);
self.drop_slot(id); self.drop_slot(id);
self.answer_invalid.remove(&id);
} }
rsc.ui_mut().textures.free(); rsc.ui_mut().textures.free();
} }
@@ -993,24 +1007,52 @@ impl UiRenderState {
// something below is about to change it -- which is the whole class // something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears // of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved. // 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 { loop {
let next = rsc for &id in rsc.widgets().needs_redraw.iter() {
.widgets() if !self.deferred.contains(&id) {
.needs_redraw let depth = self.depth(id);
.iter() self.pending.insert((depth, id));
.copied() }
.filter(|id| !self.deferred.contains(id)) }
.max_by_key(|&id| self.depth(id)); if self.pending.is_empty() {
let Some(id) = next else { break }; 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")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops); diag::bump(Counter::QueuePops);
if !self.redraw(id, rsc) { if !self.redraw(id, rsc) {
self.deferred.insert(id); self.deferred.insert(id);
} }
} }
}
self.deferred.clear(); 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 { fn depth(&self, id: WidgetId) -> usize {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::DepthReads); diag::bump(Counter::DepthReads);
@@ -1111,20 +1153,11 @@ impl UiRenderState {
if let Some(parent) = active.parent if let Some(parent) = active.parent
&& (declared_changed || alignment_changed || !active.drawn || active.answer.is_none()) && (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 // Both stay marked: the parent because it has this to draw, and
// this because the parent must draw it rather than keep what it // this because the parent must draw it rather than keep what it
// has. The mark comes off in `draw_at`, where the parent draws. // has. The mark comes off in `draw_at`, where the parent draws.
rsc.widgets_mut().needs_redraw.insert(id); self.mark(id, rsc.widgets_mut());
rsc.widgets_mut().needs_redraw.insert(parent); self.mark(parent, rsc.widgets_mut());
return false; return false;
} }
if !active.drawn { if !active.drawn {
@@ -1146,16 +1179,6 @@ impl UiRenderState {
return true; return true;
}; };
let (given_px, offered_px) = self.asked_px(id); 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 { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: active.parent, parent: active.parent,
@@ -1164,6 +1187,7 @@ impl UiRenderState {
region_node: rsc.widgets().is_region_node(id), region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask, mask: active.parent_mask,
given_region: active.given_region, given_region: active.given_region,
offer_region: active.offer_region,
offer_len: active.offer_len, offer_len: active.offer_len,
offer_placement: active.offer_placement, offer_placement: active.offer_placement,
px: given_px, px: given_px,
@@ -1173,30 +1197,61 @@ impl UiRenderState {
placement: AXES placement: AXES
.map(|axis| active.decided[axis as usize].then(|| *active.placement.axis(axis))), .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")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
// Refresh the original measurement before restoring the assigned slot. // Asked again where its parent asked: the offer's frame, composed
// Its lengths may differ even though the fraction reference is unchanged. // 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 { let offered = DrawInfo {
placement: info.offer_placement, placement: info.offer_placement,
given_region: info.offer_region,
px: offered_px,
..info ..info
}; };
let answer = self.draw_inner(id, given, offered, old, false, rsc); // Where the given differs from the offer, the first draw is only the
if info.placement != offered.placement { // 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); self.draw_inner(id, given, info, None, false, rsc);
} }
if Some(answer) != was_answer { let active = self.active.get_mut(&id).unwrap();
// Its parent chose its box knowing the old answer, so it lays out // A wider contract does not invalidate the guarantee the parent kept.
// again and chooses the box the new one asks for. // 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")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::SizeChanges); diag::bump(Counter::SizeChanges);
diag::bump(Counter::ReaderEdges); diag::bump(Counter::ReaderEdges);
} }
rsc.widgets_mut().needs_redraw.insert(parent); self.mark(parent, rsc.widgets_mut());
} }
true 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 { pub struct SlotId {
idx: u32, idx: u32,
genr: 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 // 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 // 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. // 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(); let inner = painter.widget_within(&self.inner, inside).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
+2 -10
View File
@@ -13,8 +13,6 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), 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 // 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 // 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 // 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. // drawing belongs to the layer it was made on.
Some((i, child)) => { Some((i, child)) => {
painter.child_layer_at(i); painter.child_layer_at(i);
painter painter.widget(child).size()
.widget_at(
child,
UiRegion::FULL,
[Some(placement.x), Some(placement.y)],
)
.size()
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
@@ -43,7 +35,7 @@ impl Widget for Stack {
// Every other child has the stack's own box for its region, since // 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 // the stack is what contains it, and where it sits in one bigger
// than itself is its own business. // than itself is its own business.
painter.widget_within(child, placement); painter.widget_within(child, DrawRegion::Extent(UiRegion::FULL));
} }
size 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()] 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));
}
}
}
}