Compare commits

..
Author SHA1 Message Date
iris-ai 84dad211f5 Avoid repeated plan generation and unused diagnostics in layout fuzzers 2026-09-19 13:43:11 -04:00
iris-ai add6774980 Keep retained masks and reparented drawings alive, and advance collapsed slots 2026-09-19 13:43:11 -04:00
iris-ai cadfba05dd Keep only what a room drawing is still needed for
A span measuring a child in the room kept its whole `Size`, of which the
along axis is already in `lens` and only the across one is read again when
the drawing is placed. Keep that length alone, which also retires the
rebinding of the match's result and the one in the placing loop. The
placement comment already says what becomes of a drawing made in the room,
so the measuring pass no longer says it a second time.
2026-09-19 02:51:08 -04:00
iris-ai 38b3a81053 Pin a frame by the fraction the child declared
`size_hint` resolves a child's hint against the asking widget's frame and
pins that frame, so a later draw cannot reuse a resolution made against a
different one. It asked the *resolved* hint whether it still had a fraction,
which is false whenever the frame is itself pixels -- a slot of a row, or the
box a stack's sizing child decided -- and the pin was dropped there. Ask the
declared hint, which is what made this draw depend on the frame, and what
`ruled` in `render_state` already asks for a rule.

No generated tree distinguishes the two: the fuzzer grows no `rel` rules, and
a frame that changes almost always changes a box the other pins catch. Kept
for the reason the `frame_len` pin beside it is kept -- "these two
invalidations always coincide" is an assumption nothing states.
2026-09-19 02:51:08 -04:00
11 changed files with 303 additions and 134 deletions

No files matched your search

+1
View File
@@ -53,6 +53,7 @@ pub struct ActiveData {
/// Its primitives, each keeping the box it was written in -- in this /// Its primitives, each keeping the box it was written in -- in this
/// widget's extent coordinates, which is what a move recomposes from. /// widget's extent coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>, pub primitives: Vec<RetainedPrimitive>,
/// An owned mask holds one reference independently of its primitives.
pub mask_region: Option<UiRegion>, pub mask_region: Option<UiRegion>,
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.
-15
View File
@@ -73,21 +73,6 @@ impl Holds {
} }
} }
/// What a box has to be for a part of it, this many pixels shorter, to
/// stay in this range: the range moved by that much, an end that was
/// unbounded staying so.
pub const fn longer_by(self, px: Px) -> Self {
let lo = match self.lo.raw() == Px::MIN.raw() {
true => self.lo,
false => self.lo.add(px),
};
let hi = match self.hi.raw() == Px::MAX.raw() {
true => self.hi,
false => self.hi.add(px),
};
Self { lo, hi }
}
const fn raws(lo: i64, hi: i64) -> Self { const fn raws(lo: i64, hi: i64) -> Self {
Self { Self {
lo: Px::from_raw(narrow(lo)), lo: Px::from_raw(narrow(lo)),
+49 -48
View File
@@ -35,6 +35,8 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<UiRegion>, pub(super) mask_region: Option<UiRegion>,
/// The previous drawing's owned mask, available for this draw to reclaim.
pub(super) mask_slot: Option<MaskIdx>,
/// Only children whose answers were read constrain this widget's answer. /// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds, pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
@@ -104,7 +106,6 @@ impl<'a> Painter<'a> {
fn push_primitive(&mut self, h: RetainedPrimitive) { 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:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
} }
self.primitives.push(h); self.primitives.push(h);
@@ -129,10 +130,23 @@ impl<'a> Painter<'a> {
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
let resolved = self.resolve(region); let resolved = self.resolve(region);
let move_idx = self.move_idx; let move_idx = self.move_idx;
self.mask = self.rsc.ui_mut().masks.push(Mask { let mask = Mask {
region: resolved, region: resolved,
move_idx, move_idx,
}); };
let masks = &mut self.rsc.ui_mut().masks;
self.mask = match self.mask_slot.take() {
Some(idx) => {
*masks.get_mut(idx) = mask;
idx
}
None => {
let idx = masks.push(mask);
// The owner keeps the slot alive even with no primitives.
masks.push_ref(idx);
idx
}
};
} }
/// Draws a widget in the whole of this widget's own box, with the frame /// Draws a widget in the whole of this widget's own box, with the frame
@@ -267,36 +281,32 @@ impl<'a> Painter<'a> {
let widgets = self.rsc.widgets(); let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the // A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too. // widget would draw, so it has to win over what the widget says too.
let hint = widgets let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
.size_rules(id.id())
.axis(axis)
.exact()
.or_else(|| {
widgets widgets
.get_dyn(id.id()) .get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis)) .and_then(|widget| widget.size_hint(axis))
}) });
.map(|hint| hint.within_len(self.frame.axis(axis))); let frame = self.frame.axis(axis);
let resolved = hint.map(|hint| hint.within_len(frame));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint); {
match hint { diag::hint_read(id.id(), self.id, axis, resolved);
Some(hint) => { diag::bump(match resolved {
#[cfg(feature = "layout-diagnostics")] Some(_) => Counter::HintHits,
diag::bump(Counter::HintHits); None => Counter::HintMisses,
});
}
if let Some(hint) = hint {
self.depend_on(id); self.depend_on(id);
// A fraction was just resolved against this frame, so what // Resolving a fraction against this frame makes this draw a
// this draw does with it is a function of the frame's length. // function of the frame's length. The fraction to ask about is
// the child's own: resolved against a frame of pixels, none is
// left to see it by.
if hint.rel != Rel::ZERO { if hint.rel != Rel::ZERO {
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis)); self.frame_own_len[axis as usize] = Some(frame);
}
Some(hint)
}
None => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintMisses);
None
} }
} }
resolved
} }
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
@@ -593,29 +603,20 @@ impl Painter<'_> {
result.extent[n] = holds.extent[n]; result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n]; result.extent_len[n] = holds.extent_len[n];
} }
// Its box is this widget's own less the inset. Where that is // Its box is a part of this widget's own box, in that box's
// pixels, its box is exactly that many shorter in any window, // own lengths, so what it holds for maps back through that
// so what it holds for is a range on this widget's box moved // part into a range on this widget's box. A length it pinned
// by them, and a length it pinned is this widget's length // is this widget's length less the part's pixels where the
// less them. An inset with a fraction in it is a different // part is the whole of the box less pixels, which is the one
// number of pixels in each window, and taking it off a length // shape that inverts exactly; any other part pins this
// rounds once more than taking it off pixels does: there the // widget's own length.
// child's box is a fixed expression of this one, so this (Part::Of(span), false) => {
// widget's length is pinned and the range goes on the window let part_len = span.len();
// through the child's box, the way a slot's does. result.extent[n] = holds.extent[n].through(part_len);
(Part::Inset { lead, trail }, false) => { result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel {
let inset = lead + trail; Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
match inset.rel == Rel::ZERO { _ => self.extent.axis(axis).len(),
true => { });
result.extent[n] = holds.extent[n].longer_by(inset.px);
result.extent_len[n] = holds.extent_len[n].map(|pinned| pinned + inset);
}
false => {
result.window[n] = result.window[n]
.and(holds.extent[n].through(extent.axis(axis).len()));
result.extent_len[n] = Some(self.extent.axis(axis).len());
}
}
} }
// Its box is a length this widget decided, from its own // Its box is a length this widget decided, from its own
// frame or from a sibling's answer: no length of this // frame or from a sibling's answer: no length of this
+7 -8
View File
@@ -12,13 +12,12 @@ pub enum Part {
/// here is a fraction of the window and not of the box -- the whole of a /// here is a fraction of the window and not of the box -- the whole of a
/// box is [`Self::All`], not a `rel(1.0)` span. /// box is [`Self::All`], not a `rel(1.0)` span.
From(UiSpan), From(UiSpan),
/// The box less a window length at each end, which is what a container /// A part of the box in its own coordinates, which is what a container
/// that insets one speaks -- padding, or a row asking a child in the /// that insets one speaks: taking eleven pixels off the end needs no
/// room left from its cursor. Neither end names the box's length, so a /// length, where saying the same thing in window lengths would make the
/// container can say "from here to my end" without reading how long it /// container read its own box -- and a box chosen from its own answer
/// is, and a box chosen from its own answer does not feed back into the /// then feeds back into the answer.
/// answer. Of(UiSpan),
Inset { lead: Len, trail: Len },
/// A box of this length, wherever in the parent's box the child's own /// A box of this length, wherever in the parent's box the child's own
/// alignment puts it, and that same length as its frame. Unlike `From`, /// alignment puts it, and that same length as its frame. Unlike `From`,
/// it is a length decided from above rather than a place along a /// it is a length decided from above rather than a place along a
@@ -33,7 +32,7 @@ impl Part {
match self { match self {
Self::All => extent, Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end), Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Inset { lead, trail } => UiSpan::new(extent.start + lead, extent.end - trail), Self::Of(span) => span.within(&extent),
Self::Sized(len) => { Self::Sized(len) => {
let start = extent.start + (extent.len() - len).scale(align.rel()); let start = extent.start + (extent.len() - len).scale(align.rel());
UiSpan::new(start, start + len) UiSpan::new(start, start + len)
+22 -6
View File
@@ -223,6 +223,10 @@ impl UiRenderState {
mut old: Option<ActiveData>, mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds, LayoutHolds) { ) -> (Size, LayoutHolds, LayoutHolds) {
let old_parent = old
.as_ref()
.or_else(|| self.active.get(&id))
.and_then(|a| a.parent);
let part = info.part; let part = info.part;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
@@ -275,12 +279,9 @@ impl UiRenderState {
active.part = part; active.part = part;
active.placed = info.placed; active.placed = info.placed;
active.own_align = align; active.own_align = align;
// A subtree can be reused whole under a different parent -- same box, // The previous parent must stop owning the subtree before it can
// same layer, same region node -- and nothing in the drawing says it // undraw it, whether changing hands reused the drawing or replaced it.
// changed hands. Two things read who its parent is: a deferral, which active.parent = info.parent;
// marks whoever has it to draw, and the old parent's list of children,
// which its next draw undraws whatever is missing from.
let old_parent = std::mem::replace(&mut active.parent, info.parent);
if old_parent != info.parent if old_parent != info.parent
&& let Some(old_parent) = old_parent && let Some(old_parent) = old_parent
&& let Some(old_parent) = self.active.get_mut(&old_parent) && let Some(old_parent) = self.active.get_mut(&old_parent)
@@ -312,6 +313,9 @@ impl UiRenderState {
// Reusing its index sooner could make an old parent look current. // Reusing its index sooner could make an old parent look current.
false => (info.parent_move, extent, self.slots.remove(&id)), false => (info.parent_move, extent, self.slots.remove(&id)),
}; };
let mask_slot = old
.as_ref()
.and_then(|old| old.mask_region.map(|_| old.mask));
let old_children = old.map_or_else(Vec::new, |old| old.children); let old_children = old.map_or_else(Vec::new, |old| old.children);
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
let px = info.px; let px = info.px;
@@ -330,6 +334,7 @@ impl UiRenderState {
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None, mask_region: None,
mask_slot,
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
window_own: [Holds::ANY; 2], window_own: [Holds::ANY; 2],
@@ -363,6 +368,7 @@ impl UiRenderState {
textures, textures,
primitives, primitives,
mask_region, mask_region,
mask_slot,
extent_own, extent_own,
extent_len, extent_len,
answer_under, answer_under,
@@ -421,6 +427,9 @@ impl UiRenderState {
self.undraw_rec(*c, rsc); self.undraw_rec(*c, rsc);
} }
} }
if let Some(idx) = mask_slot {
rsc.ui_mut().masks.remove(idx);
}
if let Some(idx) = retired_move { if let Some(idx) = retired_move {
self.moves.remove(idx); self.moves.remove(idx);
} }
@@ -605,6 +614,9 @@ impl UiRenderState {
} }
return None; return None;
} }
if active.parent_mask != info.mask {
return None;
}
// Drawn somewhere else in the tree: its box is in coordinates it no // Drawn somewhere else in the tree: its box is in coordinates it no
// longer sits in, and its slot names the wrong parent. // longer sits in, and its slot names the wrong parent.
if active.parent_move != info.parent_move { if active.parent_move != info.parent_move {
@@ -826,6 +838,9 @@ impl UiRenderState {
rsc.ui_mut().masks.remove(mask); rsc.ui_mut().masks.remove(mask);
} }
} }
if undraw && active.mask_region.take().is_some() {
rsc.ui_mut().masks.remove(active.mask);
}
active.primitives.clear(); active.primitives.clear();
active.textures.clear(); active.textures.clear();
rsc.ui_mut().textures.free(); rsc.ui_mut().textures.free();
@@ -913,6 +928,7 @@ impl UiRenderState {
self.slots.clear(); self.slots.clear();
self.moves.clear(); self.moves.clear();
self.layers.clear(); self.layers.clear();
rsc.ui_mut().masks = Default::default();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
self.free(rsc); self.free(rsc);
} }
+4 -4
View File
@@ -20,10 +20,10 @@ impl Widget for Pad {
// The two stay distinct -- the box can be narrower still, where a row // The two stay distinct -- the box can be narrower still, where a row
// asked this widget in the room left, and a text wraps at that. // asked this widget in the room left, and a text wraps at that.
let inset = |lead: Px, trail: Px| { let inset = |lead: Px, trail: Px| {
Place::Within(Part::Inset { Place::Within(Part::Of(UiSpan::new(
lead: Len::from_parts(Rel::ZERO, lead), Len::from_parts(Rel::ZERO, lead),
trail: Len::from_parts(Rel::ZERO, trail), Len::from_parts(Rel::ONE, -trail),
}) )))
}; };
let place = [ let place = [
inset(self.padding.left, self.padding.right), inset(self.padding.left, self.padding.right),
+18 -43
View File
@@ -10,18 +10,13 @@ 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 room left from the cursor to the row's end, said without the // The row: this span's own box, as a length of the frame its children
// row's length: a child measured in it does not make this drawing // are laid out against. Its start is nothing's business -- a slot is
// depend on how long the row is. // a length from it -- so what this reads is the length alone.
let room_from = |cursor: Len| match self.dir.sign { let far = painter.extent_len(axis);
Sign::Pos => Part::Inset { let along = |from: Len, to: Len| match self.dir.sign {
lead: cursor, Sign::Pos => UiSpan::new(from, to),
trail: Len::ZERO, Sign::Neg => UiSpan::new(far - to, far - from),
},
Sign::Neg => Part::Inset {
lead: Len::ZERO,
trail: cursor,
},
}; };
// Across itself the child sits where its own alignment says, in the // Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and // whole of the row: a span is what contains its children there, and
@@ -33,28 +28,24 @@ impl Widget for Span {
// given whatever else is in it and wherever this child sits among // given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the // them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there. // cursor, because a text has to wrap at the width actually there.
// This is the one ask a drawn fixed child gets: its slot is its
// answer, and the drawing is moved there once the shares are known.
// A hinted child is asked once, in its slot.
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());
let mut measured = Vec::with_capacity(self.children.len()); let mut drawn_across = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let size = match painter.size_hint(child, axis) { let len = match painter.size_hint(child, axis) {
Some(len) => { Some(len) => {
measured.push(None); drawn_across.push(None);
len len
} }
None => { None => {
let room = Place::Within(room_from(cursor)); let room = Place::Within(Part::From(along(cursor, far)));
let size = painter let size = painter
.widget_at(child, [None; 2], axis.pair(room, across)) .widget_at(child, [None; 2], axis.pair(room, across))
.size(); .size();
measured.push(Some(size)); drawn_across.push(Some(size.axis(!axis)));
size.axis(axis) size.axis(axis)
} }
}; };
let len = size;
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);
@@ -71,25 +62,9 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// The row: this span's own box as a length of the window, read only
// where a slot depends on it -- shares divide what is left of it,
// and a negative row counts from its end. Reading it pins the
// drawing to this length; a positive row of fixed children is not
// pinned and holds for any length its children do. Its start is
// nothing's business: a slot is a length from it.
let far = (total.leftover > Weight::ZERO || self.dir.sign == Sign::Neg)
.then(|| painter.extent_len(axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => {
let far = far.expect("a negative row reads its length");
UiSpan::new(far - to, far - from)
}
};
// What is left for the shares to divide: the row less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the frame rather than a number of pixels. // fixed, as a length of the frame rather than a number of pixels.
// Nothing where there are no shares, and nothing reads it there. let room = far - Len::from_parts(total.rel, total.px);
let room = far.map_or(Len::ZERO, |far| 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
@@ -125,8 +100,7 @@ impl Widget for Span {
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let mut start = Len::rel_min(); let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
for ((child, len), measured) in self.children.iter().zip(&lens).zip(&measured) { for ((child, &len), &across_len) in self.children.iter().zip(&lens).zip(&drawn_across) {
let len = *len;
// A child asking for nothing but a part of what is left over, // A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for // when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows. // pixels or a fraction keeps those and overflows.
@@ -134,6 +108,7 @@ impl Widget for Span {
{ {
painter.undraw(child); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
continue; continue;
} }
let from = start; let from = start;
@@ -156,10 +131,10 @@ impl Widget for Span {
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
narrow[axis as usize] = Some(slot.len()); narrow[axis as usize] = Some(slot.len());
} }
let used = match (measured, narrow[axis as usize]) { let used = match (across_len, narrow[axis as usize]) {
(Some(size), None) => { (Some(across_len), None) => {
painter.place_at(child, place); painter.place_at(child, place);
size.axis(!axis) across_len
} }
_ => painter.widget_at(child, narrow, place).len(!axis), _ => painter.widget_at(child, narrow, place).len(!axis),
}; };
+37
View File
@@ -821,3 +821,40 @@ fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
h.set_root(root); h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450)); assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
} }
#[test]
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
for collapsed in [1, 2] {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).add(&mut h.rsc);
h.set_len(head, dir.axis, 200);
let tail = rect(Color::BLUE).add(&mut h.rsc);
let tail_len = 200 - 10 * (collapsed + 1);
h.set_len(tail, dir.axis, tail_len);
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
let mut shares = Vec::new();
for _ in 0..collapsed {
let share = rect(Color::GREEN).add(&mut h.rsc);
shares.push(share);
children.push(share.add_strong(&mut h.rsc));
}
children.push(tail.add_strong(&mut h.rsc));
h.set_root(Span {
children,
dir,
gap: Px::from_int(10),
});
for share in shares {
assert!(h.region(&share).is_none());
}
let region = h.region(&tail).unwrap();
let (from, to) = match dir.sign {
Sign::Pos => (400 - tail_len, 400),
Sign::Neg => (0, tail_len),
};
assert_eq!(region.top_left.axis(dir.axis), Px::from_int(from));
assert_eq!(region.bot_right.axis(dir.axis), Px::from_int(to));
}
}
}
+151
View File
@@ -1342,3 +1342,154 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
} }
} }
} }
struct OptionalMask {
inner: StrongWidget,
enabled: bool,
}
impl Widget for OptionalMask {
fn draw(&mut self, painter: &mut Painter) -> Size {
if self.enabled {
painter.set_mask(UiRegion::FULL);
}
painter.widget(&self.inner);
Size::LEFTOVER
}
}
fn primitive_masks(h: &Harness, id: WidgetId) -> Vec<MaskIdx> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx]
.mask_idx
})
.collect()
}
#[test]
fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::RED).height(50).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let child = Stretchy {
inner: inner.add_strong(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
let masked = child.masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(masked, node);
h.set_root((first, masked).span(Dir::DOWN));
let mask = h.render.active[&masked.id()].mask;
let settled = draws.get();
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
assert_eq!(h.render.active[&masked.id()].mask, mask);
h.set_len(first, Axis::Y, 10);
h.frame();
let clip = h.rsc.ui().masks[mask.idx()];
let clip = h
.render
.moves
.resolve(clip.move_idx, clip.region)
.to_px(h.render.output_size());
assert_eq!(clip, h.region(&masked).unwrap());
assert_corners!(h, inner, (0, 10), (400, 200));
}
}
#[test]
fn adding_and_removing_a_mask_updates_existing_primitives() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = OptionalMask {
inner: inner.add_strong(&mut h.rsc),
enabled: false,
}
.add(&mut h.rsc);
h.set_root(masked);
for enabled in [true, false, true, false] {
h.rsc[masked].enabled = enabled;
h.frame();
let mask = h.render.active[&masked.id()].mask;
assert_eq!(mask == MaskIdx::NONE, !enabled);
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
}
assert_eq!(h.rsc.ui().masks.len(), 1, "retired slots must be reusable");
}
#[test]
fn an_empty_masks_slot_is_released_when_the_mask_is_removed_or_undrawn() {
let mut h = Harness::new((400, 200));
let (inner, _) = counted(&mut h, Size::LEFTOVER, false);
let masked = OptionalMask {
inner: inner.add_strong(&mut h.rsc),
enabled: true,
}
.add(&mut h.rsc);
let row = (masked,).span(Dir::DOWN).add(&mut h.rsc);
h.set_root(row);
for _ in 0..3 {
h.rsc[masked].enabled = false;
h.frame();
h.rsc[masked].enabled = true;
h.frame();
let child = h.rsc[row].pop().unwrap();
h.frame();
h.rsc[row].push(child);
h.frame();
}
assert_eq!(h.rsc.ui().masks.len(), 1);
}
struct SharedChild(Rc<StrongWidget>);
impl Widget for SharedChild {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(self.0.as_ref()).size()
}
}
struct SwitchParent {
choices: [StrongWidget; 2],
choice: usize,
}
impl Widget for SwitchParent {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(&self.choices[self.choice]).size()
}
}
#[test]
fn a_redrawn_subtree_is_not_undrawn_by_the_parent_it_left() {
for node in [false, true] {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::RED).width(40).add(&mut h.rsc);
let held: StrongWidget = leaf.add_strong(&mut h.rsc);
let shared = Rc::new(held);
let first = SharedChild(shared.clone()).add_strong(&mut h.rsc);
let second = SharedChild(shared).add_strong(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(&second, node);
let root = SwitchParent {
choices: [first, second],
choice: 0,
}
.add(&mut h.rsc);
h.set_root(root);
let before = h.region(&leaf);
h.rsc[root].choice = 1;
h.frame();
assert_eq!(h.region(&leaf), before);
}
}
+8 -4
View File
@@ -14,7 +14,7 @@
#[path = "scenario/mod.rs"] #[path = "scenario/mod.rs"]
mod scenario; mod scenario;
use iris::random::{Edits, plan}; use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds}; use scenario::{ALL, Case, diverges, env, over_seeds};
/// How deep the generator branches. The generator widens two to four ways per /// How deep the generator branches. The generator widens two to four ways per
@@ -32,8 +32,11 @@ fn depth() -> usize {
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98]; const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
fn check(seed: u64, depth: usize, case: Case) { fn check(seed: u64, depth: usize, case: Case) {
let grown = plan(seed, depth, &Edits::default()); check_plan(&plan(seed, depth, &Edits::default()), seed, depth, case);
if let Some(how) = diverges(&grown, case, seed) { }
fn check_plan(grown: &Plan, seed: u64, depth: usize, case: Case) {
if let Some(how) = diverges(grown, case, seed) {
panic!( panic!(
"seed {seed} at depth {depth} differs after {}: {how}\n\ "seed {seed} at depth {depth} differs after {}: {how}\n\
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \ reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
@@ -119,8 +122,9 @@ fn a_long_run_of_seeds_agrees() {
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(), None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
}; };
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default());
for case in ALL { for case in ALL {
check(seed, depth, case); check_plan(&grown, seed, depth, case);
} }
}); });
} }
+6 -6
View File
@@ -444,12 +444,6 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
cold.state.root = Some(root); cold.state.root = Some(root);
cold.frame(); cold.frame();
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let mut drawn = 0; let mut drawn = 0;
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));
@@ -457,6 +451,12 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
if got == want { if got == want {
continue; continue;
} }
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
// Where two trees disagree is rarely where the cause is, so the // Where two trees disagree is rarely where the cause is, so the
// ancestry comes with it, marking the widgets that own a region. // ancestry comes with it, marking the widgets that own a region.
let mut chain = Vec::new(); let mut chain = Vec::new();