diff --git a/core/src/layout_diagnostics.rs b/core/src/layout_diagnostics.rs index 3e9e0e6..47b3976 100644 --- a/core/src/layout_diagnostics.rs +++ b/core/src/layout_diagnostics.rs @@ -54,10 +54,13 @@ pub(crate) enum Counter { TextShapes, TextBreaks, GlyphPlacements, + OutsidePlacement, + OutsideFrame, + OutsideExtent, } impl Counter { - const COUNT: usize = Self::GlyphPlacements as usize + 1; + const COUNT: usize = Self::OutsideExtent as usize + 1; const NAMES: [&'static str; Self::COUNT] = [ "updates", @@ -89,6 +92,9 @@ impl Counter { "text shapes", "text line breaks", "glyph placements", + "reuse outside: the placement it was pinned to", + "reuse outside: a frame length", + "reuse outside: an extent length", ]; } diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 10de432..826f7f4 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -41,7 +41,11 @@ pub struct ActiveData { pub textures: Vec, pub primitives: Vec, pub mask_region: Option, - pub inherited_children: Vec, + /// The children whose box is a part of this widget's extent rather than + /// of its frame, and which part each was given. Moving the extent + /// re-places them, so this widget's drawing does not have to depend on + /// where its own drawing sits. + pub(crate) extent_children: Vec<(WidgetId, ExtentPlacement)>, pub children: Vec, /// The children whose size this widget read while drawing. pub size_deps: Vec, @@ -87,3 +91,22 @@ impl ActiveData { }) } } + +/// What of a container's extent a child was given: the whole of it, for a +/// wrapper whose box is its child's, or a part of it. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum ExtentPlacement { + Inherit, + Within(UiRegion), +} + +impl ExtentPlacement { + /// The child's frame in the container's frame coordinates, and the slot + /// the container chose within it. + pub fn resolve(self, extent: UiRegion) -> (UiRegion, [Option; 2]) { + match self { + Self::Inherit => (UiRegion::FULL, [Some(extent.x), Some(extent.y)]), + Self::Within(part) => (part.within(&extent), [None; 2]), + } + } +} diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 96e06bb..d256d89 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, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, - RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, - UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, + Axis, DrawRegion, ExtentPlacement, 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,7 +40,7 @@ pub struct Painter<'a> { pub(super) textures: Vec, pub(super) primitives: Vec, pub(super) mask_region: Option, - pub(super) inherited_children: Vec, + pub(super) extent_children: Vec<(WidgetId, ExtentPlacement)>, pub(super) extent_own: [Holds; 2], /// Only children whose answers were read constrain this widget's answer. pub(super) answer_under: LayoutHolds, @@ -155,7 +155,13 @@ 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)], true, false) + self.widget_at_inner( + id, + UiRegion::FULL, + [Some(own.x), Some(own.y)], + Some(ExtentPlacement::Inherit), + false, + ) } /// What a widget's rules declare its lengths to be, which whoever draws @@ -171,19 +177,38 @@ 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.inherited_children.retain(|child| *child != id.id()); + self.extent_children.retain(|(child, _)| *child != id.id()); self.state.undraw_rec(id.id(), self.rsc); } /// 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. + /// + /// `DrawRegion::Extent` gives a part of where this widget's own drawing + /// sits instead, which is what a container whose children belong inside + /// its drawing rather than inside the box it was offered wants. The + /// child's box then follows the extent without this widget's drawing + /// depending on where that extent is, so moving it re-places the child + /// rather than drawing this widget again. pub fn widget_within<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, - region: UiRegion, + region: impl Into, ) -> DrawResult<'s, 'a, W> { - self.widget_at(id, region, [None; 2]) + match region.into() { + DrawRegion::Frame(region) => self.widget_at(id, region, [None; 2]), + DrawRegion::Extent(part) => { + let within = part.within(&self.placement); + self.widget_at_inner( + id, + within, + [None; 2], + Some(ExtentPlacement::Within(part)), + false, + ) + } + } } /// Draws a widget in `region`, saying where in it the drawing goes. @@ -206,7 +231,7 @@ impl<'a> Painter<'a> { region: UiRegion, placement: [Option; 2], ) -> DrawResult<'s, 'a, W> { - self.widget_at_inner(id, region, placement, false, false) + self.widget_at_inner(id, region, placement, None, false) } fn widget_at_inner<'s, W: ?Sized>( @@ -214,15 +239,12 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, region: UiRegion, placement: [Option; 2], - inherited: bool, + extent: Option, measuring: bool, ) -> DrawResult<'s, 'a, W> { - if inherited { - if !self.inherited_children.contains(&id.id()) { - self.inherited_children.push(id.id()); - } - } else { - self.inherited_children.retain(|child| *child != id.id()); + self.extent_children.retain(|(child, _)| *child != id.id()); + if let Some(extent) = extent { + self.extent_children.push((id.id(), extent)); } let region_node = self.rsc.widgets().is_region_node(id.id()); let declared = self.declared_lens(id); @@ -289,25 +311,57 @@ impl<'a> Painter<'a> { 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); + let chosen = placement[n].unwrap_or(UiSpan::FULL).len(); + match extent { + // Its box is this widget's own, so what its drawing holds + // for is what this widget's extent holds for. + Some(ExtentPlacement::Inherit) if declared[n].is_none() => { + result.frame[n] = holds.frame[n].through(local.axis(axis).len()); + result.extent[n] = holds.extent[n]; + if holds.placement.is_some() { + result.placement = Some(self.placement); + } + } + // Its box is a part of this widget's extent, so both what + // it was given and what it took of that are ranges on the + // extent and none of them a range on the frame. That is + // what lets this widget's drawing move without being made + // again: only the part's length reaches the child, and + // where the part sits is re-placed rather than redrawn. + Some(ExtentPlacement::Within(part)) if declared[n].is_none() => { + result.extent[n] = holds.frame[n] + .and(holds.extent[n].through(chosen)) + .through(part.axis(axis).len()); + } + // A declared length is a length of this widget's frame + // wherever the box it sits in came from, so what the child + // holds for is a range on the frame either way. + _ => { + result.frame[n] = holds.frame[n].through(local.axis(axis).len()).and( + holds.extent[n] + .through(chosen) + .through(local.axis(axis).len()), + ); } - } 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()), - ); } } result }; self.under = self.under.and(in_parent(holds)); - let answer_holds = in_parent(answer_holds); + let mut answer_holds = in_parent(answer_holds); + // What it reports is a fraction of the box it was given, and that box + // is a part of this widget's extent -- so the same fraction is a + // different length once the extent is. Only the report: where the + // extent moved without changing what it holds, the drawing under it + // is re-placed rather than made again, which is what the extent ask + // is for. Pixels come up unchanged and say nothing. + if matches!(extent, Some(ExtentPlacement::Within(_))) + && AXES.into_iter().any(|axis| { + declared[axis as usize].is_none() && size.axis(axis).rel != crate::Rel::ZERO + }) + { + answer_holds.placement = Some(self.placement); + } DrawResult { child: id, painter: self, @@ -369,7 +423,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, false, true) + .widget_at_inner(child, region, offered, None, true) .len(axis); }; #[cfg(feature = "layout-diagnostics")] diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index b24a8f3..4d169c2 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -288,7 +288,7 @@ impl UiRenderState { && let Some(old_parent) = self.active.get_mut(&old_parent) { old_parent.children.retain(|child| *child != id); - old_parent.inherited_children.retain(|child| *child != id); + old_parent.extent_children.retain(|(child, _)| *child != id); } (answer.0, answer.1, settled.1) } @@ -361,7 +361,7 @@ impl UiRenderState { textures: Vec::new(), primitives: Vec::new(), mask_region: None, - inherited_children: Vec::new(), + extent_children: Vec::new(), children: Vec::new(), offered: Vec::new(), offered_px: info.offered_px, @@ -398,7 +398,7 @@ impl UiRenderState { textures, primitives, mask_region, - inherited_children, + extent_children, extent_own, answer_under, children, @@ -503,7 +503,7 @@ impl UiRenderState { textures, primitives, mask_region, - inherited_children, + extent_children, children, size_deps, declared: declared_lens(rsc.widgets(), id), @@ -680,6 +680,24 @@ impl UiRenderState { if !active.holds.contains(info.px, placement) { #[cfg(feature = "layout-diagnostics")] { + // Which of the three said no, so a frame that redraws more + // than it should says where to look. They overlap: a drawing + // can be outside two of them at once. + let holds = active.holds; + if holds.placement.is_some_and(|pinned| pinned != placement) { + diag::bump(Counter::OutsidePlacement); + } + for axis in AXES { + let n = axis as usize; + if !holds.frame[n].contains(info.px.axis(axis)) { + diag::bump(Counter::OutsideFrame); + } + if !holds.extent[n] + .contains(placement.axis(axis).len().to_px(info.px.axis(axis))) + { + diag::bump(Counter::OutsideExtent); + } + } diag::bump(Counter::ReuseOutside); diag::reuse(id, ReuseOutcome::Outside); } @@ -752,19 +770,18 @@ impl UiRenderState { } let parent_move = active.move_idx; let mask = active.mask; - let children = active.inherited_children.len(); + let children = active.extent_children.len(); for index in 0..children { - let child = self.active[&id].inherited_children[index]; + let (child, extent) = self.active[&id].extent_children[index]; + let (part, slot) = extent.resolve(placement); let active = &self.active[&child]; - let (child_local, chosen) = ask_box( - UiRegion::FULL, - active.declared, - active.own_align, - [Some(placement.x), Some(placement.y)], - ); + let (child_local, chosen) = ask_box(part, active.declared, active.own_align, slot); + // What it took of that box is its own answer, which this move did + // not ask again: keep the placement it has on any axis this + // widget is not the one choosing. let child_placement = UiRegion { - x: chosen[0].unwrap_or(UiSpan::FULL), - y: chosen[1].unwrap_or(UiSpan::FULL), + x: chosen[0].unwrap_or(active.placement.x), + y: chosen[1].unwrap_or(active.placement.y), }; let child_info = DrawInfo { layer: active.layer, @@ -924,7 +941,7 @@ impl UiRenderState { textures: Vec::new(), primitives: Vec::new(), mask_region: None, - inherited_children: Vec::new(), + extent_children: Vec::new(), children: Vec::new(), size_deps: Vec::new(), move_idx: info.parent_move, diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 1257c26..5782b02 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 = self.padding.region_of(painter.placement()); + let inside = DrawRegion::Extent(self.padding.region()); 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 2999b96..a14e389 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -13,7 +13,6 @@ 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 @@ -24,13 +23,7 @@ impl Widget for Stack { // drawing belongs to the layer it was made on. Some((i, child)) => { painter.child_layer_at(i); - painter - .widget_at( - child, - UiRegion::FULL, - [Some(placement.x), Some(placement.y)], - ) - .size() + painter.widget(child).size() } None => Size::LEFTOVER, }; @@ -42,7 +35,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, placement); + painter.widget_within(child, DrawRegion::Extent(UiRegion::FULL)); } size } diff --git a/tests/cases/retained.rs b/tests/cases/retained.rs index 2052e5b..cd09686 100644 --- a/tests/cases/retained.rs +++ b/tests/cases/retained.rs @@ -1127,3 +1127,211 @@ fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() { h.frame(); assert_eq!(leaf_draws.get(), settled + 1); } +#[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())); + } + } +} + +#[test] +fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() { + struct Measured; + impl Widget for Measured { + fn draw(&mut self, painter: &mut Painter) -> Size { + let width = painter.px_len(Axis::X); + painter.primitive(RectPrimitive::color(Color::BLUE)); + Size::from((80, if width > Px::from_int(100) { 40 } else { 60 })) + } + } + struct Frame { + child: StrongWidget, + start: f32, + } + impl Widget for Frame { + fn draw(&mut self, painter: &mut Painter) -> Size { + painter.widget_at( + &self.child, + UiRegion::FULL, + [ + Some(UiSpan::new( + Len::px(self.start), + Len::px(self.start + 200.0), + )), + Some(UiSpan::FULL), + ], + ); + Size::LEFTOVER + } + } + let mut h = Harness::new((400, 200)); + let leaf = Measured.add(&mut h.rsc); + let stack = (leaf,).stack().add_strong(&mut h.rsc); + let root = Frame { + child: stack, + start: 0.0, + } + .add(&mut h.rsc); + h.set_root(root); + assert_corners!(h, leaf, (60, 80), (140, 120)); + h.rsc[root].start = 30.0; + h.frame(); + assert_corners!(h, leaf, (90, 80), (170, 120)); + assert_eq!( + primitive_bounds(&h, leaf.id()), + vec![h.region(&leaf).unwrap()] + ); +} + +#[test] +fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() { + struct Container { + child: StrongWidget, + region: UiRegion, + } + impl Widget for Container { + fn draw(&mut self, painter: &mut Painter) -> Size { + painter + .widget_within(&self.child, DrawRegion::Extent(self.region)) + .size() + } + } + struct Frame { + child: StrongWidget, + extent: UiRegion, + answer: Rc>, + } + 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)); + } + } + } +}