diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 7cb56f7..10de432 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -41,7 +41,7 @@ pub struct ActiveData { pub textures: Vec, pub primitives: Vec, pub mask_region: Option, - pub(crate) extent_children: Vec<(WidgetId, ExtentPlacement)>, + pub inherited_children: Vec, pub children: Vec, /// The children whose size this widget read while drawing. pub size_deps: Vec, @@ -87,18 +87,3 @@ impl ActiveData { }) } } - -#[derive(Clone, Copy, Debug)] -pub(crate) enum ExtentPlacement { - Inherit, - Within(UiRegion), -} - -impl ExtentPlacement { - pub fn resolve(self, extent: UiRegion) -> (UiRegion, [Option; 2]) { - match self { - Self::Inherit => (UiRegion::FULL, [Some(extent.x), Some(extent.y)]), - Self::Within(local) => (local.within(&extent), [None; 2]), - } - } -} diff --git a/core/src/ui/layout_holds.rs b/core/src/ui/layout_holds.rs index ef0bff6..89d61c6 100644 --- a/core/src/ui/layout_holds.rs +++ b/core/src/ui/layout_holds.rs @@ -15,6 +15,25 @@ impl LayoutHolds { placement: None, }; + pub fn and(self, other: Self) -> Self { + debug_assert!( + self.placement.is_none() + || other.placement.is_none() + || self.placement == other.placement + ); + Self { + frame: [ + self.frame[0].and(other.frame[0]), + self.frame[1].and(other.frame[1]), + ], + extent: [ + self.extent[0].and(other.extent[0]), + self.extent[1].and(other.extent[1]), + ], + placement: self.placement.or(other.placement), + } + } + pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool { self.placement.is_none_or(|old| old == placement) && [Axis::X, Axis::Y].into_iter().all(|axis| { diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index bd829e4..96e06bb 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,9 +1,9 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ - Axis, DrawRegion, ExtentPlacement, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, - RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, - TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, + Axis, DrawRegion, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, + RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, + UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -40,9 +40,10 @@ pub struct Painter<'a> { pub(super) textures: Vec, pub(super) primitives: Vec, pub(super) mask_region: Option, - pub(super) extent_children: Vec<(WidgetId, ExtentPlacement)>, + pub(super) inherited_children: Vec, pub(super) extent_own: [Holds; 2], - pub(super) extent_under: [Holds; 2], + /// Only children whose answers were read constrain this widget's answer. + pub(super) answer_under: LayoutHolds, pub(super) children: Vec, /// The children asked about so far, so the first box each was asked in /// is the one recorded as its offer. @@ -59,8 +60,8 @@ pub struct Painter<'a> { /// What this draw itself read of its box in pixels, per axis: every /// length until it reads one, then that one, unless it says otherwise. pub(super) own: [Holds; 2], - /// What the children it asked about and drew keep it to. - pub(super) under: [Holds; 2], + /// Dependencies of every child drawing, including unmeasured overlays. + pub(super) under: LayoutHolds, /// The movable region this widget's primitives are positioned through: /// its own when opted in, otherwise the nearest ancestor's. pub(super) move_idx: MoveIdx, @@ -154,13 +155,7 @@ impl<'a> Painter<'a> { /// around one child wants, since its box is the child's. pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget) -> DrawResult<'s, 'a, W> { let own = self.placement; - self.widget_at_inner( - id, - UiRegion::FULL, - [Some(own.x), Some(own.y)], - Some(ExtentPlacement::Inherit), - false, - ) + self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true, false) } /// What a widget's rules declare its lengths to be, which whoever draws @@ -176,29 +171,19 @@ impl<'a> Painter<'a> { /// this frame; what it answered is still something this widget asked. pub fn undraw(&mut self, id: &StrongWidget) { self.children.retain(|child| *child != id.id()); - self.extent_children.retain(|(child, _)| *child != id.id()); + self.inherited_children.retain(|child| *child != id.id()); self.state.undraw_rec(id.id(), self.rsc); } - /// Draws a child in a frame relative to this widget's frame or extent. - /// A plain `UiRegion` is frame-relative. `DrawRegion::Extent` keeps the - /// child's frame attached to the extent without reading `placement()`. - /// The child places its answer within that frame by its own alignment. + /// Draws a child in `region`, relative to this widget's frame. The child + /// resolves declared lengths and reports against that frame, then places + /// its drawing by its own alignment. pub fn widget_within<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, - region: impl Into, + region: UiRegion, ) -> DrawResult<'s, 'a, W> { - match region.into() { - DrawRegion::Frame(region) => self.widget_at(id, region, [None; 2]), - DrawRegion::Extent(local) => self.widget_at_inner( - id, - local.within(&self.placement), - [None; 2], - Some(ExtentPlacement::Within(local)), - false, - ), - } + self.widget_at(id, region, [None; 2]) } /// Draws a widget in `region`, saying where in it the drawing goes. @@ -221,7 +206,7 @@ impl<'a> Painter<'a> { region: UiRegion, placement: [Option; 2], ) -> DrawResult<'s, 'a, W> { - self.widget_at_inner(id, region, placement, None, false) + self.widget_at_inner(id, region, placement, false, false) } fn widget_at_inner<'s, W: ?Sized>( @@ -229,12 +214,15 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, region: UiRegion, placement: [Option; 2], - extent: Option, + inherited: bool, measuring: bool, ) -> DrawResult<'s, 'a, W> { - self.extent_children.retain(|(child, _)| *child != id.id()); - if let Some(extent) = extent { - self.extent_children.push((id.id(), extent)); + if inherited { + if !self.inherited_children.contains(&id.id()) { + self.inherited_children.push(id.id()); + } + } else { + self.inherited_children.retain(|child| *child != id.id()); } let region_node = self.rsc.widgets().is_region_node(id.id()); let declared = self.declared_lens(id); @@ -276,7 +264,7 @@ impl<'a> Painter<'a> { // The answer and what it holds for, both about the box asked in. The // child's record may say something else once its drawing has been // placed: a drawing made again in its placed box holds for that box. - let (size, holds) = self.state.draw_inner( + let (size, answer_holds, holds) = self.state.draw_inner( id.id(), within, DrawInfo { @@ -297,53 +285,34 @@ impl<'a> Painter<'a> { measuring, self.rsc, ); - for axis in AXES { - let n = axis as usize; - match extent { - Some(ExtentPlacement::Inherit) if declared[n].is_none() => { - self.under[n] = - self.under[n].and(holds.frame[n].through(local.axis(axis).len())); - self.extent_under[n] = self.extent_under[n].and(holds.extent[n]); - self.reads_placement |= holds.placement.is_some(); - } - Some(ExtentPlacement::Within(part)) - if declared[n].is_none() - && part.axis(axis).start.rel == crate::Rel::ZERO - && part.axis(axis).end.rel == crate::Rel::ONE => - { - let dependent = holds.frame[n] - .and(holds.extent[n]) - .through(part.axis(axis).len()); - self.extent_under[n] = self.extent_under[n].and(dependent); - } - _ => { - let chosen = placement[n].unwrap_or(UiSpan::FULL).len(); - self.under[n] = self.under[n] - .and(holds.frame[n].through(local.axis(axis).len())) - .and( - holds.extent[n] - .through(chosen) - .through(local.axis(axis).len()), - ); - // Fractional endpoints compose before pixel evaluation. Their - // difference cannot be inverted through the extent length alone. - if matches!(extent, Some(ExtentPlacement::Within(_))) && declared[n].is_none() { - self.reads_placement = true; + let in_parent = |holds: LayoutHolds| { + let mut result = LayoutHolds::ANY; + for axis in AXES { + let n = axis as usize; + result.frame[n] = holds.frame[n].through(local.axis(axis).len()); + if inherited && declared[n].is_none() { + result.extent[n] = holds.extent[n]; + if holds.placement.is_some() { + result.placement = Some(self.placement); } + } else { + let chosen = placement[n].unwrap_or(UiSpan::FULL).len(); + result.frame[n] = result.frame[n].and( + holds.extent[n] + .through(chosen) + .through(local.axis(axis).len()), + ); } } - } - // A fractional report is composed into the parent frame, so its - // value can change with the extent even when the drawing holds. - let reads_placement = matches!(extent, Some(ExtentPlacement::Within(_))) - && AXES.into_iter().any(|axis| { - declared[axis as usize].is_none() && size.axis(axis).rel != crate::Rel::ZERO - }); + result + }; + self.under = self.under.and(in_parent(holds)); + let answer_holds = in_parent(answer_holds); DrawResult { child: id, painter: self, size: in_parent_frame(size, local.size(), declared), - reads_placement, + answer_holds, } } @@ -400,7 +369,7 @@ impl<'a> Painter<'a> { .retained_size(child.id(), px, placement, self.move_idx, self.rsc.widgets()); let Some((size, holds)) = retained else { return self - .widget_at_inner(child, region, offered, None, true) + .widget_at_inner(child, region, offered, false, true) .len(axis); }; #[cfg(feature = "layout-diagnostics")] @@ -417,7 +386,7 @@ impl<'a> Painter<'a> { y: placement[1].unwrap_or(UiSpan::FULL), }; let holds = holds.in_frame(placement); - for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { + for (axis, under) in AXES.into_iter().zip(self.answer_under.frame.iter_mut()) { *under = under.and(holds[axis as usize].through(local.axis(axis).len())); } in_parent_frame(size, local.size(), declared).axis(axis) @@ -641,7 +610,7 @@ pub struct DrawResult<'p, 'a, W: ?Sized> { painter: &'p mut Painter<'a>, child: &'p StrongWidget, size: Size, - reads_placement: bool, + answer_holds: LayoutHolds, } impl DrawResult<'_, '_, W> { @@ -652,7 +621,7 @@ impl DrawResult<'_, '_, W> { diag::size_read(self.child.id(), self.painter.id, self.size); } self.painter.depend_on(self.child); - self.painter.reads_placement |= self.reads_placement; + self.painter.answer_under = self.painter.answer_under.and(self.answer_holds); self.size } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 6b64539..7f55008 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -66,13 +66,6 @@ pub struct UiRenderState { /// A widget's move slot, which outlives any one `ActiveData`: a redraw /// replaces that while its children go on pointing at the slot. slots: HashMap, - /// Answers invalidated by a declared-length change below them. These are - /// replaced even when retained placement means the redraw is not at the - /// old offer. - answer_invalid: crate::util::HashSet, - /// Whether this frame contains a declared-length change, so any dirty - /// dependent replaces its answer too. - replace_answers: bool, /// Widgets waiting for an ancestor to draw them, so the walk down the /// depths does not pick one up again at its own depth. deferred: crate::util::HashSet, @@ -87,8 +80,6 @@ impl UiRenderState { output_size: PxVec2::ZERO, old_root: None, slots: Default::default(), - answer_invalid: Default::default(), - replace_answers: false, deferred: Default::default(), moves: Default::default(), resized: false, @@ -103,9 +94,8 @@ impl UiRenderState { /// retained entry at all. /// /// The root is the only widget a resize marks, and only where the new - /// output falls outside what its answer holds for: that range is the - /// intersection of everything under it, so admitting the new output says - /// the whole tree still stands. Where it does not, the ordinary walk + /// output invalidates its answer or its drawing. The latter includes + /// children whose size it never read. Where either fails, the ordinary walk /// draws the root, and each widget's own range decides how far down the /// new length reaches. pub fn resize(&mut self, size: impl Into, widgets: &mut Widgets) { @@ -116,10 +106,10 @@ impl UiRenderState { self.output_size = size; self.resized = true; let Some(root) = self.old_root else { return }; - let stands = self - .active - .get(&root) - .is_some_and(|active| active.answers_at(active.given_region.size().to_px(size))); + let stands = self.active.get(&root).is_some_and(|active| { + let px = active.given_region.size().to_px(size); + active.answers_at(px) && active.holds.contains(px, active.placement) + }); if !stands { widgets.needs_redraw.insert(root); } @@ -182,7 +172,6 @@ impl UiRenderState { if rsc.widgets().has_updates() { self.redraw_updates(rsc); } - self.replace_answers = false; self.free(rsc); } @@ -215,7 +204,7 @@ impl UiRenderState { mut old: Option, measuring: bool, rsc: &mut dyn UiRsc, - ) -> (Size, LayoutHolds) { + ) -> (Size, LayoutHolds, LayoutHolds) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); @@ -226,8 +215,7 @@ impl UiRenderState { // one bottom-up walk, so anything deeper has settled or deferred to // its own parent, and a deferred one leaves that parent marked. let stale = rsc.widgets().needs_redraw.contains(&id); - let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale); - let retained = match replace_answer || stale { + let retained = match stale { true => None, false => self .retained_answer(id, info) @@ -284,7 +272,7 @@ impl UiRenderState { active.given_region = info.given_region; active.offer_len = info.offer_len; if info.placement == info.offer_placement && info.px == info.offered_px { - active.answer = Some(settled); + active.answer = Some(answer); active.offer_placement = info.offer_placement; } active.decided = info.decided(); @@ -300,9 +288,9 @@ impl UiRenderState { && let Some(old_parent) = self.active.get_mut(&old_parent) { old_parent.children.retain(|child| *child != id); - old_parent.extent_children.retain(|(child, _)| *child != id); + old_parent.inherited_children.retain(|child| *child != id); } - settled + (answer.0, answer.1, settled.1) } /// Recompose retained geometry when the evaluation still holds at this extent. @@ -373,16 +361,16 @@ impl UiRenderState { textures: Vec::new(), primitives: Vec::new(), mask_region: None, - extent_children: Vec::new(), + inherited_children: Vec::new(), children: Vec::new(), offered: Vec::new(), offered_px: info.offered_px, at_offer, size_deps: Vec::new(), own: [Holds::ANY; 2], - under: [Holds::ANY; 2], + under: LayoutHolds::ANY, extent_own: [Holds::ANY; 2], - extent_under: [Holds::ANY; 2], + answer_under: LayoutHolds::ANY, depth: info.depth, move_idx, rsc, @@ -410,9 +398,9 @@ impl UiRenderState { textures, primitives, mask_region, - extent_children, + inherited_children, extent_own, - extent_under, + answer_under, children, offered: _, offered_px: _, @@ -450,14 +438,13 @@ impl UiRenderState { "'{}' ({id:?}) clips to {px:?} and reports {size}", rsc.widgets().label(id), ); - let holds = LayoutHolds { - frame: [own[0].and(under[0]), own[1].and(under[1])], - extent: [ - extent_own[0].and(extent_under[0]), - extent_own[1].and(extent_under[1]), - ], + let own_holds = LayoutHolds { + frame: own, + extent: extent_own, placement: reads_placement.then_some(placement), }; + let answer_holds = own_holds.and(answer_under); + let holds = answer_holds.and(under); debug_assert!( holds.contains(px, placement), "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", @@ -516,7 +503,7 @@ impl UiRenderState { textures, primitives, mask_region, - extent_children, + inherited_children, children, size_deps, declared: declared_lens(rsc.widgets(), id), @@ -530,7 +517,7 @@ impl UiRenderState { }; rsc.on_draw(&active); self.active.insert(id, active); - (size, holds) + (size, answer_holds) } /// Keeps a region node's entry across redraws because descendants retain @@ -765,18 +752,19 @@ impl UiRenderState { } let parent_move = active.move_idx; let mask = active.mask; - let children = active.extent_children.len(); + let children = active.inherited_children.len(); for index in 0..children { - let (child, extent) = self.active[&id].extent_children[index]; - let (child_region, chosen) = extent.resolve(placement); + let child = self.active[&id].inherited_children[index]; let active = &self.active[&child]; - let (child_local, chosen) = - ask_box(child_region, active.declared, active.own_align, chosen); - // Keep the slot chosen from measurement: the final draw can - // report a different size, for example after text reflows. + let (child_local, chosen) = ask_box( + UiRegion::FULL, + active.declared, + active.own_align, + [Some(placement.x), Some(placement.y)], + ); let child_placement = UiRegion { - x: chosen[0].unwrap_or(active.placement.x), - y: chosen[1].unwrap_or(active.placement.y), + x: chosen[0].unwrap_or(UiSpan::FULL), + y: chosen[1].unwrap_or(UiSpan::FULL), }; let child_info = DrawInfo { layer: active.layer, @@ -936,7 +924,7 @@ impl UiRenderState { textures: Vec::new(), primitives: Vec::new(), mask_region: None, - extent_children: Vec::new(), + inherited_children: Vec::new(), children: Vec::new(), size_deps: Vec::new(), move_idx: info.parent_move, @@ -958,8 +946,6 @@ impl UiRenderState { } } self.slots.clear(); - self.answer_invalid.clear(); - self.replace_answers = false; self.moves.clear(); self.layers.clear(); rsc.widgets_mut().needs_redraw.clear(); @@ -973,7 +959,6 @@ impl UiRenderState { rsc.on_remove(id); self.remove(id, true, rsc); self.drop_slot(id); - self.answer_invalid.remove(&id); } rsc.ui_mut().textures.free(); } @@ -1110,15 +1095,6 @@ impl UiRenderState { if let Some(parent) = active.parent && (declared_changed || alignment_changed || !active.drawn || active.answer.is_none()) { - if declared_changed { - self.replace_answers = true; - let mut at = Some(id); - while let Some(next) = at { - self.answer_invalid.insert(next); - rsc.widgets_mut().needs_redraw.insert(next); - at = self.active[&next].parent; - } - } // Both stay marked: the parent because it has this to draw, and // this because the parent must draw it rather than keep what it // has. The mark comes off in `draw_at`, where the parent draws. @@ -1172,7 +1148,7 @@ impl UiRenderState { placement: AXES .map(|axis| active.decided[axis as usize].then(|| *active.placement.axis(axis))), }; - let (given, was_answer) = (active.region, active.answer); + let (given, was_answer, was_holds) = (active.region, active.answer, active.holds); #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); @@ -1187,9 +1163,9 @@ impl UiRenderState { if info.placement != offered.placement { self.draw_inner(id, given, info, None, false, rsc); } - if Some(answer) != was_answer { - // Its parent chose its box knowing the old answer, so it lays out - // again and chooses the box the new one asks for. + if Some((answer.0, answer.1)) != was_answer || self.active[&id].holds != was_holds { + // The parent retains both the answer and the drawing's validity; + // even an unchanged size can narrow the range safe for a resize. #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::SizeChanges); diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 5782b02..1257c26 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -13,7 +13,7 @@ impl Widget for Pad { // it; where the box is bigger -- a share of a row, a rule over this // widget -- the slack is the inner's to sit in, and forcing the near // edge pinned it to a corner it had not asked for. - let inside = DrawRegion::Extent(self.padding.region()); + let inside = self.padding.region_of(painter.placement()); let inner = painter.widget_within(&self.inner, inside).size(); Size { x: LayoutLen { diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index a14e389..2999b96 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -13,6 +13,7 @@ impl Widget for Stack { StackSize::Default => None, StackSize::Child(i) => Some(i), }; + let placement = painter.placement(); // Whichever child sizes the stack keeps the stack's whole region as // its own -- the stack is the length that child asked for, so taking // the fraction of the stack's box again would take it twice -- and is @@ -23,7 +24,13 @@ impl Widget for Stack { // drawing belongs to the layer it was made on. Some((i, child)) => { painter.child_layer_at(i); - painter.widget(child).size() + painter + .widget_at( + child, + UiRegion::FULL, + [Some(placement.x), Some(placement.y)], + ) + .size() } None => Size::LEFTOVER, }; @@ -35,7 +42,7 @@ impl Widget for Stack { // Every other child has the stack's own box for its region, since // the stack is what contains it, and where it sits in one bigger // than itself is its own business. - painter.widget_within(child, DrawRegion::Extent(UiRegion::FULL)); + painter.widget_within(child, placement); } size } diff --git a/tests/cases/retained.rs b/tests/cases/retained.rs index 0485748..cca9dd6 100644 --- a/tests/cases/retained.rs +++ b/tests/cases/retained.rs @@ -1022,210 +1022,86 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() { } #[test] -fn padding_and_stack_frames_follow_the_extent_without_drawing_again() { - struct Observed { - widget: W, - draws: Rc>, - } - impl Widget for Observed { - fn draw(&mut self, painter: &mut Painter) -> Size { - self.draws.set(self.draws.get() + 1); - self.widget.draw(painter) - } - } - struct Frame { - child: StrongWidget, - extent: UiRegion, - } - impl Widget for Frame { - fn draw(&mut self, painter: &mut Painter) -> Size { - painter.widget_at( - &self.child, - UiRegion::FULL, - [Some(self.extent.x), Some(self.extent.y)], - ); - Size::LEFTOVER - } - } - for node in [false, true] { - let plant = |h: &mut Harness, extent| { - let draws = Rc::new(Cell::new(0)); - let leaf = rect(Color::BLUE).masked().add(&mut h.rsc); - h.rsc.widgets_mut().set_region_node(leaf, node); - let fixed = rect(Color::RED).width(31).height(19).add(&mut h.rsc); - let stack = Observed { - widget: Stack { - children: vec![leaf.add_strong(&mut h.rsc), fixed.add_strong(&mut h.rsc)], - size: StackSize::Default, - }, - draws: draws.clone(), - } - .add_strong(&mut h.rsc); - let pad = Observed { - widget: Pad { - inner: stack, - padding: Padding::uniform(7).with_left(13), - }, - draws: draws.clone(), - } - .add_strong(&mut h.rsc); - let root = Frame { child: pad, extent }.add(&mut h.rsc); - h.set_root(root); - (root, leaf, fixed, draws) - }; - let mut warm = Harness::new((403, 211)); - let (root, leaf, fixed, draws) = plant(&mut warm, UiRegion::FULL); - for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] { - let extent = UiRegion::new( - UiSpan::new(Len::rel(start) + Len::px(3.125), Len::rel(end)), - UiSpan::new(Len::px(11.25), Len::rel(end)), - ); - let before = draws.get(); - warm.rsc[root].extent = extent; - warm.frame(); - assert_eq!(draws.get(), before); - let mut cold = Harness::new((403, 211)); - let (_, other, other_fixed, _) = plant(&mut cold, extent); - for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] { - assert_eq!(warm.region(&a), cold.region(&b)); - assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b)); - } - let mask = |h: &Harness, id: WidgetId| { - let active = &h.render.active[&id]; - let mask = &h.rsc.ui().masks[active.mask.idx()]; - h.render - .moves - .resolve(mask.move_idx, mask.region) - .to_px(h.render.output_size()) - }; - assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id())); - } +fn resizing_does_not_remeasure_a_fixed_stack_for_its_unmeasured_overlay() { + let mut h = Harness::new((400, 200)); + let (sizing, _) = counted(&mut h, Size::from((100, 80)), false); + let (overlay, draws) = counted(&mut h, Size::LEFTOVER, true); + h.set_root((sizing, overlay).stack().size(StackSize::Child(0))); + let settled = draws.get(); + + h.resize((800, 300)); + h.frame(); + + assert_eq!(draws.get(), settled); + assert_corners!(h, overlay, (350, 110), (450, 190)); +} + +struct Unmeasured { + child: StrongWidget, + draws: Rc>, +} + +impl Widget for Unmeasured { + fn draw(&mut self, painter: &mut Painter) -> Size { + self.draws.set(self.draws.get() + 1); + painter.widget(&self.child); + Size::LEFTOVER } } #[test] -fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() { - struct Measured; - impl Widget for Measured { - fn draw(&mut self, painter: &mut Painter) -> Size { - let width = painter.px_len(Axis::X); - painter.primitive(RectPrimitive::color(Color::BLUE)); - Size::from((80, if width > Px::from_int(100) { 40 } else { 60 })) - } - } - struct Frame { - child: StrongWidget, - start: f32, - } - impl Widget for Frame { - fn draw(&mut self, painter: &mut Painter) -> Size { - painter.widget_at( - &self.child, - UiRegion::FULL, - [ - Some(UiSpan::new( - Len::px(self.start), - Len::px(self.start + 200.0), - )), - Some(UiSpan::FULL), - ], - ); - Size::LEFTOVER - } - } +fn a_declared_size_change_stops_at_an_independent_parent() { let mut h = Harness::new((400, 200)); - let leaf = Measured.add(&mut h.rsc); - let stack = (leaf,).stack().add_strong(&mut h.rsc); - let root = Frame { - child: stack, - start: 0.0, + let leaf = rect(Color::RED).width(100).add(&mut h.rsc); + let parent = Unmeasured { + child: leaf.add_strong(&mut h.rsc), + draws: Rc::new(Cell::new(0)), + } + .add_strong(&mut h.rsc); + let draws = Rc::new(Cell::new(0)); + h.set_root(Unmeasured { + child: parent, + draws: draws.clone(), + }); + let settled = draws.get(); + + h.set_len(leaf, Axis::X, 150); + h.frame(); + + assert_corners!(h, leaf, (125, 0), (275, 200)); + assert_eq!(draws.get(), settled); +} + +#[test] +fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() { + let mut h = Harness::new((400, 200)); + let draws = Rc::new(Cell::new(0)); + let leaf = ReadsWidth { + draws: draws.clone(), } .add(&mut h.rsc); - h.set_root(root); - assert_corners!(h, leaf, (60, 80), (140, 120)); - h.rsc[root].start = 30.0; + h.set_root((leaf,).stack()); + let settled = draws.get(); + + h.resize((800, 200)); h.frame(); - assert_corners!(h, leaf, (90, 80), (170, 120)); - assert_eq!( - primitive_bounds(&h, leaf.id()), - vec![h.region(&leaf).unwrap()] - ); + + assert!(draws.get() > settled); + assert_corners!(h, leaf, (300, 90), (500, 110)); } #[test] -fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() { - struct Container { - child: StrongWidget, - region: UiRegion, - } - impl Widget for Container { - fn draw(&mut self, painter: &mut Painter) -> Size { - painter - .widget_within(&self.child, DrawRegion::Extent(self.region)) - .size() - } - } - struct Frame { - child: StrongWidget, - extent: UiRegion, - answer: Rc>, - } - impl Widget for Frame { - fn draw(&mut self, painter: &mut Painter) -> Size { - self.answer.set( - painter - .widget_at( - &self.child, - UiRegion::FULL, - [Some(self.extent.x), Some(self.extent.y)], - ) - .size(), - ); - Size::LEFTOVER - } - } - for fractional in [false, true] { - for region in [ - UiRegion::FULL, - UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL), - ] { - let plant = |h: &mut Harness, extent| { - let size = if fractional { - Size { - x: rel(0.5), - y: LayoutLen::px(27), - } - } else { - Size::from((80, 27)) - }; - let (leaf, _) = counted(h, size, !fractional); - let child = Container { - child: leaf.add_strong(&mut h.rsc), - region, - } - .add_strong(&mut h.rsc); - let answer = Rc::new(Cell::new(Size::ZERO)); - let root = Frame { - child, - extent, - answer: answer.clone(), - } - .add(&mut h.rsc); - h.set_root(root); - (root, leaf, answer) - }; - let mut warm = Harness::new((403, 211)); - let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL); - for width in [191.125, 297.25, 83.75] { - let extent = - UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL); - warm.rsc[root].extent = extent; - warm.frame(); - let mut cold = Harness::new((403, 211)); - let (_, other, other_answer) = plant(&mut cold, extent); - assert_eq!(answer.get(), other_answer.get()); - assert_eq!(warm.region(&leaf), cold.region(&other)); - } - } - } +fn changed_drawing_dependencies_reach_ancestors_without_a_size_change() { + let mut h = Harness::new((400, 200)); + let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false); + h.set_root(((leaf,).stack(),).stack()); + + h.rsc[leaf].reads_box = true; + h.frame(); + let settled = draws.get(); + h.resize((800, 200)); + h.frame(); + + assert_eq!(draws.get(), settled + 1); + assert_corners!(h, leaf, (0, 0), (800, 200)); }