From efb416bbc3eaf3dabaf7ad0131a9255dcab05e45 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Thu, 17 Sep 2026 14:50:09 -0400 Subject: [PATCH] Retain frame and extent dependencies independently Keep the original measurement placement separate from the assigned slot. Validate frame and extent lengths before reusing an answer or drawing, and represent hint-only records as having no measured answer. Retain primitive and mask coordinates with their frame/extent reference. Forwarded children follow a reused wrapper's placement without rerunning valid draw bodies. Keep the single Widget::draw API. Restore the eight failing suite cases from the region/placement prototype, with regressions for mixed coordinate references, a changed inherited extent, the sizing-stack fraction, and an undrawn share becoming visible. This remains experimental: nested container updates do substantially more work than e44dea3 despite restoring the leaf and wrapper reuse guarantees. Do not merge it as a performance improvement. --- core/src/render/mod.rs | 3 +- core/src/ui/active.rs | 39 +++--- core/src/ui/draw_region.rs | 36 ++++++ core/src/ui/layout_holds.rs | 33 +++++ core/src/ui/mod.rs | 4 + core/src/ui/painter.rs | 181 ++++++++++++++++---------- core/src/ui/render_state.rs | 234 +++++++++++++++++++++++++--------- src/widget/mask.rs | 3 +- src/widget/position/scroll.rs | 10 +- src/widget/position/span.rs | 2 +- src/widget/text/mod.rs | 3 +- tests/cases/layout.rs | 17 +++ tests/cases/retained.rs | 91 +++++++++++-- tests/cases/unsettled.rs | 58 +++++++++ 14 files changed, 548 insertions(+), 166 deletions(-) create mode 100644 core/src/ui/draw_region.rs create mode 100644 core/src/ui/layout_holds.rs diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 09ae52b..bdcaf1f 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -106,7 +106,8 @@ impl UiRenderNode { self.active.push(i); for change in draws.apply_free() { if let Some(inst) = ui_render.active.get_mut(&change.id) { - for h in &mut inst.primitives { + for primitive in &mut inst.primitives { + let h = &mut primitive.handle; if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { h.inst_idx = change.new; break; diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 4a2d0b8..73c4a3b 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,6 +1,6 @@ use crate::{ - Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, - UiRegion, UiVec2, WidgetId, + DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive, + Size, TextureHandle, UiRegion, UiVec2, WidgetId, }; /// What is kept of a widget its parent has asked about. `drawn` says whether @@ -15,10 +15,6 @@ pub struct ActiveData { pub region: UiRegion, /// Where its drawing sits inside that box, in the box's own coordinates. pub placement: UiRegion, - /// Whether its drawing read that placement, which is what says whether - /// moving it within the region is a redraw or only a different claim on - /// the same drawing. - pub reads_placement: bool, /// The same box as lengths of its parent's box, which is the one route /// to a box in pixels: a draw threads these down a level at a time, and /// [`crate::UiRenderState::redraw`] takes the same steps back up. @@ -29,13 +25,14 @@ pub struct ActiveData { /// asked again -- and a chain of fractions has no frame in it, which is /// why a region node between two widgets cannot break it. pub offer_len: UiVec2, - /// What it answered there: the size and what that held for. - pub answer: (Size, [Holds; 2]), + pub offer_placement: [Option; 2], + /// The measured answer and its dependencies. A hint-only dependency or + /// a widget first encountered during placement has no measurement yet. + pub answer: Option<(Size, LayoutHolds)>, /// What the widget said it used of its box, the last time it drew. pub size: Size, - /// The pixel lengths of `region`, per axis, that its drawing and `size` - /// hold for. - pub holds: [Holds; 2], + /// The frame, extent and explicit placement reads that this drawing holds for. + pub holds: LayoutHolds, pub drawn: bool, pub parent: Option, /// How far down the tree it was drawn, the root being 1. Carried down a @@ -43,7 +40,9 @@ pub struct ActiveData { /// widget a frame visits and cannot drift while one is being drawn. pub depth: usize, pub textures: Vec, - pub primitives: Vec, + pub primitives: Vec, + pub mask_region: Option, + pub inherited_children: Vec, pub children: Vec, /// The children whose size this widget read while drawing. pub size_deps: Vec, @@ -74,16 +73,18 @@ pub struct ActiveData { } impl ActiveData { - /// Whether its drawing and size hold for a box of these pixel lengths. - pub fn holds_at(&self, px: crate::PxVec2) -> bool { - self.holds[0].contains(px.x) && self.holds[1].contains(px.y) - } - /// Whether what it answered still stands for a box of these pixel /// lengths -- the box it was asked in, where `holds` is about the box its /// answer then chose. pub fn answers_at(&self, px: crate::PxVec2) -> bool { - let (_, holds) = self.answer; - holds[0].contains(px.x) && holds[1].contains(px.y) + self.answer.is_some_and(|(_, holds)| { + holds.contains( + px, + UiRegion { + x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL), + y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL), + }, + ) + }) } } diff --git a/core/src/ui/draw_region.rs b/core/src/ui/draw_region.rs new file mode 100644 index 0000000..a3bf5df --- /dev/null +++ b/core/src/ui/draw_region.rs @@ -0,0 +1,36 @@ +use crate::{PrimitiveHandle, UiRegion}; + +/// Retains which box geometry follows when only the extent changes. +#[derive(Clone, Copy, Debug)] +pub enum DrawRegion { + Frame(UiRegion), + Extent(UiRegion), +} + +impl DrawRegion { + pub(crate) fn resolve(self, frame: UiRegion, extent: UiRegion) -> UiRegion { + match self { + Self::Frame(local) => local.within(&frame), + Self::Extent(local) => local.within(&extent).within(&frame), + } + } + + pub(crate) fn map(self, f: impl FnOnce(UiRegion) -> UiRegion) -> Self { + match self { + Self::Frame(local) => Self::Frame(f(local)), + Self::Extent(local) => Self::Extent(f(local)), + } + } +} + +impl From for DrawRegion { + fn from(region: UiRegion) -> Self { + Self::Frame(region) + } +} + +#[derive(Debug)] +pub struct RetainedPrimitive { + pub handle: PrimitiveHandle, + pub region: DrawRegion, +} diff --git a/core/src/ui/layout_holds.rs b/core/src/ui/layout_holds.rs new file mode 100644 index 0000000..ef0bff6 --- /dev/null +++ b/core/src/ui/layout_holds.rs @@ -0,0 +1,33 @@ +use crate::{Axis, Holds, PxVec2, UiRegion}; + +/// Dependencies of one evaluation, before the frame and extent are composed. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct LayoutHolds { + pub frame: [Holds; 2], + pub extent: [Holds; 2], + pub placement: Option, +} + +impl LayoutHolds { + pub const ANY: Self = Self { + frame: [Holds::ANY; 2], + extent: [Holds::ANY; 2], + placement: None, + }; + + pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool { + self.placement.is_none_or(|old| old == placement) + && [Axis::X, Axis::Y].into_iter().all(|axis| { + self.frame[axis as usize].contains(px.axis(axis)) + && self.extent[axis as usize] + .contains(placement.axis(axis).len().to_px(px.axis(axis))) + }) + } + + pub fn in_frame(self, placement: UiRegion) -> [Holds; 2] { + [Axis::X, Axis::Y].map(|axis| { + self.frame[axis as usize] + .and(self.extent[axis as usize].through(placement.axis(axis).len())) + }) + } +} diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 3e8e01e..563691b 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -10,12 +10,16 @@ use crate::{ pub const CHAIN_LIMIT: u32 = 64; mod active; +mod draw_region; mod holds; +mod layout_holds; mod painter; mod render_state; pub use active::*; +pub use draw_region::*; pub use holds::*; +pub use layout_holds::*; pub use painter::{Painter, PrimitiveLike}; pub use render_state::*; diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 76133b4..96b0bb2 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,12 +1,12 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ - Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget, - TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, - Weight, WidgetId, Widgets, + Axis, DrawRegion, Holds, 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, PrimitiveHandle, PrimitiveInst, - PrimitiveKind, TexturePrimitive, + GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, + TexturePrimitive, }, ui::render_state::DrawInfo, }; @@ -38,7 +38,11 @@ pub struct Painter<'a> { pub(super) px: PxVec2, pub(super) mask: MaskIdx, pub(super) textures: Vec, - pub(super) primitives: Vec, + pub(super) primitives: Vec, + pub(super) mask_region: Option, + pub(super) inherited_children: Vec, + pub(super) extent_own: [Holds; 2], + pub(super) extent_under: [Holds; 2], 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. @@ -69,13 +73,13 @@ pub struct Painter<'a> { } impl<'a> Painter<'a> { - fn primitive_at(&mut self, primitive: P, region: UiRegion) { + fn primitive_at(&mut self, primitive: P, region: DrawRegion) { let kind = self.rsc.ui_mut().primitives.kind::

(); self.write(kind, primitive, region); } /// Takes the kind, for a caller writing many of one primitive. - fn write(&mut self, kind: PrimitiveKind

, primitive: P, region: UiRegion) { + fn write(&mut self, kind: PrimitiveKind

, primitive: P, region: DrawRegion) { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::PrimitiveWrites); let h = self.state.layers.write( @@ -84,15 +88,15 @@ impl<'a> Painter<'a> { kind, id: self.id, primitive, - region, + region: region.resolve(self.region, self.placement), mask_idx: self.mask, move_idx: self.move_idx, }, ); - self.push_primitive(h); + self.push_primitive(RetainedPrimitive { handle: h, region }); } - fn push_primitive(&mut self, h: PrimitiveHandle) { + fn push_primitive(&mut self, h: RetainedPrimitive) { if self.mask != MaskIdx::NONE { // TODO: I have no clue if this works at all :joy: self.rsc.ui_mut().masks.push_ref(self.mask); @@ -102,24 +106,29 @@ impl<'a> Painter<'a> { /// Writes a primitive over the whole of this widget's own box. pub fn primitive(&mut self, primitive: impl PrimitiveLike) { - let at = self.placed(); + let at = DrawRegion::Extent(UiRegion::FULL); let primitive = primitive.into_primitive(self); self.primitive_at(primitive, at) } - /// Writes one somewhere in this widget's region. A widget that wants its - /// own box rather than the region it was given composes through - /// [`Self::placement`] first. - pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { + /// Writes in the frame by default. `DrawRegion::Extent` keeps the local + /// geometry attached to this widget's box without reading its placement. + pub fn primitive_within( + &mut self, + primitive: impl PrimitiveLike, + region: impl Into, + ) { let primitive = primitive.into_primitive(self); - self.primitive_at(primitive, region.within(&self.region)); + self.primitive_at(primitive, region.into()); } - /// `region` is in this widget's own region, as every region it writes is. - pub fn set_mask(&mut self, region: UiRegion) { + /// Sets a mask in the selected frame or extent coordinates. + pub fn set_mask(&mut self, region: impl Into) { + let region = region.into(); + self.mask_region = Some(region); assert!(self.mask == MaskIdx::NONE); self.mask = self.rsc.ui_mut().masks.push(Mask { - region: region.within(&self.region), + region: region.resolve(self.region, self.placement), move_idx: self.move_idx, }); } @@ -129,8 +138,8 @@ impl<'a> Painter<'a> { /// where this widget was put. What a container that is only a wrapper /// around one child wants, since its box is the child's. pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget) -> DrawResult<'s, 'a, W> { - let own = self.placement(); - self.widget_at(id, UiRegion::FULL, [Some(own.x), Some(own.y)]) + let own = self.placement; + self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true) } /// What a widget's rules declare its lengths to be, which whoever draws @@ -146,6 +155,7 @@ 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.state.undraw_rec(id.id(), self.rsc); } @@ -181,6 +191,23 @@ impl<'a> Painter<'a> { region: UiRegion, placement: [Option; 2], ) -> DrawResult<'s, 'a, W> { + self.widget_at_inner(id, region, placement, false) + } + + fn widget_at_inner<'s, W: ?Sized>( + &'s mut self, + id: &'s StrongWidget, + region: UiRegion, + placement: [Option; 2], + inherited: 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()); + } 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()); @@ -208,11 +235,16 @@ impl<'a> Painter<'a> { .get(&id.id()) .map_or(given_len, |a| a.offer_len), }; + let offer_placement = if first_ask { + placement + } else { + self.state + .active + .get(&id.id()) + .map_or(placement, |a| a.offer_placement) + }; let px = given_len.to_px(self.px); let offered_px = offer_len.to_px(self.offered_px); - // Whether this ask is the child's offer question, which is a question - // about lengths: the same lengths somewhere else is the same question. - let answers_offer = self.at_offer && px == offered_px; // The answer and what it holds for, both about the box asked in. The // 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. @@ -228,6 +260,7 @@ impl<'a> Painter<'a> { mask: self.mask, given_len, offer_len, + offer_placement, px, offered_px, placement, @@ -235,13 +268,23 @@ impl<'a> Painter<'a> { None, self.rsc, ); - if answers_offer { - self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds); - } // Whatever the child's answer holds for keeps this one to the boxes // that give the child a length inside it. - for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { - *under = under.and(holds[axis as usize].through(local.axis(axis).len())); + for axis in AXES { + let n = axis as usize; + let frame = holds.frame[n].through(local.axis(axis).len()); + self.under[n] = self.under[n].and(frame); + if inherited && declared[n].is_none() { + self.extent_under[n] = self.extent_under[n].and(holds.extent[n]); + self.reads_placement |= holds.placement.is_some(); + } else { + let extent = placement[n].unwrap_or(UiSpan::FULL).len(); + self.under[n] = self.under[n].and( + holds.extent[n] + .through(extent) + .through(local.axis(axis).len()), + ); + } } DrawResult { child: id, @@ -286,28 +329,38 @@ impl<'a> Painter<'a> { child: &StrongWidget, axis: Axis, region: UiRegion, + placement: [Option; 2], ) -> Option { let declared = self.declared_lens(child); let align = self.rsc.widgets().alignment(child.id()); - let (local, _) = ask_box(region, declared, align, [None; 2]); - let first_ask = self.offer(child.id()); - if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) { - active.offer_len = local.size(); - } + let (local, placement) = ask_box(region, declared, align, placement); + let first_ask = self.at_offer && !self.offered.contains(&child.id()); + if let Some(hint) = self.size_hint(child, axis) { return Some(hint); } let px = local.size().to_px(self.px); - let (size, holds) = - self.state - .retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?; + let (size, holds) = self.state.retained_size( + child.id(), + px, + placement, + self.move_idx, + self.rsc.widgets(), + )?; #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::RetainedSizeHits); self.depend_on(child); if first_ask { + self.offered.push(child.id()); let active = self.state.active.get_mut(&child.id()).unwrap(); - active.answer = (size, holds); + active.offer_len = local.size(); + active.offer_placement = placement; } + let placement = UiRegion { + x: placement[0].unwrap_or(UiSpan::FULL), + y: placement[1].unwrap_or(UiSpan::FULL), + }; + let holds = holds.in_frame(placement); for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { *under = under.and(holds[axis as usize].through(local.axis(axis).len())); } @@ -343,22 +396,24 @@ impl<'a> Painter<'a> { ui.text.render(buffer, attrs, width) } - /// `origin` is in this widget's own region, as every region it writes is. + /// Writes glyphs in the selected frame or extent coordinates. // TODO: merge the text methods into the primitive ones. - pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { - let origin = origin.within(&self.region); + pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into) { + let origin = origin.into(); let kind = self.rsc.ui_mut().primitives.kind::(); for glyph in text.glyphs.iter() { - let mut region = origin; - region.x.end = region.x.start; - region.y.end = region.y.start; - let mut region = region.offset(UiVec2::from_px(glyph.offset)); - let size = PxVec2::new( - Px::from_int(glyph.entry.width as i32), - Px::from_int(glyph.entry.height as i32), - ); - region.x.end = region.x.start.offset(size.x); - region.y.end = region.y.start.offset(size.y); + let region = origin.map(|mut region| { + region.x.end = region.x.start; + region.y.end = region.y.start; + let mut region = region.offset(UiVec2::from_px(glyph.offset)); + let size = PxVec2::new( + Px::from_int(glyph.entry.width as i32), + Px::from_int(glyph.entry.height as i32), + ); + region.x.end = region.x.start.offset(size.x); + region.y.end = region.y.start.offset(size.y); + region + }); self.write( kind, GlyphPrimitive { @@ -393,15 +448,6 @@ impl<'a> Painter<'a> { self.placement } - /// This widget's own box in the coordinates its primitives are written - /// in: its placement composed through the region it was given. - fn placed(&mut self) -> UiRegion { - match self.placement() == UiRegion::FULL { - true => self.region, - false => self.placement.within(&self.region), - } - } - /// 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. @@ -444,11 +490,11 @@ impl<'a> Painter<'a> { /// One axis of this widget's own box in pixels. Prefer this to /// [`Self::px_size`] when the other axis cannot affect the drawing. pub fn px_len(&mut self, axis: Axis) -> Px { - let part = self.placement().axis(axis).len(); + let part = self.placement.axis(axis).len(); let len = part.to_px(self.px.axis(axis)); - let own = &mut self.own[axis as usize]; + let own = &mut self.extent_own[axis as usize]; if *own == Holds::ANY { - *own = Holds::at(len).through(part); + *own = Holds::at(len); } len } @@ -458,7 +504,7 @@ impl<'a> Painter<'a> { /// of the box, and the same reported size. A widget that read its length /// in pixels holds for that one alone until it says otherwise. pub fn holds(&mut self, axis: Axis, holds: impl Into) { - let part = self.placement().axis(axis).len(); + let part = self.placement.axis(axis).len(); let holds = holds.into(); debug_assert!( holds.contains(part.to_px(self.px.axis(axis))), @@ -466,10 +512,7 @@ impl<'a> Painter<'a> { self.label(), self.id ); - // Kept as a range of the region's lengths, which is the one variable - // every range here is about: its own box is a part of that box, and - // `through` is the exact preimage of taking the part. - self.own[axis as usize] = holds.through(part); + self.extent_own[axis as usize] = holds; } /// One axis of the box this widget's parent gave it, in pixels -- what a diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 206c6f3..f465f90 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -2,9 +2,9 @@ use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::ui::painter::{ask_box, declared_lens, placed_box, placed_lens}; use crate::{ - ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, - PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight, - WidgetId, Widgets, + ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, + Moves, Painter, PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, + UiVec2, Weight, WidgetId, Widgets, util::{HashMap, Vec2}, }; @@ -26,6 +26,7 @@ pub(super) struct DrawInfo { /// carries them unchanged while its own region is the placement inside. pub given_len: UiVec2, pub offer_len: UiVec2, + pub offer_placement: [Option; 2], /// This ask's box in pixels, and the offer's: one multiply from the /// parent's own, which is where every pixel length in layout comes from. pub px: PxVec2, @@ -142,6 +143,7 @@ impl UiRenderState { mask: MaskIdx::NONE, given_len: region.size(), offer_len: UiVec2::FULL_SIZE, + offer_placement: [None; 2], px, offered_px: px, placement: [None; 2], @@ -214,7 +216,7 @@ impl UiRenderState { info: DrawInfo, mut old: Option, rsc: &mut dyn UiRsc, - ) -> (Size, [Holds; 2]) { + ) -> (Size, LayoutHolds) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); @@ -230,7 +232,7 @@ impl UiRenderState { true => None, false => self .retained_answer(id, info) - .or_else(|| self.try_reuse(id, region, info, rsc)), + .or_else(|| self.try_reuse(id, region, info.offered_placement(), info, rsc)), }; let answer = retained.unwrap_or_else(|| { if old.is_none() { @@ -252,14 +254,23 @@ impl UiRenderState { }; self.place(id, region, placement, info, rsc); - // The answer is only reusable while both parts of the operation are: - // what the widget reported, and what it drew once placed. Both are - // ranges of the region's own lengths, since that is the box neither - // ask changes. + // On axes chosen by the parent, measurement and drawing share an + // extent. Otherwise the answer fixes the final extent as a function + // of the frame, so pull that drawing's validity back through it. let drawing_holds = self.active[&id].holds; let mut settled = answer; for axis in AXES { - settled.1[axis as usize] = settled.1[axis as usize].and(drawing_holds[axis as usize]); + let n = axis as usize; + settled.1.frame[n] = settled.1.frame[n].and(drawing_holds.frame[n]); + if info.placement[n].is_some() { + settled.1.extent[n] = settled.1.extent[n].and(drawing_holds.extent[n]); + } else { + settled.1.frame[n] = settled.1.frame[n] + .and(drawing_holds.extent[n].through(placement.axis(axis).len())); + } + } + if drawing_holds.placement.is_some() && info.placement.iter().any(Option::is_some) { + settled.1.placement = Some(info.offered_placement()); } let active = self.active.get_mut(&id).unwrap(); @@ -269,7 +280,10 @@ impl UiRenderState { active.region = region; active.given_len = info.given_len; active.offer_len = info.offer_len; - active.answer = settled; + if info.placement == info.offer_placement && info.px == info.offered_px { + active.answer = Some(settled); + active.offer_placement = info.offer_placement; + } active.decided = info.decided(); active.own_align = align; // A subtree can be reused whole under a different parent -- same box, @@ -283,14 +297,12 @@ 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); } settled } - /// Puts the drawing where the answer says it goes. A drawing that never - /// read its placement is the same drawing wherever it is put, so all that - /// changes is what box the widget is recorded as occupying; one that read - /// it is drawn again, and only where the placement it read has moved. + /// Recompose retained geometry when the evaluation still holds at this extent. fn place( &mut self, id: WidgetId, @@ -299,15 +311,7 @@ impl UiRenderState { info: DrawInfo, rsc: &mut dyn UiRsc, ) { - let active = &self.active[&id]; - // A drawing that never read its placement is the same drawing - // wherever it is put, so only what the widget is recorded as - // occupying changes; one that read it stands only for the placement - // it read. Either way the drawing still has to be where the region - // now is, which is what a retained answer on its own does not do. - let stands = !active.reads_placement || active.placement == placement; - if stands && self.try_reuse(id, region, info, rsc).is_some() { - self.active.get_mut(&id).unwrap().placement = placement; + if self.try_reuse(id, region, placement, info, rsc).is_some() { return; } #[cfg(feature = "layout-diagnostics")] @@ -325,7 +329,7 @@ impl UiRenderState { info: DrawInfo, old: Option, rsc: &mut dyn UiRsc, - ) -> (Size, [Holds; 2]) { + ) -> (Size, LayoutHolds) { 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. @@ -339,16 +343,19 @@ impl UiRenderState { false => (info.parent_move, region, self.slots.remove(&id)), }; let (old_children, old_answer) = match old { - Some(old) => (old.children, Some(old.answer)), + Some(old) => (old.children, old.answer), None => (Vec::new(), None), }; rsc.widgets_mut().needs_redraw.remove(&id); - // A box of the offered lengths asks the offer's question wherever it - // sits, since what a drawing depends on is its lengths -- and - // equality is the comparison, these being counts of a step rather - // than floats to be compared for nearness. + // Only evaluation at the original offer establishes the children's + // offers. A placing evaluation must not overwrite that question. let px = info.px; - let at_offer = px == info.offered_px; + let at_offer = px == info.offered_px + && placement + == UiRegion { + x: info.offer_placement[0].unwrap_or(UiSpan::FULL), + y: info.offer_placement[1].unwrap_or(UiSpan::FULL), + }; let mut painter = Painter { state: self, @@ -362,6 +369,8 @@ impl UiRenderState { id, textures: Vec::new(), primitives: Vec::new(), + mask_region: None, + inherited_children: Vec::new(), children: Vec::new(), offered: Vec::new(), offered_px: info.offered_px, @@ -369,6 +378,8 @@ impl UiRenderState { size_deps: Vec::new(), own: [Holds::ANY; 2], under: [Holds::ANY; 2], + extent_own: [Holds::ANY; 2], + extent_under: [Holds::ANY; 2], depth: info.depth, move_idx, rsc, @@ -395,6 +406,10 @@ impl UiRenderState { mask, textures, primitives, + mask_region, + inherited_children, + extent_own, + extent_under, children, offered: _, offered_px: _, @@ -432,9 +447,16 @@ impl UiRenderState { "'{}' ({id:?}) clips to {px:?} and reports {size}", rsc.widgets().label(id), ); - let holds = [own[0].and(under[0]), own[1].and(under[1])]; + 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]), + ], + placement: reads_placement.then_some(placement), + }; debug_assert!( - holds[0].contains(px.x) && holds[1].contains(px.y), + holds.contains(px, placement), "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", rsc.widgets().label(id), ); @@ -463,6 +485,7 @@ impl UiRenderState { mask, given_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE, + offer_placement: [None; 2], px, offered_px: px, placement: [None; 2], @@ -477,11 +500,11 @@ impl UiRenderState { id, region, placement, - reads_placement, given_len: info.given_len, offer_len: info.offer_len, + offer_placement: info.offer_placement, // Whoever asked writes the answer, if this was the asking. - answer: old_answer.unwrap_or((size, holds)), + answer: old_answer, size, holds, drawn: true, @@ -489,6 +512,8 @@ impl UiRenderState { depth: info.depth, textures, primitives, + mask_region, + inherited_children, children, size_deps, declared: declared_lens(rsc.widgets(), id), @@ -532,18 +557,24 @@ impl UiRenderState { &self, id: WidgetId, px: PxVec2, + placement: [Option; 2], parent_move: MoveIdx, widgets: &Widgets, - ) -> Option<(Size, [Holds; 2])> { + ) -> Option<(Size, LayoutHolds)> { if widgets.needs_redraw.contains(&id) { return None; } let active = self.active.get(&id)?; - let (size, holds) = active.answer; + let (size, holds) = active.answer?; let valid = active.drawn && active.parent_move == parent_move - && holds[0].contains(px.x) - && holds[1].contains(px.y); + && holds.contains( + px, + UiRegion { + x: placement[0].unwrap_or(UiSpan::FULL), + y: placement[1].unwrap_or(UiSpan::FULL), + }, + ); valid.then_some((size, holds)) } @@ -551,7 +582,7 @@ impl UiRenderState { /// drawing ended up. Alignment is exactly that case: the first box is the /// question and the smaller placed box holds the drawing. Whether the /// answer is stale at all is its caller's question, asked once there. - fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, [Holds; 2])> { + fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, LayoutHolds)> { let active = self.active.get(&id)?; let has_region_node = active.move_idx != active.parent_move; if !active.drawn @@ -560,7 +591,11 @@ impl UiRenderState { { return None; } - active.answers_at(info.px).then_some(active.answer) + let answer = active.answer?; + answer + .1 + .contains(info.px, info.offered_placement()) + .then_some(answer) } /// The pixel lengths of the box a widget was given and of the box it was @@ -600,9 +635,10 @@ impl UiRenderState { &mut self, id: WidgetId, region: UiRegion, + placement: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc, - ) -> Option<(Size, [Holds; 2])> { + ) -> Option<(Size, LayoutHolds)> { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReuseAttempts); if rsc.widgets().needs_redraw.contains(&id) { @@ -651,7 +687,7 @@ impl UiRenderState { // In pixels, because `region` is a fraction of the box its parent // drew in and that box may be what changed -- an unchanged fraction // of a box half the size is half the widget. - if !active.holds_at(info.px) { + if !active.holds.contains(info.px, placement) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseOutside); @@ -659,6 +695,7 @@ impl UiRenderState { } return None; } + let extent_moved = active.placement != placement; let moved = active.region != region; let (answer, old_region, slot) = ((active.size, active.holds), active.region, active.move_idx); @@ -670,6 +707,9 @@ impl UiRenderState { self.remap_subtree(id, &remap, info.parent_move, rsc); } } + if extent_moved { + self.reposition(id, region, placement, info, rsc); + } self.redepth(id, info.depth); let active = self.active.get_mut(&id).unwrap(); active.region = region; @@ -698,6 +738,70 @@ impl UiRenderState { Some(answer) } + fn reposition( + &mut self, + id: WidgetId, + region: UiRegion, + placement: UiRegion, + info: DrawInfo, + rsc: &mut dyn UiRsc, + ) { + let active = self.active.get_mut(&id).unwrap(); + active.region = region; + active.placement = placement; + let local = if info.region_node { + UiRegion::FULL + } else { + region + }; + for primitive in &active.primitives { + let handle = &primitive.handle; + *self.layers[handle.layer].region_mut(handle) = + primitive.region.resolve(local, placement); + } + if let Some(mask_region) = active.mask_region { + rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.resolve(local, placement); + } + let parent_move = active.move_idx; + let mask = active.mask; + let children = active.inherited_children.len(); + for index in 0..children { + let child = self.active[&id].inherited_children[index]; + 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_placement = UiRegion { + x: chosen[0].unwrap_or(UiSpan::FULL), + y: chosen[1].unwrap_or(UiSpan::FULL), + }; + let child_info = DrawInfo { + layer: active.layer, + parent: Some(id), + depth: info.depth + 1, + parent_move, + region_node: active.move_idx != active.parent_move, + mask, + given_len: child_local.size(), + offer_len: active.offer_len, + offer_placement: active.offer_placement, + px: child_local.size().to_px(info.px), + offered_px: active.offer_len.to_px(info.offered_px), + placement: chosen, + }; + self.place( + child, + child_local.within(&local), + child_placement, + child_info, + rsc, + ); + } + } + /// A reused subtree keeps its shape, so every widget in it moves by the /// same amount -- and where the top of it did not move, none of it did, /// which is what makes this free in the ordinary case. @@ -733,11 +837,12 @@ impl UiRenderState { self.moves.set(active.move_idx, region); return; } - for handle in &active.primitives { - let region = self.layers[handle.layer].region_mut(handle); - *region = remap.apply(*region); - } active.region = remap.apply(active.region); + for primitive in &active.primitives { + let handle = &primitive.handle; + *self.layers[handle.layer].region_mut(handle) = + primitive.region.resolve(active.region, active.placement); + } let own_mask = (active.mask != active.parent_mask).then_some(active.mask); let children = active.children.len(); // A mask the widget set itself moves with it; one it inherited @@ -745,7 +850,10 @@ impl UiRenderState { if let Some(idx) = own_mask { let mask = rsc.ui_mut().masks.get_mut(idx); debug_assert_eq!(mask.move_idx, parent_move); - mask.region = remap.apply(mask.region); + mask.region = active + .mask_region + .unwrap() + .resolve(active.region, active.placement); } for index in 0..children { let child = self.active[&id].children[index]; @@ -768,8 +876,8 @@ impl UiRenderState { fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option { let mut active = self.active.remove(&id); if let Some(active) = &mut active { - for h in &active.primitives { - let mask = self.layers.free(h); + for primitive in &active.primitives { + let mask = self.layers.free(&primitive.handle); if mask != MaskIdx::NONE { rsc.ui_mut().masks.remove(mask); } @@ -825,17 +933,19 @@ impl UiRenderState { id, region: UiRegion::FULL, placement: UiRegion::FULL, - reads_placement: false, given_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE, - answer: (size, [Holds::ANY; 2]), + offer_placement: [None; 2], + answer: None, size, - holds: [Holds::ANY; 2], + holds: LayoutHolds::ANY, drawn: false, parent: info.parent, depth: info.depth, textures: Vec::new(), primitives: Vec::new(), + mask_region: None, + inherited_children: Vec::new(), children: Vec::new(), size_deps: Vec::new(), move_idx: info.parent_move, @@ -1007,7 +1117,7 @@ impl UiRenderState { let declared_changed = declared_lens(rsc.widgets(), id) != active.declared; let alignment_changed = rsc.widgets().alignment(id) != active.own_align; if let Some(parent) = active.parent - && (declared_changed || alignment_changed || !active.drawn) + && (declared_changed || alignment_changed || !active.drawn || active.answer.is_none()) { if declared_changed { self.replace_answers = true; @@ -1063,6 +1173,7 @@ impl UiRenderState { mask: active.parent_mask, given_len: active.given_len, offer_len: active.offer_len, + offer_placement: active.offer_placement, px: given_px, offered_px, // The same question its parent asked: the axes its parent chose @@ -1075,10 +1186,17 @@ impl UiRenderState { diag::bump(Counter::LocalRedraws); let old = self.remove(id, false, rsc); - // `draw_inner` places the answer inside that box itself, which is the - // ask that leaves the widget where its parent put it. - let answer = self.draw_inner(id, given, info, old, rsc); - if answer != was_answer { + // Refresh the original measurement before restoring the assigned slot. + // Its lengths may differ even though the fraction reference is unchanged. + let offered = DrawInfo { + placement: info.offer_placement, + ..info + }; + let answer = self.draw_inner(id, given, offered, old, rsc); + if info.placement != offered.placement { + self.draw_inner(id, given, info, None, 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. #[cfg(feature = "layout-diagnostics")] diff --git a/src/widget/mask.rs b/src/widget/mask.rs index db58eb2..7524991 100644 --- a/src/widget/mask.rs +++ b/src/widget/mask.rs @@ -6,8 +6,7 @@ pub struct Masked { impl Widget for Masked { fn draw(&mut self, painter: &mut Painter) -> Size { - let own = painter.placement(); - painter.set_mask(own); + painter.set_mask(DrawRegion::Extent(UiRegion::FULL)); painter.widget(&self.inner); // What it occupies is its box, on both axes, for the reason `Scroll` // reports the same: it clips what is inside to that box, so it can diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 4981b64..63dc95f 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -15,10 +15,12 @@ impl Widget for Scroll { // Draw in the whole container only when its scrolling-axis length is // not already known, then draw it at the scrolled offset. let whole = UiRegion::FULL; - let answer_len = match painter.known_len(&self.inner, self.axis, whole) { - Some(len) => len, - None => painter.widget(&self.inner).size().axis(self.axis), - }; + let own = painter.placement(); + let answer_len = + match painter.known_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]) { + Some(len) => len, + None => painter.widget(&self.inner).size().axis(self.axis), + }; let content = answer_len.apply_leftover(); self.container_len = container_len; self.content_len = content.to_px(container_len); diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index d6cb611..345d309 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -37,7 +37,7 @@ impl Widget for Span { // from the cursor, because a text has to wrap at the width // actually there. let room = axis.pair(Some(along(cursor, far)), None); - let len = match painter.known_len(child, axis, region) { + let len = match painter.known_len(child, axis, region, room) { Some(len) => len, None => painter.widget_at(child, region, room).len(axis), }; diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index eea7d1e..e0dd0b4 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -89,8 +89,7 @@ impl TextView { // hair under that line, and the break made in it is not the break a // cold layout makes there. let size = Size::from_px(PxVec2::ceil_from_f32(tex.size)); - let within = region.within(&painter.placement()); - painter.glyphs(tex, within); + painter.glyphs(tex, DrawRegion::Extent(region)); (region, size) } diff --git a/tests/cases/layout.rs b/tests/cases/layout.rs index d7a4d33..e7f3a4c 100644 --- a/tests/cases/layout.rs +++ b/tests/cases/layout.rs @@ -699,3 +699,20 @@ fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() { } } } + +#[test] +fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() { + let mut h = Harness::new((400, 200)); + let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc); + let behind = rect(Color::BLUE).add(&mut h.rsc); + let stack = Stack { + children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)], + size: StackSize::Child(1), + } + .add(&mut h.rsc); + h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0))); + + assert_corners!(h, stack, (0, 0), (200, 200)); + assert_corners!(h, half, (0, 0), (200, 200)); + assert_corners!(h, behind, (0, 0), (200, 200)); +} diff --git a/tests/cases/retained.rs b/tests/cases/retained.rs index 98368b9..932b3ab 100644 --- a/tests/cases/retained.rs +++ b/tests/cases/retained.rs @@ -156,16 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() { h.set_root((hinted, asked).span(Dir::RIGHT)); assert_eq!(told_draws.get(), 1); - // Reading its box makes its drawing hold for the measuring box alone, - // and it reports less than that box: so it is drawn again in the box its - // answer places it in, and once more in the final box the span chooses. - // A widget that says what it holds for, as text does, skips the middle - // one. - assert_eq!( - asked_draws.get(), - 3, - "drawn to be measured, in its placed box, then in its final box" - ); + // Only the available length changes: positioning the final slot does + // not invalidate a numeric size read. + assert_eq!(asked_draws.get(), 2); } #[test] @@ -737,3 +730,81 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() { "the span it moved to is the one the change has to reach" ); } + +fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec { + h.render.active[&id] + .primitives + .iter() + .map(|primitive| { + let handle = &primitive.handle; + let instance = &h.render.layers[handle.layer].primitives()[handle.kind as usize] + .as_ref() + .unwrap() + .instances()[handle.inst_idx]; + h.render + .moves + .resolve(instance.move_idx, instance.region) + .to_px(h.render.output_size()) + }) + .collect() +} + +#[test] +fn frame_geometry_and_extent_geometry_keep_their_references() { + struct Both(Rc>); + impl Widget for Both { + fn draw(&mut self, painter: &mut Painter) -> Size { + self.0.set(self.0.get() + 1); + painter.primitive_within(RectPrimitive::color(Color::RED), UiRegion::FULL); + painter.primitive(RectPrimitive::color(Color::BLUE)); + Size::LEFTOVER + } + } + for node in [false, true] { + let mut h = Harness::new((400, 200)); + let first = rect(Color::GREEN).width(100).add(&mut h.rsc); + let draws = Rc::new(Cell::new(0)); + let both = Both(draws.clone()).add(&mut h.rsc); + h.rsc.widgets_mut().set_region_node(both, node); + h.set_root((first, both).span(Dir::RIGHT)); + let count = draws.get(); + h.set_len(first, Axis::X, 200); + h.frame(); + assert_eq!(draws.get(), count); + let bounds = primitive_bounds(&h, both.id()); + assert_eq!(bounds[0].top_left.x, Px::ZERO); + assert_eq!(bounds[0].bot_right.x, Px::from_int(400)); + assert_eq!(bounds[1].top_left.x, Px::from_int(200)); + assert_eq!(bounds[1].bot_right.x, Px::from_int(400)); + } +} + +#[test] +fn changing_an_inherited_extent_keeps_the_original_measurement_offer() { + fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget, WeakWidget) { + let first = rect(Color::RED).width(width).add(&mut h.rsc); + let words = wtext(text).size(20).wrap(true).add(&mut h.rsc); + let through = Stretchy { + inner: words.add_strong(&mut h.rsc), + draws: Rc::new(Cell::new(0)), + } + .add(&mut h.rsc); + h.set_root((first, through).span(Dir::RIGHT)); + (words, first) + } + let short = "one two"; + let long = "one two three four five six seven eight nine ten eleven twelve"; + let mut warm = Harness::new((400, 200)); + let (words, first) = build(&mut warm, 50, short); + warm.set_len(first, Axis::X, 200); + warm.frame(); + *warm.rsc[words].content = long.to_string(); + warm.frame(); + let mut cold = Harness::new((400, 200)); + let (other, _) = build(&mut cold, 200, long); + assert_eq!(warm.region(&words), cold.region(&other)); + assert_eq!( + primitive_bounds(&warm, words.id()), + primitive_bounds(&cold, other.id()) + ); +} diff --git a/tests/cases/unsettled.rs b/tests/cases/unsettled.rs index d851606..2182559 100644 --- a/tests/cases/unsettled.rs +++ b/tests/cases/unsettled.rs @@ -614,3 +614,61 @@ fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() { assert_eq!(warm.region(&text), cold.region(&cold_text)); } + +#[test] +fn adding_text_to_a_reverse_row_keeps_its_shared_height() { + fn build( + h: &mut Harness, + changed: bool, + ) -> (WeakWidget, WeakWidget, Vec) { + let wrap = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.").size(16).wrap(true).add_strong(&mut h.rsc); + let one = || { + wtext("one line, overflowing whatever it is given") + .size(16) + .wrap(false) + }; + let plain = one().add_strong(&mut h.rsc); + let shared = one() + .width(LayoutLen::LEFTOVER) + .height(LayoutLen::LEFTOVER) + .add(&mut h.rsc); + let mut extra: Vec = vec![ + rect(Color::RED).add_strong(&mut h.rsc), + one().add_strong(&mut h.rsc), + one().add_strong(&mut h.rsc), + ]; + let children: Vec = if changed { + let mut children: Vec = vec![plain, shared.add_strong(&mut h.rsc)]; + children.append(&mut extra); + children + } else { + vec![wrap, plain, shared.add_strong(&mut h.rsc)] + }; + let row = Span { + children, + dir: Dir::LEFT, + gap: Px::ZERO, + } + .height(LayoutLen::rel(1.0)) + .add(&mut h.rsc); + let fill: StrongWidget = rect(Color::BLUE).add_strong(&mut h.rsc); + let children: Vec = vec![fill, row.add_strong(&mut h.rsc)]; + let root = Span { + children, + dir: Dir::RIGHT, + gap: Px::from_int(4), + } + .height(LayoutLen::rel(1.0)) + .add(&mut h.rsc); + h.set_root(root); + (row, shared, extra) + } + let mut warm = Harness::new((900, 1200)); + let (row, shared, extra) = build(&mut warm, false); + warm.rsc[row].children.remove(0); + warm.rsc[row].children.extend(extra); + warm.frame(); + let mut cold = Harness::new((900, 1200)); + let (_, other, _) = build(&mut cold, true); + assert_eq!(warm.region(&shared), cold.region(&other)); +}