diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 946447e..a3749e3 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,6 +1,6 @@ use crate::{ LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, - Size, TextureHandle, UiRegion, WidgetId, + Size, TextureHandle, UiRegion, UiVec2, WidgetId, }; /// What is kept of a widget its parent has asked about. `drawn` says whether @@ -9,22 +9,15 @@ use crate::{ #[derive(Debug)] pub struct ActiveData { pub id: WidgetId, - /// Its frame in `parent_move`'s coordinates: what a fraction it declares - /// or reports is a fraction of, composed. Everything it draws sits inside - /// this by way of `extent`. - pub frame_abs: UiRegion, - /// Where its drawing goes, in the frame's own coordinates. + /// Where its drawing goes, in its region node's coordinates. pub extent: UiRegion, - /// That frame in its parent's frame coordinates, before composition: - /// forwarded whole by a transparent container, narrowed by a declared - /// length. Its length is the same on every ask, which is what - /// a local redraw relies on to ask its parent's own question again. - pub frame: UiRegion, - /// The length its frame was narrowed to on each axis, as a length of - /// its parent's frame: a declared rule, or a box its parent decided for - /// it. `None` forwards the parent's frame whole. Kept as a length rather - /// than a position so that every placement puts the frame back in the - /// part it is given. + /// What a fraction declared or reported under this widget is a fraction + /// of, as a length of the window. + pub frame: UiVec2, + /// A frame its parent decided for it on each axis -- a row's slot, or + /// padding's frame less its pixels -- as a length of the window. `None` + /// forwards the parent's frame. What it declared is kept separately in + /// `declared` and is a fraction of whichever of the two reached it. pub narrow: [Option; 2], /// Where its drawing was put, as a part of its parent's box, and where /// it was asked. The two differ where a container asks in one place and @@ -33,9 +26,9 @@ pub struct ActiveData { /// so a box that moved re-places every child by re-adding that start. pub place: [Place; 2], pub offer_place: [Place; 2], - /// The box it was asked in, in its frame's coordinates: the box its - /// drawing was made in and the one its contract is about. Its drawing - /// is placed elsewhere by re-expression, never by asking again there. + /// The box it was asked in, in the parent's region-node coordinates: the + /// box its drawing was made in and the one its contract is about. Its + /// drawing is placed elsewhere by re-expression, never by asking again. pub offer_part: UiRegion, /// The measured answer and its dependencies. A hint-only dependency or /// a widget first encountered during placement has no measurement yet. @@ -45,9 +38,10 @@ pub struct ActiveData { /// rests on the first answer and its drawing on the last, so only the /// parent can ask either again. pub re_asked: bool, - /// What the widget said it used of its frame, the last time it drew. + /// What the widget reported, in window-unit lengths. pub size: Size, - /// The frame and extent reads that this drawing holds for. + /// The window and extent reads that this drawing holds for, and the + /// frame and box it pinned. pub holds: LayoutHolds, pub drawn: bool, pub parent: Option, @@ -73,7 +67,8 @@ pub struct ActiveData { /// Its alignment when it was last drawn, which a change to the property /// is found against. pub own_align: RegionAlign, - /// The movable region whose coordinates `frame_abs` uses. + /// The movable region whose coordinates `extent` uses when this widget + /// does not own a region node. pub parent_move: MoveIdx, /// The mask its drawing is clipped to: one it set itself, or the one it /// inherited from whoever drew it. @@ -95,12 +90,12 @@ impl ActiveData { self.answer.map(|(size, _)| size) } - /// Whether what it answered still stands for a frame of these pixel - /// lengths. The answer was given in the box its parent first asked - /// about, which is what it is checked against -- `holds` on the record - /// is about the box the answer then chose. - pub fn answers_at(&self, px: crate::PxVec2, part: UiRegion) -> bool { + /// Whether what it answered still stands in this window, for the frame + /// and the box it was asked in. The answer was given in the box its + /// parent first asked about, which is what it is checked against -- + /// `holds` on the record is about the box the answer then chose. + pub fn answers_at(&self, window: crate::PxVec2, part: UiRegion) -> bool { self.answer - .is_some_and(|(_, holds)| holds.contains(px, part)) + .is_some_and(|(_, holds)| holds.contains(window, self.frame, part)) } } diff --git a/core/src/ui/layout_holds.rs b/core/src/ui/layout_holds.rs index ab38a96..c108685 100644 --- a/core/src/ui/layout_holds.rs +++ b/core/src/ui/layout_holds.rs @@ -1,20 +1,27 @@ -use crate::{Axis, Holds, Len, PxVec2, UiRegion}; +use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2}; const AXES: [Axis; 2] = [Axis::X, Axis::Y]; -/// What one evaluation of a widget depends on: the pixel lengths of its -/// frame and of its own box that its drawing and its answer hold for, and -/// the symbolic length of its own box where it read one. +/// What one evaluation of a widget depends on: the window lengths its reads +/// hold for, the pixel lengths of its own box, and the symbolic lengths of +/// that box and of its frame where either one is what it was expressed in. /// -/// The symbolic length is a pin rather than a range: a container places its +/// The symbolic lengths are pins rather than ranges: a container places its /// children as lengths of its frame measured from where its own box starts, /// so what it draws turns on that box's length and on nothing about where it -/// is. It reaches the parent only where the box it pinned is the parent's -/// own; anywhere else the parent chose that length itself, and a widget -/// pinned this way is checked when it is re-placed. +/// is. A box pin reaches the parent only where the box it pinned is the +/// parent's own; anywhere else the parent chose that length itself, and a +/// widget pinned this way is checked when it is re-placed. +/// +/// A frame pin says the answer or the drawing is a fraction of the frame, +/// which is a different length wherever the frame is a different one -- at +/// the same window size, so no range of window pixels can say it. A length +/// of the frame that is only pixels is not one: it is that many pixels +/// whatever the frame turns out to be. #[derive(Clone, Copy, Debug, PartialEq)] pub struct LayoutHolds { pub frame: [Holds; 2], + pub frame_len: [Option; 2], pub extent: [Holds; 2], pub extent_len: [Option; 2], } @@ -22,6 +29,7 @@ pub struct LayoutHolds { impl LayoutHolds { pub const ANY: Self = Self { frame: [Holds::ANY; 2], + frame_len: [None; 2], extent: [Holds::ANY; 2], extent_len: [None; 2], }; @@ -36,7 +44,13 @@ impl LayoutHolds { || other.extent_len[n].is_none() || self.extent_len[n] == other.extent_len[n] ); + debug_assert!( + self.frame_len[n].is_none() + || other.frame_len[n].is_none() + || self.frame_len[n] == other.frame_len[n] + ); result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]); + result.frame_len[n] = self.frame_len[n].or(other.frame_len[n]); } result } @@ -48,15 +62,17 @@ impl LayoutHolds { && self.extent[n].lo <= other.extent[n].lo && self.extent[n].hi >= other.extent[n].hi && self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len)) + && self.frame_len[n].is_none_or(|len| other.frame_len[n] == Some(len)) }) } - pub fn contains(self, px: PxVec2, extent: UiRegion) -> bool { + pub fn contains(self, window: PxVec2, frame: UiVec2, extent: UiRegion) -> bool { AXES.into_iter().all(|axis| { let n = axis as usize; let len = extent.axis(axis).len(); - self.frame[n].contains(px.axis(axis)) - && self.extent[n].contains(len.to_px(px.axis(axis))) + self.frame[n].contains(window.axis(axis)) + && self.frame_len[n].is_none_or(|pinned| pinned == frame.axis(axis)) + && self.extent[n].contains(len.to_px(window.axis(axis))) && self.extent_len[n].is_none_or(|pinned| pinned == len) }) } diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 3b306c8..0859325 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -17,26 +17,20 @@ pub struct Painter<'a> { pub(super) state: &'a mut UiRenderState, pub(super) rsc: &'a mut dyn UiRsc, - /// What a fraction this widget declares or reports is a fraction of, in - /// the coordinates of `move_idx`: forwarded from its parent unchanged - /// through a span, a stack or a scroll, and narrowed only by what was - /// decided above it -- a declared length, or the root. Its length - /// is the same on every ask of the widget, which is what keeps a fraction - /// under it from being resolved twice. - pub(super) frame: UiRegion, - /// Where this widget's drawing goes, in the frame's own coordinates. - /// Everything it writes is in these coordinates, and its children are - /// placed as parts of it. + /// This widget's frame, per axis: a length of the window, and what a + /// fraction it or anything under it declares or reports is a fraction + /// of. A length rather than a box, so padding can take from both the + /// frame and the box without either becoming the other. + pub(super) frame: UiVec2, + /// Where this widget's drawing goes, in its region node's coordinates. pub(super) extent: UiRegion, /// The extent's symbolic length where this draw read it, which makes the /// drawing one that holds for that length alone -- the way reading a /// length in pixels makes it hold for that number of pixels. pub(super) extent_len: [Option; 2], - /// The frame in pixels, which its children's frames are a length of: - /// threaded down rather than composed back up the chain, so every length - /// in layout is one multiply from its parent's and [`Holds::through`] - /// inverts exactly that. - pub(super) px: PxVec2, + /// The window in pixels. Frames and boxes become pixels against this one + /// unit, regardless of region-node boundaries. + pub(super) window: PxVec2, pub(super) mask: MaskIdx, pub(super) textures: Vec, pub(super) primitives: Vec, @@ -46,10 +40,13 @@ pub struct Painter<'a> { pub(super) children: Vec, /// The children whose size this widget read while drawing. pub(super) size_deps: Vec, - /// What this draw itself read of its frame in pixels, per axis: every - /// length until it reads one, then that one, unless it says otherwise. + /// What this draw itself read of the window in pixels, per axis: every + /// window until it reads one, then that one, unless it says otherwise. pub(super) frame_own: [Holds; 2], - /// The same for its own box. + /// Its frame's symbolic length where this draw read it, which makes the + /// drawing one that holds for that frame alone. + pub(super) frame_own_len: [Option; 2], + /// The window reads' equivalent for its own box. pub(super) extent_own: [Holds; 2], /// What each child's drawing depends on. Asking a child again replaces /// its drawing, so it replaces this too rather than narrowing it. @@ -76,12 +73,10 @@ impl<'a> Painter<'a> { self.write_resolved(kind, primitive, region, self.resolve(region)); } - /// A box in this widget's extent coordinates, composed into the - /// coordinates its move slot is in: through the extent, then through the - /// frame the extent is a part of. The same two steps a recomposition - /// replays, so a moved drawing lands where a cold one does. + /// A box in this widget's extent coordinates, composed into its region + /// node's coordinates. fn resolve(&self, region: UiRegion) -> UiRegion { - region.within(&self.extent).within(&self.frame) + region.within(&self.extent) } fn write_resolved( @@ -173,27 +168,19 @@ impl<'a> Painter<'a> { let region_node = self.rsc.widgets().is_region_node(id.id()); let declared = self.declared_lens(id); let align = self.rsc.widgets().alignment(id.id()); - let declared_narrow = narrowed_by(declared); - let narrow = [ - declared_narrow[0].or(narrow[0]), - declared_narrow[1].or(narrow[1]), - ]; - let (local, extent) = frame_and_extent(part_of(self.extent, place), narrow, align); - let within = match local == UiRegion::FULL { - true => self.frame, - false => local.within(&self.frame), - }; + let (frame, extent) = + frame_and_extent(self.extent, self.frame, place, narrow, declared, align); #[cfg(feature = "layout-diagnostics")] if region_node { diag::bump(Counter::RegionNodeDraws); - diag::region_node(id.id(), self.id, within); + diag::region_node(id.id(), self.id, extent); } // A child listed twice would be moved twice. let re_asked = self.children.contains(&id.id()); if !re_asked { self.children.push(id.id()); } - let px = local.size().to_px(self.px); + let px = frame.to_px(self.window); let (size, answer_holds, holds) = self.state.draw_inner( id.id(), DrawInfo { @@ -203,8 +190,7 @@ impl<'a> Painter<'a> { parent_move: self.move_idx, region_node, mask: self.mask, - frame: local, - frame_abs: within, + frame, part: extent, place, offer_place: place, @@ -215,9 +201,8 @@ impl<'a> Painter<'a> { None, self.rsc, ); - let own = self.extent; - let compose = |holds| in_parent(holds, local, extent, place, narrow, own); - let holds = compose(holds); + let holds = self.in_parent(holds, extent, place, narrow, declared); + let answer_holds = self.in_parent(answer_holds, extent, place, narrow, declared); match self.under.iter_mut().find(|(child, _)| *child == id.id()) { Some((_, kept)) => *kept = holds, None => self.under.push((id.id(), holds)), @@ -225,8 +210,8 @@ impl<'a> Painter<'a> { DrawResult { child: id, painter: self, - size: in_parent_frame(size, local.size(), declared), - answer_holds: compose(answer_holds), + size, + answer_holds, } } @@ -258,8 +243,8 @@ impl<'a> Painter<'a> { Placing { id: self.id, extent: self.extent, - local: self.frame, - px: self.px, + frame: self.frame, + window: self.window, depth: self.depth, move_idx: self.move_idx, mask: self.mask, @@ -367,6 +352,17 @@ impl<'a> Painter<'a> { len } + /// The symbolic length of this widget's frame along one axis: what a + /// fraction it or anything under it declares is a fraction of. A + /// container reads it to hand a length of it down -- padding, which + /// takes its pixels off. Reading it pins the drawing to that frame, the + /// way [`Self::extent_len`] pins it to the box. + pub fn frame_len(&mut self, axis: Axis) -> Len { + let len = self.frame.axis(axis); + self.frame_own_len[axis as usize] = Some(len); + len + } + /// 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 /// the way the box around it would have placed the widget. @@ -402,7 +398,7 @@ impl<'a> Painter<'a> { /// [`Self::px_size`] when the other axis cannot affect the drawing. pub fn px_len(&mut self, axis: Axis) -> Px { let part = self.extent.axis(axis).len(); - let len = part.to_px(self.px.axis(axis)); + let len = part.to_px(self.window.axis(axis)); let own = &mut self.extent_own[axis as usize]; if *own == Holds::ANY { *own = Holds::at(len); @@ -418,7 +414,7 @@ impl<'a> Painter<'a> { let part = self.extent.axis(axis).len(); let holds = holds.into(); debug_assert!( - holds.contains(part.to_px(self.px.axis(axis))), + holds.contains(part.to_px(self.window.axis(axis))), "'{}' ({:?}) says its drawing holds for lengths that leave out its own box", self.label(), self.id @@ -426,26 +422,19 @@ impl<'a> Painter<'a> { self.extent_own[axis as usize] = holds; } - /// One axis of this widget's frame 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 frame_px_len(&mut self, axis: Axis) -> Px { - let len = self.px.axis(axis); - let own = &mut self.frame_own[axis as usize]; - if *own == Holds::ANY { - *own = Holds::at(len); - } - len + /// One window axis in pixels. Every length in layout is a length of the + /// window, so this is what one becomes pixels against. + pub fn window_px_len(&self, axis: Axis) -> Px { + self.window.axis(axis) } - /// [`Self::holds`] stated about the frame rather than about this - /// widget's own box, for a container whose drawing turns on what its - /// fractions are of rather than on the part of it it took. - pub fn frame_holds(&mut self, axis: Axis, holds: impl Into) { + /// A validity range already stated about the window. Containers use + /// this after branching on a window-unit length. + pub fn window_holds(&mut self, axis: Axis, holds: impl Into) { let holds = holds.into(); debug_assert!( - holds.contains(self.px.axis(axis)), - "'{}' ({:?}) says its drawing holds for lengths that leave out its frame", + holds.contains(self.window.axis(axis)), + "'{}' ({:?}) says its drawing holds for windows that leave out this one", self.label(), self.id ); @@ -537,81 +526,77 @@ impl PrimitiveLike for &TextureHandle { } } -/// What a child depends on, said about the boxes the widget that drew it -/// has rather than the ones the child was given. -/// -/// `frame` is the child's frame in this widget's frame coordinates and -/// `extent` the box it was given, in the child's own frame coordinates. Both -/// reach it as one length, so what it holds for maps back through that -/// length exactly -- and where the box it was given is this widget's own, -/// what it says about that box is what this widget can say about its own. -/// `own` is this widget's own box, for a pin that cannot be said exactly. -pub(crate) fn in_parent( - holds: LayoutHolds, - frame: UiRegion, - extent: UiRegion, - place: [Place; 2], - narrow: [Option; 2], - own: UiRegion, -) -> LayoutHolds { - let mut result = LayoutHolds::ANY; - for axis in AXES { - let n = axis as usize; - let frame_len = frame.axis(axis).len(); - result.frame[n] = holds.frame[n].through(frame_len); - match (place[n].part(), narrow[n]) { - // Its box is this widget's own, or a part of it in that box's - // own lengths: so what it holds for is a range on this widget's - // own box, which is what lets that box move without a redraw. A - // length it pinned is this widget's length wherever the part is - // the whole of it, and pins the same way. - (Part::All, None) => { - result.extent[n] = holds.extent[n]; - result.extent_len[n] = holds.extent_len[n]; - } - // Its box is a part of this widget's own box, in that box's own - // lengths, so what it holds for maps back through that part into - // a range on this widget's box. A length it pinned is this - // widget's length less the part's pixels where the part is the - // whole of the box less pixels, which is the one shape that - // inverts exactly; any other part pins this widget's own length. - (Part::Of(span), None) => { - let part_len = span.len(); - result.extent[n] = holds.extent[n].through(part_len); - result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel { - Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px), - _ => own.axis(axis).len(), - }); - } - // Its box is a part of this widget's frame, or a length of it - // decided here: a length of the frame is all that reaches it, - // so what it holds for is a range on the frame and none of it - // on this widget's own box. - _ => { - result.frame[n] = result.frame[n].and( - holds.extent[n] - .through(extent.axis(axis).len()) - .through(frame_len), - ); +/// Moves what a child depends on into this widget's own terms: this +/// method's `impl` block is where a `Painter`'s own boxes are, so it takes +/// only what the child was asked with. +impl Painter<'_> { + /// Frame ranges are already ranges on the window and combine directly. + /// A frame pin becomes this widget's own frame wherever a length of it + /// is what reached the child; where only pixels did, no length of this + /// frame can change the child's and the pin stops here. + /// + /// Extent validity maps back through the part of this widget's box, + /// where the box the child was asked in is that part; a declared length + /// places the box inside the part instead, and then only that length + /// reaches the child. A narrowed frame is not one of these: it decides + /// what fractions under the child mean and leaves the box the part it + /// was given. + fn in_parent( + &self, + holds: LayoutHolds, + extent: UiRegion, + place: [Place; 2], + narrow: [Option; 2], + declared: [Option; 2], + ) -> LayoutHolds { + let mut result = LayoutHolds::ANY; + for axis in AXES { + let n = axis as usize; + // Every frame range is already a range on the window: the + // widget's own read converted through its frame exactly once. + result.frame[n] = holds.frame[n]; + let reaches = narrow[n].is_none() + && !matches!(place[n].part(), Part::Sized(_)) + && declared[n].is_none_or(|len| len.rel != Rel::ZERO); + result.frame_len[n] = holds.frame_len[n].and(reaches.then(|| self.frame.axis(axis))); + match (place[n].part(), declared[n].is_some()) { + // Its box is this widget's own, or a part of it in that + // box's own lengths: so what it holds for is a range on this + // widget's own box, which is what lets that box move without + // a redraw. A length it pinned is this widget's length + // wherever the part is the whole of it, and pins the same + // way. + (Part::All, false) => { + result.extent[n] = holds.extent[n]; + result.extent_len[n] = holds.extent_len[n]; + } + // Its box is a part of this widget's own box, in that box's + // own lengths, so what it holds for maps back through that + // part into a range on this widget's box. A length it pinned + // is this widget's length less the part's pixels where the + // part is the whole of the box less pixels, which is the one + // shape that inverts exactly; any other part pins this + // widget's own length. + (Part::Of(span), false) => { + let part_len = span.len(); + result.extent[n] = holds.extent[n].through(part_len); + result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel { + Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px), + _ => self.extent.axis(axis).len(), + }); + } + // Its box is a part of this widget's frame, or a length of + // it decided here: a length of the frame is all that reaches + // it, so what it holds for is a range on the frame and none + // of it on this widget's own box. + _ => { + result.frame[n] = + result.frame[n].and(holds.extent[n].through(extent.axis(axis).len())); + } } } + result } - result -} - -/// A child's answer as lengths of the parent's own region. A widget reports -/// a fraction of its own region, and `of` is that region as a length of this -/// one. Pixels come through untouched, being that many pixels wherever they -/// end up. A declared axis is already the parent's: it resolved the rule in -/// its own region, and the rule is what the report says. -fn in_parent_frame(size: Size, of: UiVec2, declared: [Option; 2]) -> Size { - let mut size = size; - for (axis, declared) in AXES.into_iter().zip(declared) { - if declared.is_none() { - *size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis)); - } - } - size } /// What a widget declares a length of its box to be. `leftover` is not one: a @@ -676,49 +661,58 @@ pub(crate) fn placed_extent( placed } -/// The part of a widget's own box a `place` names, in the coordinates that -/// box is in. -pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion { - let mut part = extent; - for axis in AXES { - *part.axis_mut(axis) = place[axis as usize].part().of(*extent.axis(axis)); - } - part -} - -/// The length a rule gives a child's frame, per axis: a fraction in it is a -/// fraction of the frame the child was given, which is the one length the -/// rule can mean. -pub(crate) fn narrowed_by(declared: [Option; 2]) -> [Option; 2] { - declared.map(|declared| declared.map(|len| Len::from_parts(len.rel, len.px))) -} - -/// The frame a child is asked in and the box it is asked in, both in the -/// coordinates of the widget asking. +/// The frame length and the box a child is asked in, in the coordinates the +/// widget asking draws in. /// -/// `part` is what of the caller's own box the child is given. `narrow` is a -/// length decided for the child's frame -- a rule, a share, a box a sibling -/// decided -- which makes the frame the box the child is asked in: that -/// length is what decided where it goes, placed in the part by the child's -/// alignment. Where nothing narrowed it, the frame is the caller's own and -/// the part is the box. -/// -/// A length rather than a position, so that a child placed again is put back -/// in whatever part it is given rather than where it first was. +/// `own` is that widget's own box, and `place` what of it the child is +/// given. `narrow` is a frame the container decided for the child -- a row's +/// slot, or padding's frame less its pixels -- and [`Part::Sized`] one a +/// sibling's answer decided; both are window lengths, like every other +/// length here, since a slot of a row is not a fraction of anything the row +/// can name. The child's declaration is a fraction of whichever reached it, +/// and is the only one of the three that also places the box: a box the +/// caller decided is what `place` names. pub(crate) fn frame_and_extent( - part: UiRegion, + own: UiRegion, + parent_frame: UiVec2, + place: [Place; 2], narrow: [Option; 2], + declared: [Option; 2], align: RegionAlign, -) -> (UiRegion, UiRegion) { - let mut frame = UiRegion::FULL; +) -> (UiVec2, UiRegion) { + let part = part_of(own, place, align); + let mut frame = parent_frame; let mut extent = part; - for (axis, narrow) in AXES.into_iter().zip(narrow) { - if let Some(len) = narrow { + for axis in AXES { + let n = axis as usize; + let sized = match place[n].part() { + Part::Sized(len) => Some(len), + _ => None, + }; + let base = sized + .or(narrow[n]) + .unwrap_or_else(|| parent_frame.axis(axis)); + let len = declared[n] + .map(|len| Len::from_parts(len.rel, len.px).within_len(base)) + .unwrap_or(base); + *frame.axis_mut(axis) = len; + if declared[n].is_some() { let slot = part.axis(axis); let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel()); - *frame.axis_mut(axis) = UiSpan::new(start, start + len); - *extent.axis_mut(axis) = UiSpan::FULL; + *extent.axis_mut(axis) = UiSpan::new(start, start + len); } } (frame, extent) } + +/// The part of a widget's own box a `place` names, in the coordinates that +/// box is in. +fn part_of(extent: UiRegion, place: [Place; 2], align: RegionAlign) -> UiRegion { + let mut part = extent; + for axis in AXES { + *part.axis_mut(axis) = place[axis as usize] + .part() + .of(*extent.axis(axis), align.axis(axis)); + } + part +} diff --git a/core/src/ui/place.rs b/core/src/ui/place.rs index c292716..04821c9 100644 --- a/core/src/ui/place.rs +++ b/core/src/ui/place.rs @@ -1,4 +1,4 @@ -use crate::{PrimitiveHandle, UiRegion, UiSpan}; +use crate::{AxisAlign, Len, PrimitiveHandle, UiRegion, UiSpan}; /// What of a widget's own box a child is given, along one axis. #[derive(Clone, Copy, Debug, PartialEq)] @@ -16,15 +16,25 @@ pub enum Part { /// container read its own box -- and a box chosen from its own answer /// then feeds back into the answer. Of(UiSpan), + /// 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`, + /// it is a length decided from above rather than a place along a + /// container's cursor -- what a stack's sizing child decides for the + /// rest. + Sized(Len), } impl Part { /// Where it lands in the coordinates `extent` is in. - pub(crate) fn of(self, extent: UiSpan) -> UiSpan { + pub(crate) fn of(self, extent: UiSpan, align: AxisAlign) -> UiSpan { match self { Self::All => extent, Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end), Self::Of(span) => span.within(&extent), + Self::Sized(len) => { + let start = extent.start + (extent.len() - len).scale(align.rel()); + UiSpan::new(start, start + len) + } } } } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index de68a80..42fef88 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,10 +1,10 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; -use crate::ui::painter::{declared_lens, frame_and_extent, narrowed_by, part_of, placed_extent}; +use crate::ui::painter::{declared_lens, frame_and_extent, placed_extent}; use crate::{ ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, - Moves, Painter, Part, PixelRegion, Place, PxVec2, Size, StrongWidget, UiRegion, UiRsc, Weight, - WidgetId, Widgets, + Moves, Painter, Part, PixelRegion, Place, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, + UiSpan, UiVec2, Weight, WidgetId, Widgets, util::{HashMap, Vec2}, }; @@ -20,28 +20,23 @@ pub(super) struct DrawInfo { pub parent_move: MoveIdx, pub region_node: bool, pub mask: MaskIdx, - /// The frame in the parent widget's frame coordinates, before - /// composition. Its length is the same on every ask of the widget. - pub frame: UiRegion, - /// That frame composed into `parent_move`'s coordinates, which is what - /// the widget's own drawing is written within. - pub frame_abs: UiRegion, - /// The box the widget is asked in, in the frame's own coordinates: the - /// part of the parent's own box that `place` names, before the widget's - /// answer is placed inside it. + /// What a fraction declared or reported under this widget is a fraction + /// of, as a length of the window. + pub frame: UiVec2, + /// The box the widget is asked in, in its parent region node's + /// coordinates. pub part: UiRegion, /// Where the widget is put, and where it was asked, as parts of the /// parent's box. See [`Place`]. The two are one ask's place until the /// parent places the answer somewhere else. pub place: [Place; 2], pub offer_place: [Place; 2], - /// The length the frame was narrowed to on each axis, as a length of - /// the parent's frame, where anything narrowed it. + /// A frame the parent decided for it on each axis, as a length of the + /// window, which the widget's own declaration is a fraction of. pub narrow: [Option; 2], /// Whether the parent already asked about this widget in this draw. pub re_asked: bool, - /// The frame in pixels: one multiply from the parent's own, which is - /// where every pixel length in layout comes from. + /// The frame in pixels, resolved once against the window. pub px: PxVec2, } @@ -58,10 +53,8 @@ impl DrawInfo { pub(super) struct Placing { pub id: WidgetId, pub extent: UiRegion, - /// The widget's frame in the coordinates its children compose within: - /// `FULL` where it is a region node, since its box is that node. - pub local: UiRegion, - pub px: PxVec2, + pub frame: UiVec2, + pub window: PxVec2, pub depth: usize, pub move_idx: MoveIdx, pub mask: MaskIdx, @@ -126,23 +119,20 @@ impl UiRenderState { self.resized = true; let Some(root) = self.old_root else { return }; let stands = self.active.get(&root).is_some_and(|active| { - let px = active.frame.size().to_px(size); // Nothing above the root chose anything, so the box it was first // asked about is the whole of its frame. - active.answers_at(px, active.offer_part) && active.holds.contains(px, active.offer_part) + active.answers_at(size, active.offer_part) + && active.holds.contains(size, active.frame, active.offer_part) }); if !stands { widgets.needs_redraw.insert(root); } } - /// The root is asked about in the output: the window is where a fraction - /// becomes pixels rather than a box of its own, so the root's box is the - /// first length threaded down. Its own rules narrow that box, and where - /// they do the narrowed box is also the offer -- nothing above it chose - /// anything else. - fn root_info(&self, region: UiRegion) -> DrawInfo { - let px = region.size().to_px(self.output_size); + /// The root is asked about in the output. Its own rules narrow both its + /// frame and box; nothing above it chose a different one. + fn root_info(&self, frame: UiVec2, extent: UiRegion) -> DrawInfo { + let px = frame.to_px(self.output_size); DrawInfo { layer: 0, parent: None, @@ -150,9 +140,8 @@ impl UiRenderState { parent_move: MoveIdx::NONE, region_node: false, mask: MaskIdx::NONE, - frame: region, - frame_abs: region, - part: UiRegion::FULL, + frame, + part: extent, place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], narrow: [None; 2], @@ -203,17 +192,24 @@ impl UiRenderState { let _layout = diag::timer(TimerKind::FullLayout); self.clear(rsc); if let Some(id) = root { - let region = Self::root_region(id.id(), rsc.widgets()); - let info = self.root_info(region); + let (frame, extent) = Self::root_layout(id.id(), rsc.widgets()); + let info = self.root_info(frame, extent); self.draw_inner(id.id(), info, None, rsc); } } - /// The root's frame: the window, narrowed by the root's own rules. Its - /// extent is that frame, since nothing above it chose anything else. - fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { - let narrow = narrowed_by(declared_lens(widgets, id)); - frame_and_extent(UiRegion::FULL, narrow, widgets.alignment(id)).0 + /// The root's frame and box: the window, taken in by the root's own + /// rules. Nothing above it narrowed anything or chose where it goes, so + /// its declaration is the whole of what decides either. + fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) { + frame_and_extent( + UiRegion::FULL, + UiVec2::FULL_SIZE, + [Place::Within(Part::All); 2], + [None; 2], + declared_lens(widgets, id), + widgets.alignment(id), + ) } pub(super) fn draw_inner( @@ -223,11 +219,11 @@ impl UiRenderState { mut old: Option, rsc: &mut dyn UiRsc, ) -> (Size, LayoutHolds, LayoutHolds) { - let (frame, part) = (info.frame_abs, info.part); + let part = info.part; #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); - diag::draw_request(id, info.parent, frame, info.px, info.region_node); + diag::draw_request(id, info.parent, part, info.px, info.region_node); } let align = rsc.widgets().alignment(id); let declared = declared_lens(rsc.widgets(), id); @@ -245,8 +241,7 @@ impl UiRenderState { .flatten() .and_then(|answer| { let extent = placed_extent(part, answer.0, declared, info.fill(), align); - self.try_reuse(id, frame, part, extent, info, rsc) - .map(|()| answer) + self.try_reuse(id, part, extent, info, rsc).map(|()| answer) }); let answer = reused.unwrap_or_else(|| { if old.is_none() { @@ -258,7 +253,7 @@ impl UiRenderState { // open. let extent = placed_extent(part, answer.0, declared, info.fill(), align); if extent != part { - self.reposition(id, frame, extent, info, rsc); + self.relocate(id, extent, info, rsc); } answer }); @@ -268,7 +263,6 @@ impl UiRenderState { // Whoever asked owns how the boxes were reached: the frame it stated, // and what of its own box it asked in. A local redraw asks the same // question again from these. - active.frame_abs = frame; active.frame = info.frame; active.narrow = info.narrow; active.re_asked = info.re_asked; @@ -301,29 +295,30 @@ impl UiRenderState { old: Option, rsc: &mut dyn UiRsc, ) -> (Size, LayoutHolds) { - let frame = info.frame_abs; - let (move_idx, local, retired_move) = match info.region_node { - // Its box becomes its movable region, so it draws in that - // region's coordinates and its box is one entry to rewrite. + let frame = info.frame; + let (move_idx, extent, retired_move) = match info.region_node { + // A node entry is only a translation. Its local box keeps the + // same window-unit length as the box in its parent's node. true => ( - self.move_slot(id, info.parent_move, frame), - UiRegion::FULL, + self.move_slot(id, info.parent_move, translation(extent)), + local_region(extent), None, ), // Keep the old entry alive until every descendant has migrated. // Reusing its index sooner could make an old parent look current. - false => (info.parent_move, frame, self.slots.remove(&id)), + false => (info.parent_move, extent, self.slots.remove(&id)), }; let old_children = old.map_or_else(Vec::new, |old| old.children); rsc.widgets_mut().needs_redraw.remove(&id); let px = info.px; + let window = self.output_size; let mut painter = Painter { state: self, - frame: local, + frame, extent, extent_len: [None; 2], - px, + window, mask: info.mask, layer: info.layer, own_layer: info.layer, @@ -334,6 +329,7 @@ impl UiRenderState { children: Vec::new(), size_deps: Vec::new(), frame_own: [Holds::ANY; 2], + frame_own_len: [None; 2], under: Vec::new(), extent_own: [Holds::ANY; 2], answer_under: LayoutHolds::ANY, @@ -358,7 +354,7 @@ impl UiRenderState { rsc: _, frame: _, extent: _, - px: _, + window: _, mask, textures, primitives, @@ -369,6 +365,7 @@ impl UiRenderState { children, size_deps, frame_own, + frame_own_len, under, move_idx, layer, @@ -384,11 +381,23 @@ impl UiRenderState { ); // A rule wins on the axis it names, and the draw answers the rest. // Applied here so it is one place rather than every widget that could - // carry one, and so the widget under a rule never learns of it. + // carry one, and so the widget under a rule never learns of it. The + // frame is the answer where the rule gave a length outright: it was + // resolved into the frame when the child was asked, and resolving it + // again here would take the fraction of a fraction. let rules = rsc.widgets().size_rules(id); + let ruled = |axis: Axis, reported: LayoutLen| match rules.axis(axis).exact() { + None => reported, + Some(len) if len.leftover == Weight::ZERO => LayoutLen { + rel: info.frame.axis(axis).rel, + px: info.frame.axis(axis).px, + leftover: Weight::ZERO, + }, + Some(len) => len.within_len(info.frame.axis(axis)), + }; let size = Size { - x: rules.x.apply(size.x), - y: rules.y.apply(size.y), + x: ruled(Axis::X, size.x), + y: ruled(Axis::Y, size.y), }; // A widget that clipped its contents to its box drew nothing outside // it, so reporting more than the box asks to be placed at a length it @@ -396,7 +405,10 @@ impl UiRenderState { // Overflowing is otherwise ordinary: a text too tall for the box it // was offered reports the height it needs. debug_assert!( - mask == info.mask || AXES.into_iter().all(|axis| within_box(size, px, axis)), + mask == info.mask + || AXES + .into_iter() + .all(|axis| within_box(size, extent, self.output_size, axis)), "'{}' ({id:?}) clips to {px:?} and reports {size}", rsc.widgets().label(id), ); @@ -408,8 +420,23 @@ impl UiRenderState { if let Some(idx) = retired_move { self.moves.remove(idx); } + // A rule that is a fraction of the frame is answered with the + // frame's own length, so the answer is that frame's and not just + // that many pixels of this window -- the same pin a widget that read + // its frame took for its drawing. + let frame_len = AXES.map(|axis| { + let fraction = rules + .axis(axis) + .exact() + .is_some_and(|len| len.rel != Rel::ZERO); + match fraction { + true => Some(info.frame.axis(axis)), + false => frame_own_len[axis as usize], + } + }); let own_holds = LayoutHolds { frame: frame_own, + frame_len, extent: extent_own, extent_len, }; @@ -418,7 +445,7 @@ impl UiRenderState { .into_iter() .fold(answer_holds, |holds, (_, child)| holds.and(child)); debug_assert!( - holds.contains(px, extent), + holds.contains(self.output_size, info.frame, extent), "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", rsc.widgets().label(id), ); @@ -436,8 +463,7 @@ impl UiRenderState { parent_move: move_idx, region_node: false, mask, - frame: UiRegion::FULL, - frame_abs: UiRegion::FULL, + frame: UiVec2::FULL_SIZE, part: UiRegion::FULL, place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], @@ -453,7 +479,6 @@ impl UiRenderState { let active = ActiveData { id, - frame_abs: frame, extent, frame: info.frame, narrow: info.narrow, @@ -525,26 +550,10 @@ impl UiRenderState { return None; } let answer = active.answer?; - answer.1.contains(info.px, part).then_some(answer) - } - - /// The pixel lengths of a widget's frame, which is what a local redraw - /// needs to ask the question its parent asked. - /// - /// It is threaded down from the window a length of a box at a time, and - /// this takes the same steps back up: a widget's frame is a length of its - /// parent's frame, and that chain has no coordinate frame in it, so a - /// region node cannot break it -- and it lands on the number a cold - /// layout computes rather than near it. - fn asked_px(&self, id: WidgetId) -> PxVec2 { - let active = &self.active[&id]; - // Nothing above the root: the window is where a fraction becomes - // pixels, which is also the whole of the frame the root is given. - let parent_px = match active.parent.and_then(|p| self.active.get(&p)) { - Some(parent) => self.asked_px(parent.id), - None => self.output_size, - }; - active.frame.size().to_px(parent_px) + answer + .1 + .contains(self.output_size, info.frame, part) + .then_some(answer) } /// Keeps the retained drawing if its contract holds for `part`, the box @@ -552,7 +561,6 @@ impl UiRenderState { fn try_reuse( &mut self, id: WidgetId, - frame: UiRegion, part: UiRegion, extent: UiRegion, info: DrawInfo, @@ -603,10 +611,10 @@ impl UiRenderState { } return None; } - // In pixels, because the frame is a fraction of its parent's and - // that may be what changed -- an unchanged fraction of a box half the + // In pixels, because the box is a fraction of the window and that + // may be what changed -- an unchanged fraction of a window half the // size is half the widget. - if !active.holds.contains(info.px, part) { + if !active.holds.contains(self.output_size, info.frame, part) { #[cfg(feature = "layout-diagnostics")] { // Which of the three said no, so a frame that redraws more @@ -618,10 +626,14 @@ impl UiRenderState { if holds.extent_len[n].is_some_and(|pinned| pinned != part.axis(axis).len()) { diag::bump(Counter::OutsidePinnedLen); } - if !holds.frame[n].contains(info.px.axis(axis)) { + if !holds.frame[n].contains(self.output_size.axis(axis)) + || holds.frame_len[n].is_some_and(|pinned| pinned != info.frame.axis(axis)) + { diag::bump(Counter::OutsideFrame); } - if !holds.extent[n].contains(part.axis(axis).len().to_px(info.px.axis(axis))) { + if !holds.extent[n] + .contains(part.axis(axis).len().to_px(self.output_size.axis(axis))) + { diag::bump(Counter::OutsideExtent); } } @@ -630,21 +642,14 @@ impl UiRenderState { } return None; } - self.relocate(id, frame, extent, info, rsc); + self.relocate(id, extent, info, rsc); Some(()) } /// Puts a retained drawing where its parent now has it, without drawing: - /// a moved frame recomposes the subtree or rewrites its node, and a box - /// of another length re-expresses everything inside it. - fn relocate( - &mut self, - id: WidgetId, - frame: UiRegion, - extent: UiRegion, - info: DrawInfo, - rsc: &mut dyn UiRsc, - ) { + /// a widget with a node of its own writes that node's translation, and + /// one without re-expresses its own drawing and everything inside it. + fn relocate(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { let active = &self.active[&id]; debug_assert!( !rsc.widgets().needs_redraw.contains(&id), @@ -652,22 +657,20 @@ impl UiRenderState { rsc.widgets().label(id) ); let has_region_node = active.move_idx != active.parent_move; - let extent_moved = active.extent != extent; - let moved = active.frame_abs != frame; + let local = match has_region_node { + true => local_region(extent), + false => extent, + }; + let moved = active.extent != local; let slot = active.move_idx; - if moved { - if has_region_node { - self.moves.set(slot, frame); - } else { - self.recompose_subtree(id, frame, info.parent_move, rsc); - } + if has_region_node { + self.moves.set(slot, translation(extent)); } - if extent_moved { - self.reposition(id, frame, extent, info, rsc); + if moved { + self.reposition(id, local, info, rsc); } self.redepth(id, info.depth); let active = self.active.get_mut(&id).unwrap(); - active.frame_abs = frame; active.frame = info.frame; active.place = info.place; #[cfg(feature = "layout-diagnostics")] @@ -709,7 +712,7 @@ impl UiRenderState { rsc: &mut dyn UiRsc, ) { let active = &self.active[&child]; - let (frame, part) = Self::re_ask(active, at.extent, place); + let (frame, part) = Self::ask_again(active, at, place); let extent = placed_extent( part, active.measured().unwrap_or(active.size), @@ -725,63 +728,50 @@ impl UiRenderState { region_node: active.move_idx != active.parent_move, mask: at.mask, frame, - frame_abs: frame.within(&at.local), part, place, offer_place: active.offer_place, narrow: active.narrow, re_asked: active.re_asked, - px: frame.size().to_px(at.px), + px: frame.to_px(at.window), }; - self.relocate(child, info.frame_abs, extent, info, rsc); + self.relocate(child, extent, info, rsc); } - /// The frame and the box a widget is given at `place` of its parent's - /// box, from what it already has. A frame's length is the same on every - /// ask, so a narrowed frame is put back where it sits in the part rather - /// than resolved a second time. - fn re_ask( - active: &ActiveData, - parent_box: UiRegion, - place: [Place; 2], - ) -> (UiRegion, UiRegion) { - frame_and_extent(part_of(parent_box, place), active.narrow, active.own_align) + /// The frame and the box a widget already drawn is given at `place` of + /// the box its parent is being taken as. What narrowed its frame and what + /// it declared are its own record's, so both are resolved against that + /// parent's frame again exactly as the first ask resolved them. + fn ask_again(active: &ActiveData, at: &Placing, place: [Place; 2]) -> (UiVec2, UiRegion) { + frame_and_extent( + at.extent, + at.frame, + place, + active.narrow, + active.declared, + active.own_align, + ) } /// Re-places everything inside a widget whose own box moved. Every child /// is placed as a part of that box, so each one's new box is its retained /// part re-added to the new start -- and a child whose own box then did /// not change is not touched at all. - fn reposition( - &mut self, - id: WidgetId, - frame: UiRegion, - extent: UiRegion, - info: DrawInfo, - rsc: &mut dyn UiRsc, - ) { + fn reposition(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { let active = self.active.get_mut(&id).unwrap(); - active.frame_abs = frame; active.extent = extent; - let local = if info.region_node { - UiRegion::FULL - } else { - frame - }; for primitive in &active.primitives { let handle = &primitive.handle; - *self.layers[handle.layer].region_mut(handle) = - primitive.region.within(&extent).within(&local); + *self.layers[handle.layer].region_mut(handle) = primitive.region.within(&extent); } if let Some(mask_region) = active.mask_region { - rsc.ui_mut().masks.get_mut(active.mask).region = - mask_region.within(&extent).within(&local); + rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&extent); } let at = Placing { id, extent, - local, - px: info.px, + frame: info.frame, + window: self.output_size, depth: info.depth, move_idx: active.move_idx, mask: active.mask, @@ -811,38 +801,6 @@ impl UiRenderState { } } - /// Replays the original local compositions, including their rounding order. - /// A region node terminates the walk because its contents name its slot. - fn recompose_subtree( - &mut self, - id: WidgetId, - frame: UiRegion, - parent_move: MoveIdx, - rsc: &mut dyn UiRsc, - ) { - let active = self.active.get_mut(&id).unwrap(); - active.frame_abs = frame; - if active.move_idx != parent_move { - self.moves.set(active.move_idx, frame); - return; - } - let extent = active.extent; - for primitive in &active.primitives { - let handle = &primitive.handle; - *self.layers[handle.layer].region_mut(handle) = - primitive.region.within(&extent).within(&frame); - } - if let Some(local) = active.mask_region { - rsc.ui_mut().masks.get_mut(active.mask).region = local.within(&extent).within(&frame); - } - let children = active.children.len(); - for index in 0..children { - let child = self.active[&id].children[index]; - let local = self.active[&child].frame; - self.recompose_subtree(child, local.within(&frame), parent_move, rsc); - } - } - fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool { let Some(widget) = rsc.widgets().get_dyn(id) else { return true; @@ -913,9 +871,8 @@ impl UiRenderState { id, ActiveData { id, - frame_abs: UiRegion::FULL, extent: UiRegion::FULL, - frame: UiRegion::FULL, + frame: UiVec2::FULL_SIZE, narrow: [None; 2], place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], @@ -1102,9 +1059,8 @@ impl UiRenderState { pub fn window_region(&self, id: &impl IdLike) -> Option { let active = self.active.get(&id.id())?; active.drawn.then(|| { - let placed = active.extent.within(&active.frame_abs); self.moves - .resolve(active.parent_move, placed) + .resolve(active.move_idx, active.extent) .to_px(self.output_size) }) } @@ -1146,10 +1102,10 @@ impl UiRenderState { // box is its own to work out again against the output. Every other // widget was given one. let Some(parent) = active.parent else { - let region = Self::root_region(id, rsc.widgets()); + let (frame, extent) = Self::root_layout(id, rsc.widgets()); let info = DrawInfo { mask: active.parent_mask, - ..self.root_info(region) + ..self.root_info(frame, extent) }; #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); @@ -1157,7 +1113,6 @@ impl UiRenderState { self.draw_inner(id, info, old, rsc); return true; }; - let px = self.asked_px(id); let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.place); // The question its parent asked, asked again: the same place of the // box the parent was asked in, which is the box the parent's own @@ -1165,13 +1120,7 @@ impl UiRenderState { // parent's answer put its own drawing is not a question anybody // asked, and nothing is asked in it here either. let asked = self.placing_of(parent, self.active[&parent].offer_part); - let (frame, part) = Self::re_ask(active, asked.extent, active.offer_place); - debug_assert_eq!( - frame.size(), - active.frame.size(), - "'{}' ({id:?}) asked again in a frame of another length", - rsc.widgets().label(id) - ); + let (frame, part) = Self::ask_again(active, &asked, active.offer_place); let info = DrawInfo { layer: active.layer, parent: active.parent, @@ -1180,13 +1129,12 @@ impl UiRenderState { region_node: rsc.widgets().is_region_node(id), mask: active.parent_mask, frame, - frame_abs: frame.within(&asked.local), part, place: active.offer_place, offer_place: active.offer_place, narrow: active.narrow, re_asked: false, - px, + px: frame.to_px(self.output_size), }; #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); @@ -1202,7 +1150,9 @@ impl UiRenderState { { active.answer = was_answer; } - if active.holds.covers(was_holds) && was_holds.contains(px, active.extent) { + if active.holds.covers(was_holds) + && was_holds.contains(self.output_size, active.frame, active.extent) + { active.holds = was_holds; } if active.answer != was_answer || active.holds != was_holds { @@ -1232,11 +1182,8 @@ impl UiRenderState { Placing { id, extent, - local: match active.move_idx != active.parent_move { - true => UiRegion::FULL, - false => active.frame_abs, - }, - px: self.asked_px(id), + frame: active.frame, + window: self.output_size, depth: active.depth, move_idx: active.move_idx, mask: active.mask, @@ -1245,12 +1192,33 @@ impl UiRenderState { } /// Whether what a widget reports along `axis` is inside the box it drew in. -/// A share is a length only to whoever divides one, so it is not a claim -/// about this box and cannot exceed it. -fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool { +/// Both are lengths of the window, so the comparison is in its pixels. A +/// share is a length only to whoever divides one, so it is not a claim about +/// this box and cannot exceed it. +fn within_box(size: Size, extent: UiRegion, window: PxVec2, axis: Axis) -> bool { let len = size.axis(axis); - let box_len = px.axis(axis); - len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len + let window = window.axis(axis); + len.leftover != Weight::ZERO + || Len::from_parts(len.rel, len.px).to_px(window) <= extent.axis(axis).len().to_px(window) +} + +/// A box in a fresh region node keeps its window-unit length and starts at +/// that node's origin. +fn local_region(region: UiRegion) -> UiRegion { + let size = region.size(); + UiRegion::new( + UiSpan::new(Len::ZERO, size.x), + UiSpan::new(Len::ZERO, size.y), + ) +} + +/// A region node changes only the origin. A full relative span anchored at +/// the box start composes as that translation in both the CPU and shader. +fn translation(region: UiRegion) -> UiRegion { + UiRegion { + x: UiSpan::new(region.x.start, region.x.start + Len::FULL), + y: UiSpan::new(region.y.start, region.y.start + Len::FULL), + } } impl Default for UiRenderState { diff --git a/src/random.rs b/src/random.rs index 2ab983f..870b992 100644 --- a/src/random.rs +++ b/src/random.rs @@ -122,7 +122,9 @@ impl Widget for Branch { let measured = painter .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .len(Axis::X); - let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); + let px = measured + .apply_leftover() + .to_px(painter.window_px_len(Axis::X)); let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let place = [Place::Within(Part::All), below]; diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 8d10772..474c993 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -14,12 +14,11 @@ impl Widget for Pad { // 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. // - // The padding goes around what it pads: the frame passes through, so - // the inner's fractions mean what they would without it, and only - // the box it draws in is moved in by the pixels. Said as a part of - // this widget's own box in that box's own lengths, so nothing here - // reads how long the box is -- and a box chosen from this widget's - // own answer therefore does not feed back into that answer. + // Padding is an inset of both: it comes off the frame, so `rel(1)` + // under it fills this widget rather than overflowing it by the + // padding, and it comes off the box, so what is drawn sits inside. + // 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. let inset = |lead: Px, trail: Px| { Place::Within(Part::Of(UiSpan::new( Len::from_parts(Rel::ZERO, lead), @@ -30,7 +29,16 @@ impl Widget for Pad { inset(self.padding.left, self.padding.right), inset(self.padding.top, self.padding.bottom), ]; - let inner = painter.widget_at(&self.inner, [None; 2], place).size(); + // Read from this widget's own frame rather than written as a + // fraction of it: a frame is a length of the window like everything + // else here, and taking the padding off is the whole of what this + // widget does to it. + let narrow = [ + (Axis::X, self.padding.left + self.padding.right), + (Axis::Y, self.padding.top + self.padding.bottom), + ] + .map(|(axis, pixels)| Some(painter.frame_len(axis) - Len::from_parts(Rel::ZERO, pixels))); + let inner = painter.widget_at(&self.inner, narrow, place).size(); Size { x: LayoutLen { px: inner.x.px + self.padding.left + self.padding.right, diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 4721f0e..bca8770 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -16,7 +16,8 @@ impl Widget for Scroll { let answer_len = painter .widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2]) .len(self.axis); - let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len); + let fixed = + Len::from_parts(answer_len.rel, answer_len.px).to_px(painter.window_px_len(self.axis)); self.container_len = container_len; self.content_len = fixed.max(container_len); diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 298a94f..5c8bb08 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -68,12 +68,12 @@ impl Widget for Span { // exist at all turns on this. let mut shares = false; if total.leftover > Weight::ZERO { - shares = room.to_px(painter.frame_px_len(axis)) > Px::ZERO; + shares = room.to_px(painter.window_px_len(axis)) > Px::ZERO; let holds = match shares { true => Holds::from(Px::STEP..=Px::MAX), false => Holds::from(Px::MIN..=Px::ZERO), }; - painter.frame_holds(axis, holds.through(room)); + painter.window_holds(axis, holds.through(room)); } // Across itself a span is as long as its longest child -- unless a diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index 46de2f0..6932465 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -33,16 +33,19 @@ impl Widget for Stack { // fraction under them is a fraction of it. A share leaves the axis // to whoever gave the stack its box. Where a child sits in a box // bigger than itself is its own business. - let narrow = [Axis::X, Axis::Y].map(|axis| { + let place = [Axis::X, Axis::Y].map(|axis| { let len = size.axis(axis); - (len.leftover == Weight::ZERO).then(|| Len::from_parts(len.rel, len.px)) + match len.leftover == Weight::ZERO { + true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))), + false => Place::Within(Part::All), + } }); for (i, child) in self.children.iter().enumerate() { if sizing == Some(i) { continue; } painter.child_layer_at(i); - painter.widget_at(child, narrow, [Place::Within(Part::All); 2]); + painter.widget_at(child, [None; 2], place); } size } diff --git a/tests/cases/determinism.rs b/tests/cases/determinism.rs index b3d9c3e..4f16d7e 100644 --- a/tests/cases/determinism.rs +++ b/tests/cases/determinism.rs @@ -26,7 +26,9 @@ impl Widget for BranchesOnMeasurement { let measured = painter .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .len(Axis::X); - let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); + let px = measured + .apply_leftover() + .to_px(painter.window_px_len(Axis::X)); let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let place = [Place::Within(Part::All), below]; diff --git a/tests/cases/layout.rs b/tests/cases/layout.rs index 27fd5f1..3d97eb7 100644 --- a/tests/cases/layout.rs +++ b/tests/cases/layout.rs @@ -83,10 +83,8 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() { assert!(crowded > whole_row, "{crowded} against {whole_row}"); } -/// The same reading through a pad: padding goes around what it pads and -/// does not narrow what a fraction under it is a fraction of, so half of the -/// window plus the padding is what the pad takes and where the next child -/// starts. +/// Padding is an inset: it narrows the frame a fraction resolves against and +/// adds itself back to the padded widget's reported length. #[test] fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() { let mut h = Harness::new((400, 100)); @@ -97,9 +95,83 @@ fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() { // placed inside it by its own alignment, which is not what is under test. h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0))); - assert_corners!(h, inner, (10, 10), (210, 90)); - assert_corners!(h, padded, (0, 0), (220, 100)); - assert_corners!(h, tail, (220, 0), (320, 100)); + assert_corners!(h, inner, (10, 10), (200, 90)); + assert_corners!(h, padded, (0, 0), (210, 100)); + assert_corners!(h, tail, (210, 0), (310, 100)); +} + +const PARAGRAPH: &str = "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."; + +/// The worked example of what padding insets: in a 900 px row after a 24 px +/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by +/// the icon's width, while a wrapping text beside it is asked in the room +/// left, 900 - 24 - 32, and wraps there. +#[test] +fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() { + let mut h = Harness::new((900, 200)); + let icon = rect(Color::RED).width(24).add(&mut h.rsc); + let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc); + let padded = fill.pad(16).add(&mut h.rsc); + h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0))); + let fill_width = h.region(&fill).unwrap().size().x; + assert_eq!(fill_width, Px::from_int(868)); + + let mut h = Harness::new((900, 200)); + let icon = rect(Color::RED).width(24).add(&mut h.rsc); + let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); + let padded = text.pad(16).add(&mut h.rsc); + h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0))); + let active = &h.render.active[&text.id()]; + let window = h.render.output_size().x; + let asked = active.offer_part.x.len().to_px(window); + assert_eq!(active.frame.x.to_px(window), Px::from_int(868)); + assert_eq!(asked, Px::from_int(844)); +} + +/// The other way round: a share inside padding. A slot is a length of the +/// row, which is already the padded width, so what the span decided reaches +/// the child as it stands -- taking the padding off a second time would make +/// `rel(1.0)` in the slot shorter than the slot. +#[test] +fn a_share_inside_padding_fills_the_slot_it_was_given() { + let mut h = Harness::new((900, 200)); + let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc); + let first = Span { + children: vec![fill.add_strong(&mut h.rsc)], + dir: Dir::RIGHT, + gap: Px::ZERO, + } + .width(leftover(1)) + .add(&mut h.rsc); + let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc); + let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc); + h.set_root(row.pad(16)); + + assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434)); + assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434)); +} + +/// The same padding in a share instead: the slot is 450, so both the +/// fraction and the wrap are the slot less the padding, and the two agree. +#[test] +fn padding_narrows_both_frame_and_box_inside_a_share() { + let mut h = Harness::new((900, 200)); + let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc); + let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc); + let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc); + h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0))); + assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418)); + + let mut h = Harness::new((900, 200)); + let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); + let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc); + let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc); + h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0))); + let active = &h.render.active[&text.id()]; + let window = h.render.output_size().x; + assert_eq!(active.frame.x.to_px(window), Px::from_int(418)); + assert_eq!(active.offer_part.x.len().to_px(window), Px::from_int(418)); } #[test] @@ -428,8 +500,7 @@ fn a_row_of_equal_shares_fills_it_exactly() { /// a step of. Kept in step with `snap_floor` in `prelude.wgsl`. fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { let active = &h.render.active[&id]; - let drawn = active.extent.within(&active.frame_abs); - let region = h.render.moves.resolve(active.parent_move, drawn); + let region = h.render.moves.resolve(active.move_idx, active.extent); let dim = h.size().axis(axis); let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor(); let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32()); @@ -740,3 +811,13 @@ fn a_fixed_child_is_centered_in_its_wrappers_share() { assert_corners!(h, wrapper, (200, 0), (900, 400)); assert_corners!(h, leaf, (500, 150), (600, 250)); } + +/// The root's frame is the window and its rule is a fraction of that, which +/// is one resolution and not two: nothing above it narrowed anything. +#[test] +fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() { + let mut h = Harness::new((900, 200)); + let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc); + h.set_root(root); + assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450)); +} diff --git a/tests/cases/unsettled.rs b/tests/cases/unsettled.rs index 70f921a..4337214 100644 --- a/tests/cases/unsettled.rs +++ b/tests/cases/unsettled.rs @@ -733,6 +733,51 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() { const PARAGRAPH: &str = "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."; +fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec, WidgetId) { + let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc); + h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None); + if fixed { + h.rsc.widgets_mut().set_size_rules( + sizing.id(), + Some(LayoutLen::px(112)), + Some(LayoutLen::px(101)), + ); + } + let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); + let pad = Pad { + padding: Padding::ZERO, + inner: text.add_strong(&mut h.rsc), + } + .add(&mut h.rsc); + let stack = Stack { + children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)], + size: StackSize::Child(0), + } + .add(&mut h.rsc); + h.set_root(stack); + ( + vec![sizing.id(), text.id(), pad.id(), stack.id()], + sizing.id(), + ) +} + +#[test] +fn fixing_a_stacks_sizing_child_repositions_its_overlay() { + let mut warm = Harness::new((900, 1200)); + let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false); + warm.frame(); + warm.rsc.widgets_mut().set_size_rules( + sizing, + Some(LayoutLen::px(112)), + Some(LayoutLen::px(101)), + ); + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true); + assert_same_regions(&warm, &ids, &cold, &cold_ids); +} + /// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4, /// `shuffle-swap-for-three`). The stack takes its size from the span above, /// the span takes its width from the longest line of the texts in it, and @@ -927,3 +972,54 @@ fn emptying_a_column_the_row_asked_twice_asks_the_row_again() { assert_same_regions(&warm, &ids, &cold, &cold_ids); } + +/// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column +/// divides the box it is given between two shares, so its drawing holds for +/// that box's length alone, and the pads above it pass that dependency up: +/// each one's box is a part of the box it was asked in. Padding narrowing +/// the frame it hands down does not change that, and while it was taken to, +/// changing the rule over the pads relocated the column's drawing into the +/// new box instead of dividing it again. +fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec { + let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc); + let bottom = rect(Color::RED).add(&mut h.rsc); + let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc); + let inner = Pad { + padding: Padding::ZERO, + inner: column.add_strong(&mut h.rsc), + } + .add(&mut h.rsc); + let outer = Pad { + padding: Padding::ZERO, + inner: inner.add_strong(&mut h.rsc), + } + .height(height) + .add(&mut h.rsc); + let beside = rect(Color::BLUE).add(&mut h.rsc); + h.set_root((outer, beside).span(Dir::RIGHT)); + vec![ + top.id(), + bottom.id(), + column.id(), + inner.id(), + outer.id(), + beside.id(), + ] +} + +#[test] +fn changing_a_rule_over_two_pads_divides_the_column_again() { + let mut warm = Harness::new((900, 1200)); + let ids = plant_two_shares_under_two_pads(&mut warm, 88.0); + warm.frame(); + warm.rsc + .widgets_mut() + .set_size_rules(ids[4], None, Some(LayoutLen::px(105))); + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0); + cold.frame(); + + assert_same_regions(&warm, &ids, &cold, &cold_ids); +} diff --git a/tests/scenario/mod.rs b/tests/scenario/mod.rs index c0106a0..caefb14 100644 --- a/tests/scenario/mod.rs +++ b/tests/scenario/mod.rs @@ -103,6 +103,11 @@ pub enum Case { /// A resize and then a size change, so a retained answer is asked to /// survive two different kinds of invalidation in a row. ResizeSize, + /// A size change and then a resize, which is the other order and not the + /// same test: a length answered as a fraction of one box and kept as a + /// fraction of another agrees at the size it was changed at and parts + /// from it at every other one. + SizeResize, /// A few declared sizes. Size, /// Every declared size at once, so every reader of a size has a changed @@ -119,12 +124,13 @@ pub enum Case { Shuffle(Shuffle), } -pub const ALL: [Case; 15] = [ +pub const ALL: [Case; 16] = [ Case::Repaint, Case::RepaintSome, Case::Resize, Case::ResizeRepaint, Case::ResizeSize, + Case::SizeResize, Case::Size, Case::EverySize, Case::Align, @@ -146,6 +152,7 @@ impl Case { Self::Resize => "resize", Self::ResizeRepaint => "resize-repaint", Self::ResizeSize => "resize-size", + Self::SizeResize => "size-resize", Self::Size => "size", Self::EverySize => "every-size", Self::Align => "align", @@ -170,6 +177,15 @@ impl Case { _ => (STILL, STILL), } } + + /// The window the warm tree is taken to after the change, where the case + /// is about what the change left behind rather than about the change. + fn then_resize(self) -> Option<(f32, f32)> { + match self { + Self::SizeResize => Some(INNER), + _ => None, + } + } } fn mark(warm: &mut Harness, tree: &Tree, step: usize) { @@ -284,7 +300,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu warm.frame(); return out; } - Case::Size | Case::ResizeSize => Edits { + Case::Size | Case::ResizeSize | Case::SizeResize => Edits { sizes: some_sizes(warm, tree, rng), ..Default::default() }, @@ -384,6 +400,17 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String { label } +/// One widget's layout as it stands: the frame its fractions resolved +/// against, the box it was asked in, the box its drawing went in, and what +/// it reported. In window units, which is what both trees are in. +fn record(id: WidgetId, h: &Harness) -> String { + let active = &h.render.active[&id]; + format!( + "frame {} ask {} box {} size {}", + active.frame, active.offer_part, active.extent, active.size, + ) +} + /// 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, /// so one plan under one case is one comparison however it was reached. @@ -400,12 +427,29 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option { warm.frame(); } let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed)); + // Whatever the change left, seen at another window: an answer kept as a + // fraction of the wrong length is the same number of pixels where it was + // made and a different one everywhere else. + let end = match case.then_resize() { + Some(after) => { + warm.resize(after); + warm.frame(); + after + } + None => end, + }; let mut cold = Harness::new(end); let (root, cold_tree) = build(&mut cold.rsc, &cold_plan); cold.state.root = Some(root); cold.frame(); + let places: HashMap = tree + .ids + .iter() + .enumerate() + .map(|(i, &id)| (id, i)) + .collect(); let mut drawn = 0; for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); @@ -416,6 +460,7 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option { // Where two trees disagree is rarely where the cause is, so the // ancestry comes with it, marking the widgets that own a region. let mut chain = Vec::new(); + let mut records = Vec::new(); let mut at = Some(w); while let Some(id) = at { let active = &warm.render.active[&id]; @@ -424,11 +469,22 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option { false => "*", }; chain.push(format!("{}{node}", describe(id, &warm))); + // What each level was asked in on both sides, since the level + // where the two stop agreeing is the one to look at rather than + // the leaf that reported the difference. + let cold_id = places.get(&id).and_then(|&i| cold_tree.ids.get(i)); + records.push(format!( + " {}\n warm {}\n cold {}", + describe(id, &warm), + record(id, &warm), + cold_id.map_or("-".into(), |&id| record(id, &cold)), + )); at = active.parent; } return Some(format!( - "widget {i}\n warm {got:?}\n cold {want:?}\n {}", - chain.join(" < ") + "widget {i}\n warm {got:?}\n cold {want:?}\n {}\n{}", + chain.join(" < "), + records.join("\n"), )); } match drawn { diff --git a/tests/shrink.rs b/tests/shrink.rs index 4c4b52e..fff73f3 100644 --- a/tests/shrink.rs +++ b/tests/shrink.rs @@ -70,10 +70,14 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() { over_seeds(seeds, |seed| { let grown = plan(seed, depth, &Edits::default()); for &case in &cases { - let Some(how) = diverges(&grown, case, seed) else { + if diverges(&grown, case, seed).is_none() { continue; - }; + } let small = shrink(grown.clone(), case, seed); + // Described from the shrunk tree: the grown tree's chain names + // widgets that are no longer there, and the ancestry of the + // failure is what a test is written from. + let how = diverges(&small, case, seed).unwrap_or_default(); println!( "seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}", case.name(),