Compare commits

...
Author SHA1 Message Date
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
iris-ai 7601aa2a5d Measure container children without intermediate placement 2026-09-17 15:53:24 -04:00
iris-ai c330ecec2b Skip inverse arithmetic for unrestricted layout validity 2026-09-17 15:53:24 -04:00
iris-ai 39f7b08c6c Honor fixed child alignment inside parent-selected slots 2026-09-17 15:53:24 -04:00
iris-ai 2ed5503717 Recompose retained frames exactly and preserve text width validity
Keep each widget's original local frame and replay the same composition
order on reuse. Remove inverse region remapping, including its fixed-frame
fallback that forced otherwise valid subtrees to draw again.

Require exact pixel-region equality in the shared generated oracle. Check
primitive and mask geometry as well as draw reuse when fixed frames resize.
Publish text's retained line-break range, with no upper bound when there
are no soft breaks, and cover widening, explicit newlines, and empty text.

Compared with efb416b, the depth-8 diagnostic rig performs 7-9% fewer widget
evaluations in the affected phases. Uninstrumented release runs use 3.5%
fewer instructions for size changes and 5.0% fewer for resize. Repaint and
scroll use 0.7% and 0.6% more instructions. Container updates remain substantially more expensive than the e44dea3 baseline;
this is still an experimental continuation, not a production replacement.
2026-09-17 15:11:03 -04:00
iris-ai efb416bbc3 Retain frame and extent dependencies independently
Keep the original measurement placement separate from the assigned slot.
Validate frame and extent lengths before reusing an answer or drawing, and
represent hint-only records as having no measured answer.

Retain primitive and mask coordinates with their frame/extent reference.
Forwarded children follow a reused wrapper's placement without rerunning
valid draw bodies. Keep the single Widget::draw API.

Restore the eight failing suite cases from the region/placement prototype,
with regressions for mixed coordinate references, a changed inherited
extent, the sizing-stack fraction, and an undrawn share becoming visible.

This remains experimental: nested container updates do substantially more
work than e44dea3 despite restoring the leaf and wrapper reuse guarantees.
Do not merge it as a performance improvement.
2026-09-17 14:50:09 -04:00
iris-ai 5fcace1bfa WIP: a widget's region stays put and its placement moves in it
The protocol split: `region` is the box a parent gives a widget -- what a
fraction it declares or reports is a fraction of, and the coordinates
every region it writes composes within -- and it is the same box on the
ask that measures and the ask that places. `placement` is what of that
region the drawing takes, chosen by the parent per axis or by the
widget's own answer and alignment.

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

84 of 92 suite tests pass. Five text and region-node cases still diverge
warm against cold, and three count a second widget draw where a span's
measuring ask and its placing ask give different placements.
2026-09-17 13:53:14 -04:00
23 changed files with 1657 additions and 562 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",
]; ];
} }
+11
View File
@@ -7,6 +7,17 @@ pub enum Axis {
Y, Y,
} }
impl Axis {
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
match self {
Self::X => [aligned, ortho],
Self::Y => [ortho, aligned],
}
}
}
impl std::ops::Not for Axis { impl std::ops::Not for Axis {
type Output = Self; type Output = Self;
+17
View File
@@ -176,6 +176,23 @@ impl TextBuffer {
self.layout_key.as_ref()?.max_width self.layout_key.as_ref()?.max_width
} }
/// Widths covered by the current line breaks, including a wider shaping
/// retained when a later draw requested a narrower box.
pub fn width_holds(&self) -> crate::Holds {
let Some(width) = self.wrap_width() else {
return crate::Holds::ANY;
};
let width = Px::from_f32(width);
let soft_wrapped = self.layout.lines().any(|line| {
matches!(
line.break_reason(),
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
)
});
let upper = if soft_wrapped { width } else { Px::MAX };
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
}
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height()) Vec2::new(self.layout.width(), self.layout.height())
} }
+2 -1
View File
@@ -106,7 +106,8 @@ impl UiRenderNode {
self.active.push(i); self.active.push(i);
for change in draws.apply_free() { for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives { for primitive in &mut inst.primitives {
let h = &mut primitive.handle;
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
+51 -24
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
UiRegion, UiVec2, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,29 +9,29 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// The box its drawing is in, in `parent_move`'s coordinates. /// The box its parent gave it, in `parent_move`'s coordinates: what it
/// was asked about, and what a fraction under it is a fraction of. A
/// local redraw asks here.
pub region: UiRegion, pub region: UiRegion,
/// The box its parent gave it, in the same coordinates: what it was /// Where its drawing sits inside that box, in the box's own coordinates.
/// asked about, before its own answer placed its drawing inside it. pub placement: UiRegion,
/// `region` is that placement, and a local redraw asks here. /// The original frame in its parent widget's coordinates. Recomposition
pub given: UiRegion, /// and pixel-length evaluation both follow this chain.
/// The same box as lengths of its parent's box, which is the one route pub given_region: UiRegion,
/// to a box in pixels: a draw threads these down a level at a time, and
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
pub given_len: UiVec2,
/// 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
/// asked again -- and a chain of fractions has no frame in it, which is /// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it. /// why a region node between two widgets cannot break it.
pub offer_len: UiVec2, pub offer_len: UiVec2,
/// What it answered there: the size and what that held for. pub offer_placement: [Option<crate::UiSpan>; 2],
pub answer: (Size, [Holds; 2]), /// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// What the widget said it used of its box, the last time it drew. /// What the widget said it used of its box, the last time it drew.
pub size: Size, pub size: Size,
/// The pixel lengths of `region`, per axis, that its drawing and `size` /// The frame, extent and explicit placement reads that this drawing holds for.
/// hold for. pub holds: LayoutHolds,
pub holds: [Holds; 2],
pub drawn: bool, pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a /// How far down the tree it was drawn, the root being 1. Carried down a
@@ -39,7 +39,13 @@ pub struct ActiveData {
/// widget a frame visits and cannot drift while one is being drawn. /// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize, pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>,
/// 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>,
@@ -70,16 +76,37 @@ pub struct ActiveData {
} }
impl ActiveData { impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths.
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
}
/// Whether what it answered still stands for a box of these pixel /// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its /// lengths -- the box it was asked in, where `holds` is about the box its
/// answer then chose. /// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool { pub fn answers_at(&self, px: crate::PxVec2) -> bool {
let (_, holds) = self.answer; self.answer.is_some_and(|(_, holds)| {
holds[0].contains(px.x) && holds[1].contains(px.y) holds.contains(
px,
UiRegion {
x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL),
y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL),
},
)
})
}
}
/// 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]),
}
} }
} }
+36
View File
@@ -0,0 +1,36 @@
use crate::{PrimitiveHandle, UiRegion};
/// Retains which box geometry follows when only the extent changes.
#[derive(Clone, Copy, Debug)]
pub enum DrawRegion {
Frame(UiRegion),
Extent(UiRegion),
}
impl DrawRegion {
pub(crate) fn resolve(self, frame: UiRegion, extent: UiRegion) -> UiRegion {
match self {
Self::Frame(local) => local.within(&frame),
Self::Extent(local) => local.within(&extent).within(&frame),
}
}
pub(crate) fn map(self, f: impl FnOnce(UiRegion) -> UiRegion) -> Self {
match self {
Self::Frame(local) => Self::Frame(f(local)),
Self::Extent(local) => Self::Extent(f(local)),
}
}
}
impl From<UiRegion> for DrawRegion {
fn from(region: UiRegion) -> Self {
Self::Frame(region)
}
}
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: DrawRegion,
}
+13
View File
@@ -52,6 +52,9 @@ impl Holds {
/// allowance: inverting it is two divisions and nothing else, and the /// allowance: inverting it is two divisions and nothing else, and the
/// whole of a box maps back to itself. /// whole of a box maps back to itself.
pub const fn through(self, len: Len) -> Self { pub const fn through(self, len: Len) -> Self {
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
return Self::ANY;
}
let rel = len.rel.raw() as i64; let rel = len.rel.raw() as i64;
if rel == 0 { if rel == 0 {
return Self::ANY; return Self::ANY;
@@ -92,6 +95,16 @@ mod tests {
use super::*; use super::*;
use crate::Rel; use crate::Rel;
#[test]
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
for px in [-8, 0, 8] {
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
assert_eq!(Holds::ANY.through(len), Holds::ANY);
}
}
}
#[test] #[test]
fn through_reverses_a_range_for_a_negative_fraction() { fn through_reverses_a_range_for_a_negative_fraction() {
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20. // `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
+63
View File
@@ -0,0 +1,63 @@
use crate::{Axis, Holds, PxVec2, UiRegion};
/// Dependencies of one evaluation, before the frame and extent are composed.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub frame: [Holds; 2],
pub extent: [Holds; 2],
pub placement: Option<UiRegion>,
}
impl LayoutHolds {
pub const ANY: Self = Self {
frame: [Holds::ANY; 2],
extent: [Holds::ANY; 2],
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| {
self.frame[axis as usize].contains(px.axis(axis))
&& self.extent[axis as usize]
.contains(placement.axis(axis).len().to_px(px.axis(axis)))
})
}
pub fn in_frame(self, placement: UiRegion) -> [Holds; 2] {
[Axis::X, Axis::Y].map(|axis| {
self.frame[axis as usize]
.and(self.extent[axis as usize].through(placement.axis(axis).len()))
})
}
}
+4
View File
@@ -10,12 +10,16 @@ use crate::{
pub const CHAIN_LIMIT: u32 = 64; pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod draw_region;
mod holds; mod holds;
mod layout_holds;
mod painter; mod painter;
mod render_state; mod render_state;
pub use active::*; pub use active::*;
pub use draw_region::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
+310 -126
View File
@@ -1,12 +1,12 @@
#[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, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget, Axis, DrawRegion, ExtentPlacement, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
WidgetId, Widgets, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::render_state::DrawInfo,
}; };
@@ -17,8 +17,20 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's box, in the coordinates of `move_idx`. /// The box its parent gave it, in the coordinates of `move_idx`: what a
/// fraction of this widget's area is a fraction of, and what every region
/// it writes composes within. The same box on the ask that measures and
/// the ask that places, which is what keeps a fraction under it from
/// being resolved twice.
pub(super) region: UiRegion, pub(super) region: UiRegion,
/// Where this widget's drawing sits inside that box, in the box's own
/// coordinates: `FULL` while its answer is not yet known, and the box
/// its answer or its parent chose once one of them has.
pub(super) placement: UiRegion,
/// Whether this draw read its placement, which makes the drawing one
/// that holds for that placement alone -- the way reading a length in
/// pixels makes it hold for that length.
pub(super) reads_placement: bool,
/// That box in pixels, which its children's are a length of: threaded /// That box in pixels, which its children's are a length of: threaded
/// down from the box this widget was given rather than composed back up /// down from the box this widget was given rather than composed back up
/// the chain, so every length in layout is one multiply from its /// the chain, so every length in layout is one multiply from its
@@ -26,7 +38,12 @@ pub struct Painter<'a> {
pub(super) px: PxVec2, pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<DrawRegion>,
pub(super) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub(super) extent_own: [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.
@@ -43,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,
@@ -57,13 +74,28 @@ pub struct Painter<'a> {
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: DrawRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>(); let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region); self.write(kind, primitive, region);
} }
/// 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: UiRegion) { 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(
@@ -72,15 +104,15 @@ impl<'a> Painter<'a> {
kind, kind,
id: self.id, id: self.id,
primitive, primitive,
region, region: resolved,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx, move_idx: self.move_idx,
}, },
); );
self.push_primitive(h); self.push_primitive(RetainedPrimitive { handle: h, region });
} }
fn push_primitive(&mut self, h: PrimitiveHandle) { fn push_primitive(&mut self, h: RetainedPrimitive) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -88,28 +120,48 @@ impl<'a> Painter<'a> {
self.primitives.push(h); self.primitives.push(h);
} }
/// Writes a primitive to be rendered /// Writes a primitive over the whole of this widget's own box.
pub fn primitive(&mut self, primitive: impl PrimitiveLike) { pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let at = DrawRegion::Extent(UiRegion::FULL);
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region) self.primitive_at(primitive, at)
} }
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { /// Writes in the frame by default. `DrawRegion::Extent` keeps the local
/// geometry attached to this widget's box without reading its placement.
pub fn primitive_within(
&mut self,
primitive: impl PrimitiveLike,
region: impl Into<DrawRegion>,
) {
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region)); self.primitive_at(primitive, region.into());
} }
pub fn set_mask(&mut self, region: UiRegion) { /// Sets a mask in the selected frame or extent coordinates.
pub fn set_mask(&mut self, region: impl Into<DrawRegion>) {
let region = region.into();
self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { self.mask = self.rsc.ui_mut().masks.push(Mask {
region, region: region.resolve(self.region, self.placement),
move_idx: self.move_idx, move_idx: self.move_idx,
}); });
} }
/// Draws a widget within this widget's region. /// Draws a widget in the whole of this widget's own box: it gets the
/// same region -- the same area for its fractions to be of -- and is put
/// where this widget was put. What a container that is only a wrapper
/// 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> {
self.widget_within(id, UiRegion::FULL) let own = self.placement;
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
@@ -125,55 +177,77 @@ 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.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 coordinates, and the child's declared lengths are still to be /// resolves declared lengths and reports against that frame, then places
/// taken from it. Where the child's drawing sits inside what it is given /// its drawing by its own alignment.
/// is the child's alignment, applied where the child is drawn, so a ///
/// container positions a child either by handing it a box of exactly its /// `DrawRegion::Extent` gives a part of where this widget's drawing sits
/// length or by leaving it room and letting its alignment decide. /// 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, region.size(), [false; 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 what the answer means. /// Draws a widget in `region`, saying where in it the drawing goes.
/// ///
/// `reports_of` is what a fraction the child reports is a fraction of, as /// `region` is the child's own area: what a fraction it declares or
/// lengths of this widget's own box. It is the box the child was given /// reports is a fraction of, and the coordinates the regions it writes
/// wherever that box is the child's whole area -- a pad's inset, a stack /// compose within. It is the same box on the ask that measures and the
/// child, a scroll's content -- and a span passes its own extent along /// ask that places, which is what stops a fraction under it being
/// the row instead: it offers each child the room left from its cursor, /// resolved twice.
/// because a text has to wrap at the width actually there, while
/// `rel(0.5)` still means half the span wherever the child sits in it.
/// ///
/// A `decided` axis is one where this box was chosen from the widget's /// `placement` is what of that region the child's drawing takes, per
/// own answer. On those the answer is not placed inside the box again: it /// axis, wherever this widget is choosing. `None` leaves the axis to the
/// already is the box, and a fraction taken of it a second time would /// child's own answer and alignment, which is what
/// shrink it twice. A container uses that where it hands back exactly /// [`Self::widget_within`] passes. A span passes the whole row as the
/// what a child asked for -- a span placing a child at the length it /// region, so `rel(0.5)` is half the row wherever the child sits in it,
/// reported, a scroll giving its content the content's own length. /// and places the child by passing the slot along its axis.
pub fn widget_at<'s, W: ?Sized>( pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
reports_of: UiVec2, placement: [Option<UiSpan>; 2],
decided: [bool; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, None, false)
}
fn widget_at_inner<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
extent: Option<ExtentPlacement>,
measuring: bool,
) -> DrawResult<'s, 'a, W> {
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 region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
// Composing `FULL` through a box is not quite the identity in f32, let (local, placement) = ask_box(region, declared, align, placement);
// so a child with nothing declared keeps the box it would have had.
let local = match declared.iter().any(Option::is_some) {
true => declared_box(region, declared, align),
false => region,
};
let within = match local == UiRegion::FULL { let within = match local == UiRegion::FULL {
true => self.region, true => self.region,
false => local.within(&self.region), false => local.within(&self.region),
@@ -197,15 +271,20 @@ 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_placement = if first_ask {
placement
} else {
self.state
.active
.get(&id.id())
.map_or(placement, |a| a.offer_placement)
};
let px = given_len.to_px(self.px); let px = given_len.to_px(self.px);
let offered_px = offer_len.to_px(self.offered_px); let offered_px = offer_len.to_px(self.offered_px);
// Whether this ask is the child's offer question, which is a question
// about lengths: the same lengths somewhere else is the same question.
let answers_offer = self.at_offer && px == offered_px;
// The answer and what it holds for, both about the box asked in. The // 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 {
@@ -215,27 +294,73 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
given_len, given_region: local,
offer_len, offer_len,
offer_placement,
px, px,
offered_px, offered_px,
decided, placement,
}, },
None, None,
measuring,
self.rsc, self.rsc,
); );
if answers_offer { let in_parent = |holds: LayoutHolds| {
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds); let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
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);
} }
// Whatever the child's answer holds for keeps this one to the boxes }
// that give the child a length inside it. // Its box is a part of this widget's extent, so what it
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { // holds for is a range on that extent and none of it a
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); // 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(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 { DrawResult {
child: id, child: id,
painter: self, painter: self,
size: in_parent_frame(size, reports_of, declared), size: in_parent_frame(size, local.size(), declared),
answer_holds,
} }
} }
@@ -267,42 +392,52 @@ impl<'a> Painter<'a> {
} }
} }
/// A child's length in the box it is about to be offered, if it can be /// Measures a child's length from its hint, a retained answer, or `draw`.
/// had without drawing it: from its hint, or from a drawing it already /// A fresh draw evaluates the offer without placing its answer. The caller
/// has that holds for that box. `reports_of` is what a fraction in the /// must later place or undraw the child.
/// answer is a fraction of, as it is for [`Self::widget_at`]. pub fn measure_len<W: ?Sized>(
pub fn known_len<W: ?Sized>(
&mut self, &mut self,
child: &StrongWidget<W>, child: &StrongWidget<W>,
axis: Axis, axis: Axis,
region: UiRegion, region: UiRegion,
reports_of: UiVec2, placement: [Option<UiSpan>; 2],
) -> Option<LayoutLen> { ) -> LayoutLen {
let offered = placement;
let declared = self.declared_lens(child); let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id()); let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align); let (local, placement) = ask_box(region, declared, align, placement);
let first_ask = self.offer(child.id()); let first_ask = self.at_offer && !self.offered.contains(&child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer_len = local.size();
}
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return hint;
} }
let px = local.size().to_px(self.px); let px = local.size().to_px(self.px);
let (size, holds) = let retained =
self.state self.state
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?; .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, None, true)
.len(axis);
};
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits); diag::bump(Counter::RetainedSizeHits);
self.depend_on(child); self.depend_on(child);
if first_ask { if first_ask {
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.answer = (size, holds); active.offer_len = local.size();
active.offer_placement = placement;
} }
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { let placement = UiRegion {
x: placement[0].unwrap_or(UiSpan::FULL),
y: placement[1].unwrap_or(UiSpan::FULL),
};
let holds = holds.in_frame(placement);
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()));
} }
Some(in_parent_frame(size, reports_of, declared).axis(axis)) in_parent_frame(size, local.size(), declared).axis(axis)
} }
/// Whether this is the first box a child is asked about in during a draw /// Whether this is the first box a child is asked about in during a draw
@@ -334,11 +469,16 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// Writes glyphs in the selected frame or extent coordinates.
// 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: UiRegion) { 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>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let mut region = origin; 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));
@@ -348,7 +488,9 @@ 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);
self.write( region
};
self.write_resolved(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
uv_min: glyph.entry.uv_min, uv_min: glyph.entry.uv_min,
@@ -357,17 +499,32 @@ impl<'a> Painter<'a> {
color: text.color, color: text.color,
flags: glyph.entry.flags(), flags: glyph.entry.flags(),
}, },
region, origin.map(place),
place(resolved),
); );
} }
} }
/// This widget's box, in the coordinates its own primitives are written /// The box this widget's parent gave it, in the coordinates its own
/// in -- so a region composed `within` it may be drawn directly. /// primitives are written in -- so a region composed `within` it may be
/// drawn directly. Its own box is [`Self::placement`] of this one.
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
self.region self.region
} }
/// Where this widget's drawing goes inside the box it was given, in that
/// box's coordinates: what its own answer took of it, or what its parent
/// chose for it. `FULL` on the ask that measures, since nothing has been
/// placed yet.
///
/// Reading it is what says the drawing depends on it, so a widget that
/// positions its own content reads it and is drawn again once its box is
/// known, and one that fills whatever it is given never is.
pub fn placement(&mut self) -> UiRegion {
self.reads_placement = true;
self.placement
}
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
/// widget that positions its own content reads it to place that content /// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget. /// the way the box around it would have placed the widget.
@@ -401,20 +558,44 @@ impl<'a> Painter<'a> {
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR) placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
} }
/// This widget's box in pixels. Reading it makes the drawing one that /// This widget's own box in pixels. Reading it makes the drawing one
/// holds for this box only, until `holds` says how far it goes. /// that holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 { pub fn px_size(&mut self) -> PxVec2 {
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) { PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
}
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.placement.axis(axis).len();
let len = part.to_px(self.px.axis(axis));
let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
} }
} len
self.px
} }
/// One axis of this widget's box in pixels. Prefer this to /// The lengths of this widget's own box on `axis` that what it is drawing
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// holds for -- the same primitives, in the same fractions and offsets
pub fn px_len(&mut self, axis: Axis) -> Px { /// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let part = self.placement.axis(axis).len();
let holds = holds.into();
debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.extent_own[axis as usize] = holds;
}
/// One axis of the box this widget's parent gave it, in pixels -- what a
/// fraction of its area resolves against, and so what a container divides
/// among its children. Its own box is a part of this one.
pub fn region_px_len(&mut self, axis: Axis) -> Px {
let len = self.px.axis(axis); let len = self.px.axis(axis);
let own = &mut self.own[axis as usize]; let own = &mut self.own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
@@ -423,15 +604,14 @@ impl<'a> Painter<'a> {
len len
} }
/// The lengths of this widget's box on `axis` that what it is drawing /// [`Self::holds`] stated about the region rather than about this
/// holds for -- the same primitives, in the same fractions and offsets /// widget's own box, for a container whose drawing turns on the box it
/// of the box, and the same reported size. A widget that read its /// was given rather than on the part of it it took.
/// length in pixels holds for that one alone until it says otherwise. pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.px.axis(axis)), holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box", "'{}' ({:?}) says its drawing holds for lengths that leave out its region",
self.label(), self.label(),
self.id self.id
); );
@@ -478,6 +658,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> {
@@ -488,6 +669,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
} }
@@ -521,19 +703,16 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// A child's answer as lengths of the parent's own box. A widget reports a /// A child's answer as lengths of the parent's own region. A widget reports
/// fraction, and `reports_of` is the length that fraction is of: the box the /// a fraction of its own region, and `of` is that region as a length of this
/// child was given wherever that is the child's whole area, and the parent's /// one. Pixels come through untouched, being that many pixels wherever they
/// own extent wherever the box is a positional remainder, as a span's is /// end up. A declared axis is already the parent's: it resolved the rule in
/// after an earlier child. Pixels come through untouched either way, being /// its own region, and the rule is what the report says.
/// that many pixels wherever they end up. A declared axis is already the fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
/// parent's: it resolved the rule in its own box, and the rule is what the
/// report says.
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size; let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) { for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() { if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis)); *size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis));
} }
} }
size size
@@ -562,9 +741,9 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
/// Whether what a widget reported along an axis is the whole of the box it /// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. A share fills, because a /// is in rather than a part to be placed inside it. A share fills, because a
/// share is a length only to whoever divides one, and whoever did is the one /// share is a length only to whoever divides one, and whoever did is the one
/// that handed down this box. A declared axis does too: `declared_box` /// that handed down this box. A declared axis does too: the rule already gave
/// already placed it, in the parent's box, and the rule's length is what the /// the region its length, and the rule's length is what the widget reports
/// widget reports there. And an axis the parent decided from the answer is /// there. And an axis the parent decided from the answer is
/// the answer already. /// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool { pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided reported.leftover != Weight::ZERO || declared.is_some() || decided
@@ -612,21 +791,26 @@ pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) ->
placed placed
} }
/// Takes a widget's declared lengths in the box `region` is given in, since a /// A declared axis gets a frame of that length, aligned within the parent's
/// fraction of a length means a fraction of that one, and puts what is left /// slot (or the offer). Undeclared axes keep the offered frame and chosen
/// over on the side its alignment says. A caller that already reserved the /// placement, so their reported fractions retain that reference.
/// space hands back the same length, so this is the identity for it. pub(crate) fn ask_box(
pub(crate) fn declared_box(
mut region: UiRegion, mut region: UiRegion,
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
align: RegionAlign, align: RegionAlign,
) -> UiRegion { placement: [Option<UiSpan>; 2],
for (axis, len) in AXES.into_iter().zip(declared) { ) -> (UiRegion, [Option<UiSpan>; 2]) {
let Some(len) = len else { continue }; let mut placed = [None; 2];
for (axis, (len, chosen)) in AXES.into_iter().zip(declared.into_iter().zip(placement)) {
let Some(len) = len else {
placed[axis as usize] = chosen;
continue;
};
let span = region.axis_mut(axis); let span = region.axis_mut(axis);
let len = Len::from_parts(len.rel, len.px); let len = Len::from_parts(len.rel, len.px);
span.start += (span.len() - len).scale(align.axis(axis).rel()); let slot = chosen.unwrap_or(*span);
span.start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len; span.end = span.start + len;
} }
region (region, placed)
} }
File diff suppressed because it is too large. Load diff
+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
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.widget(&self.inner); painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll` // What it occupies is its box, on both axes, for the reason `Scroll`
// reports the same: it clips what is inside to that box, so it can // reports the same: it clips what is inside to that box, so it can
+8 -5
View File
@@ -13,9 +13,8 @@ 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 inner = painter let inside = DrawRegion::Extent(self.padding.region());
.widget_within(&self.inner, self.padding.region()) let inner = painter.widget_within(&self.inner, inside).size();
.size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
@@ -53,14 +52,18 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
pub fn region(&self) -> UiRegion { /// `region` less this padding on each side.
let mut region = UiRegion::FULL; pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left; region.x.start.px += self.left;
region.y.start.px += self.top; region.y.start.px += self.top;
region.x.end.px -= self.right; region.x.end.px -= self.right;
region.y.end.px -= self.bottom; region.y.end.px -= self.bottom;
region region
} }
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = Px::from_num(amt);
Self { Self {
+8 -5
View File
@@ -15,10 +15,9 @@ impl Widget for Scroll {
// Draw in the whole container only when its scrolling-axis length is // Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset. // not already known, then draw it at the scrolled offset.
let whole = UiRegion::FULL; let whole = UiRegion::FULL;
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) { let own = painter.placement();
Some(len) => len, let answer_len =
None => painter.widget(&self.inner).size().axis(self.axis), painter.measure_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]);
};
let content = answer_len.apply_leftover(); let content = answer_len.apply_leftover();
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(container_len); self.content_len = content.to_px(container_len);
@@ -64,7 +63,11 @@ impl Widget for Scroll {
region = region.offset(offset); region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
} }
painter.widget_at(&self.inner, region, region.size(), [true; 2]); // The viewport is the inner's region, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is put is the content
// box, scrolled.
painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
+35 -36
View File
@@ -10,26 +10,34 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The row: this span's own box, as a span of the region it was given.
// Its children are laid out along it, and what they declare or report
// is a fraction of the region -- the area this span was told it has,
// which it passes on unchanged.
let own = painter.placement();
let row = *own.axis(axis);
// Across itself the span's own box is the child's region: a span is
// what contains its children there, and nothing divides that axis.
// Along it the whole region is, so a fraction means the same thing
// for every child however much of the row is left when it is asked.
let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(row.start + from, row.start + to),
Sign::Neg => UiSpan::new(row.end - to, row.end - from),
};
let far = row.len();
// A length for every child before their final boxes are chosen: from // A length for every child before their final boxes are chosen: from
// a hint where one exists, and from drawing otherwise. // a hint where one exists, and from drawing otherwise.
let mut cursor = Len::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let mut span = UiSpan::new(cursor, Len::rel_max()); // The whole region is the child's, so `rel(0.5)` is half the area
if self.dir.sign == Sign::Neg { // this span was given whatever else is in it and wherever this
span.flip(); // child sits among them. What it is placed in is the room left
} // from the cursor, because a text has to wrap at the width
let region = UiRegion::from_axis(axis, span, UiSpan::FULL); // actually there.
// Offered the room left from the cursor, because a text has to let room = axis.pair(Some(along(cursor, far)), None);
// wrap at the width actually there, but reporting a fraction of let len = painter.measure_len(child, axis, region, room);
// the whole row: `rel(0.5)` is half the span whatever else is in
// it and wherever this child sits among them.
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
Some(len) => len,
None => painter
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
.len(axis),
};
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
lens.push(len); lens.push(len);
@@ -46,9 +54,9 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// What is left for the shares to divide: the box less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the box rather than a number of pixels. // fixed, as a length of the region rather than a number of pixels.
let room = Len::rel_max() - Len::from_parts(total.rel, total.px); let room = far - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room` // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the // itself, and answered back through the same expression, so the
@@ -60,12 +68,12 @@ impl Widget for Span {
// exist at all turns on this. // exist at all turns on this.
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
shares = room.to_px(painter.px_len(axis)) > Px::ZERO; shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
let holds = match shares { let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX), true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO), false => Holds::from(Px::MIN..=Px::ZERO),
}; };
painter.holds(axis, holds.through(room)); painter.region_holds(axis, holds.through(room));
} }
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
@@ -94,28 +102,19 @@ impl Widget for Span {
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let mut span = UiSpan::FULL; let from = start;
span.start = start;
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
taken += len.leftover; taken += len.leftover;
} }
fixed.px += len.px; fixed.px += len.px;
fixed.rel += len.rel; fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room); start = shared(fixed, taken, total.leftover, room);
span.end = start; // Along the row the span says where the child goes; across it the
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); // child sits where its own alignment says. Its region is the
if self.dir.sign == Sign::Neg { // whole of what this span was given either way, which is what its
region.flip(axis); // fractions are of.
} let placed =
// Along the row this box is the child's own answer, so the answer painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
// is not placed in it again; across it the child sits where its
// alignment says.
let placed = painter.widget_at(
child,
region,
UiVec2::FULL_SIZE,
[axis == Axis::X, axis == Axis::Y],
);
if shrinks { if shrinks {
let used = placed.len(!axis); let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
+11 -13
View File
@@ -13,9 +13,10 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
}; };
// Whichever child sizes the stack decides the box every child gets. // Whichever child sizes the stack keeps the stack's whole region as
// The stack reports that size, so a child given a longer box would // its own -- the stack is the length that child asked for, so taking
// draw outside what the stack says it occupies. // the fraction of the stack's box again would take it twice -- and is
// put where the stack itself is put.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse // On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained // rather than a second drawing of it somewhere else: a retained
@@ -26,18 +27,15 @@ impl Widget for Stack {
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i); painter.child_layer_at(i);
// The sizing child placed its own content in the box its answer // Every other child has the stack's own box for its region, since
// decided, and this box was derived from that answer, so applying // the stack is what contains it, and where it sits in one bigger
// its alignment again here would place it twice. Every other // than itself is its own business.
// child is handed a box that owes nothing to its own answer, and painter.widget_within(child, DrawRegion::Extent(UiRegion::FULL));
// where it sits in one bigger than itself is its own business.
match sizing == Some(i) {
true => painter.widget_at(child, region, region.size(), [true; 2]),
false => painter.widget_within(child, region),
};
} }
size size
} }
+5 -15
View File
@@ -50,20 +50,11 @@ impl TextView {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X)); let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
// The shaper measures in floats, which is where a glyph advance comes // The shaper measures in floats, which is where a glyph advance comes
// from; what it answers goes back on the grid. // from; what it answers goes back on the grid.
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32)); painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
// A greedy break is the same break at every width from its longest if width.is_some() {
// line up to the one it was made at: each line still fits, and none painter.holds(Axis::X, self.buf.width_holds());
// could take a word that did not fit in the wider box. A line too
// long to fit at all says nothing about narrower boxes.
//
// The step at or above that longest line rather than the nearest
// one, since the shaper measures in floats: the nearest step is
// under the line half the time, and a range starting there admits a
// box the line does not fit in, where the break is not this one.
if let Some(width) = width {
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
} }
text self.buf.rendered().expect("render_text placed the glyphs")
} }
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
@@ -89,8 +80,7 @@ impl TextView {
// hair under that line, and the break made in it is not the break a // hair under that line, and the break made in it is not the break a
// cold layout makes there. // cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size)); let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
let within = region.within(&painter.region()); painter.glyphs(tex, DrawRegion::Extent(region));
painter.glyphs(tex, within);
(region, size) (region, size)
} }
+47 -8
View File
@@ -20,11 +20,12 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
assert_corners!(h, right, (100, 0), (400, 200)); assert_corners!(h, right, (100, 0), (400, 200));
} }
/// A span offers each child the room left after the one before, because a /// A span places each child in the room left after the one before, because a
/// text has to wrap at the width actually there, but reads what the child /// text has to wrap at the width actually there, but the child's region is
/// reports as a fraction of the whole row. So two children asking for half /// the whole row. So two children asking for half each take the whole row
/// each take the whole row between them, however much of it was left when /// between them, however much of it was left when each was asked, and a third
/// each was asked, and a third overflows. /// overflows -- and a span passes its own region on unchanged, so a child of
/// a nested span asking for half asks for half of the same row.
#[test] #[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() { fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100)); let mut h = Harness::new((400, 100));
@@ -34,10 +35,10 @@ fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc); let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0))); h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported and drawn there // The nested span is placed at the length it reported, and its own child
// once more; half of that final box is what its own child takes. // asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100)); assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (300, 100)); assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100)); assert_corners!(h, tail, (400, 0), (500, 100));
} }
@@ -698,3 +699,41 @@ fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
} }
} }
} }
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
#[test]
fn a_fixed_child_is_centered_in_its_wrappers_share() {
let mut h = Harness::new((600, 300));
let leaf = rect(Color::RED).sized((100, 100)).center().add(&mut h.rsc);
let wrapper = leaf
.wrapper()
.width(leftover(2))
.height(rel(1.0))
.add(&mut h.rsc);
let other = rect(Color::BLUE).width(200).add(&mut h.rsc);
h.set_root((other, wrapper).span(Dir::RIGHT));
assert_corners!(h, wrapper, (200, 0), (600, 300));
assert_corners!(h, leaf, (350, 100), (450, 200));
h.resize((900, 400));
h.frame();
assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250));
}
+608 -10
View File
@@ -156,16 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT)); h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1); assert_eq!(told_draws.get(), 1);
// Reading its box makes its drawing hold for the measuring box alone, // Only the available length changes: positioning the final slot does
// and it reports less than that box: so it is drawn again in the box its // not invalidate a numeric size read.
// answer places it in, and once more in the final box the span chooses. assert_eq!(asked_draws.get(), 2);
// A widget that says what it holds for, as text does, skips the middle
// one.
assert_eq!(
asked_draws.get(),
3,
"drawn to be measured, in its placed box, then in its final box"
);
} }
#[test] #[test]
@@ -737,3 +730,608 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
"the span it moved to is the one the change has to reach" "the span it moved to is the one the change has to reach"
); );
} }
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
let instance = &h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx];
h.render
.moves
.resolve(instance.move_idx, instance.region)
.to_px(h.render.output_size())
})
.collect()
}
#[test]
fn frame_geometry_and_extent_geometry_keep_their_references() {
struct Both(Rc<Cell<usize>>);
impl Widget for Both {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.primitive_within(RectPrimitive::color(Color::RED), UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::GREEN).width(100).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let both = Both(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(both, node);
h.set_root((first, both).span(Dir::RIGHT));
let count = draws.get();
h.set_len(first, Axis::X, 200);
h.frame();
assert_eq!(draws.get(), count);
let bounds = primitive_bounds(&h, both.id());
assert_eq!(bounds[0].top_left.x, Px::ZERO);
assert_eq!(bounds[0].bot_right.x, Px::from_int(400));
assert_eq!(bounds[1].top_left.x, Px::from_int(200));
assert_eq!(bounds[1].bot_right.x, Px::from_int(400));
}
}
#[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
let first = rect(Color::RED).width(width).add(&mut h.rsc);
let words = wtext(text).size(20).wrap(true).add(&mut h.rsc);
let through = Stretchy {
inner: words.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add(&mut h.rsc);
h.set_root((first, through).span(Dir::RIGHT));
(words, first)
}
let short = "one two";
let long = "one two three four five six seven eight nine ten eleven twelve";
let mut warm = Harness::new((400, 200));
let (words, first) = build(&mut warm, 50, short);
warm.set_len(first, Axis::X, 200);
warm.frame();
*warm.rsc[words].content = long.to_string();
warm.frame();
let mut cold = Harness::new((400, 200));
let (other, _) = build(&mut cold, 200, long);
assert_eq!(warm.region(&words), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, words.id()),
primitive_bounds(&cold, other.id())
);
}
#[test]
fn widening_text_without_soft_breaks_reuses_its_drawing() {
struct CountedText {
text: Text,
draws: Rc<Cell<usize>>,
}
impl Widget for CountedText {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.text.draw(painter)
}
}
for content in ["Short text", "Two hard\nline breaks\nhere", ""] {
let plant = |h: &mut Harness| {
let mut text = Text::new(content);
text.wrap = true;
let draws = Rc::new(Cell::new(0));
let root = CountedText {
text,
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(root);
(root, draws)
};
let mut warm = Harness::new((300, 200));
let (root, draws) = plant(&mut warm);
let before = draws.get();
warm.resize((500, 200));
warm.frame();
assert_eq!(draws.get(), before, "{content:?}");
let mut cold = Harness::new((500, 200));
let (other, _) = plant(&mut cold);
assert_eq!(warm.region(&root), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, root.id()),
primitive_bounds(&cold, other.id())
);
}
}
#[test]
fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
struct Frame {
child: StrongWidget,
region: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_within(&self.child, self.region);
Size::LEFTOVER
}
}
struct Painted(Rc<Cell<usize>>);
impl Widget for Painted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
let fixed = |start, end| UiRegion::new(UiSpan::new(Len::px(start), Len::px(end)), UiSpan::FULL);
for node in [false, true] {
let plant = |h: &mut Harness, region| {
let draws = Rc::new(Cell::new(0));
let leaf = Painted(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node);
let inner = Frame {
child: leaf.add_strong(&mut h.rsc),
region: UiRegion::new(UiSpan::new(Len::rel(0.23), Len::rel(0.83)), UiSpan::FULL),
}
.add_strong(&mut h.rsc);
let root = Frame {
child: inner,
region,
}
.add(&mut h.rsc);
h.set_root(root);
(root, leaf, draws)
};
let mut warm = Harness::new((400, 200));
let (root, leaf, draws) = plant(&mut warm, fixed(7.0, 104.0));
let before = draws.get();
warm.rsc[root].region = fixed(19.0, 180.0);
warm.frame();
assert_eq!(draws.get(), before);
let mut cold = Harness::new((400, 200));
let (_, other, _) = plant(&mut cold, fixed(19.0, 180.0));
assert_eq!(warm.region(&leaf), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, leaf.id()),
primitive_bounds(&cold, other.id())
);
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 a_span_does_not_place_its_measurement_before_assigning_the_childs_slot() {
struct MeasuredBox(Rc<Cell<usize>>);
impl Widget for MeasuredBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.px_size();
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((100, 50))
}
}
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = MeasuredBox(draws.clone()).add(&mut h.rsc);
h.set_root((leaf,).span(Dir::RIGHT).width(rel(1.0)).height(rel(1.0)));
assert_eq!(draws.get(), 3);
assert_corners!(h, leaf, (0, 75), (100, 125));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
h.frame();
assert_eq!(draws.get(), 3);
h.resize((600, 300));
h.frame();
assert_eq!(draws.get(), 6);
assert_corners!(h, leaf, (0, 125), (100, 175));
assert_eq!(
primitive_bounds(&h, leaf.id()),
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));
}
}
}
}
+58
View File
@@ -614,3 +614,61 @@ fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
assert_eq!(warm.region(&text), cold.region(&cold_text)); assert_eq!(warm.region(&text), cold.region(&cold_text));
} }
#[test]
fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
fn build(
h: &mut Harness,
changed: bool,
) -> (WeakWidget<Span>, WeakWidget<Text>, Vec<StrongWidget>) {
let wrap = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.").size(16).wrap(true).add_strong(&mut h.rsc);
let one = || {
wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
};
let plain = one().add_strong(&mut h.rsc);
let shared = one()
.width(LayoutLen::LEFTOVER)
.height(LayoutLen::LEFTOVER)
.add(&mut h.rsc);
let mut extra: Vec<StrongWidget> = vec![
rect(Color::RED).add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
];
let children: Vec<StrongWidget> = if changed {
let mut children: Vec<StrongWidget> = vec![plain, shared.add_strong(&mut h.rsc)];
children.append(&mut extra);
children
} else {
vec![wrap, plain, shared.add_strong(&mut h.rsc)]
};
let row = Span {
children,
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
let fill: StrongWidget = rect(Color::BLUE).add_strong(&mut h.rsc);
let children: Vec<StrongWidget> = vec![fill, row.add_strong(&mut h.rsc)];
let root = Span {
children,
dir: Dir::RIGHT,
gap: Px::from_int(4),
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
h.set_root(root);
(row, shared, extra)
}
let mut warm = Harness::new((900, 1200));
let (row, shared, extra) = build(&mut warm, false);
warm.rsc[row].children.remove(0);
warm.rsc[row].children.extend(extra);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other));
}
+7 -4
View File
@@ -5,11 +5,11 @@
//! cargo test --release --features layout-diagnostics \ //! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture //! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! Uninstrumented hardware totals for one phase: //! Build the uninstrumented test with `cargo test --release --test
//! layout_diagnostics --no-run`, then run the emitted executable directly:
//! //!
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \ //! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
//! -e cycles:u,instructions:u cargo test --release \ //! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
//! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or //! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and //! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
@@ -134,6 +134,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
{ {
let diagnostics = iris::core::layout_diagnostics::take(); let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames)); print!("{}", diagnostics.per_frame(frames));
for event in diagnostics.traces() {
println!(" {event:?}");
}
for callsite in diagnostics.hot_text().iter().take(3) { for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new(); let mut ancestry = Vec::new();
let mut id = Some(callsite.id); let mut id = Some(callsite.id);
+1 -30
View File
@@ -42,21 +42,6 @@ const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0); const INNER: (f32, f32) = (640.0, 900.0);
const STILL: (f32, f32) = (900.0, 1200.0); const STILL: (f32, f32) = (900.0, 1200.0);
/// The same box, to two steps of the grid between the two ways of reaching
/// it. A move, a repaint, a row of shares and every length in pixels land on
/// the same number. What needs the slack is a position: a box centred in a
/// fraction of its parent against the same box centred in its own pixels,
/// and a box re-expressed as a fraction of a parent that changed length.
/// A step is a thousandth of a pixel, where this was a twentieth of one
/// before any of it was on a grid.
///
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
/// stopped being composed: it passes the 100-seed oracle and fails the
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
/// what is left here is the resize path's own rounding rather than a length
/// reached two ways.
const AGREE_STEPS: i32 = 2;
/// A way of changing what a span holds. Each is a shape worth its own case: /// A way of changing what a span holds. Each is a shape worth its own case:
/// taking a child out of the middle is not the same as emptying a span, and /// taking a child out of the middle is not the same as emptying a span, and
/// adding one is not the same as adding three. /// adding one is not the same as adding three.
@@ -399,20 +384,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label label
} }
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
match (got, want) {
(Some(got), Some(want)) => {
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
same(got.top_left.x, want.top_left.x)
&& same(got.top_left.y, want.top_left.y)
&& same(got.bot_right.x, want.bot_right.x)
&& same(got.bot_right.y, want.bot_right.y)
}
(None, None) => true,
_ => false,
}
}
/// Runs `case` on the tree `plan` describes, warm and cold, and says where /// Runs `case` on the tree `plan` describes, warm and cold, and says where
/// the two disagree. `seed` chooses only the values a case picks at random, /// the two disagree. `seed` chooses only the values a case picks at random,
/// so one plan under one case is one comparison however it was reached. /// so one plan under one case is one comparison however it was reached.
@@ -439,7 +410,7 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() { for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
drawn += got.is_some() as usize; drawn += got.is_some() as usize;
if same_region(got, want) { if got == want {
continue; continue;
} }
// Where two trees disagree is rarely where the cause is, so the // Where two trees disagree is rarely where the cause is, so the