diff --git a/core/src/orientation/mod.rs b/core/src/orientation/mod.rs index 894de8b..65040ec 100644 --- a/core/src/orientation/mod.rs +++ b/core/src/orientation/mod.rs @@ -2,7 +2,6 @@ mod align; mod axis; mod len; mod pos; -mod wide; use crate::util::Vec2; @@ -10,4 +9,3 @@ pub use align::*; pub use axis::*; pub use len::*; pub use pos::*; -pub use wide::*; diff --git a/core/src/orientation/wide.rs b/core/src/orientation/wide.rs deleted file mode 100644 index 7d92344..0000000 --- a/core/src/orientation/wide.rs +++ /dev/null @@ -1,221 +0,0 @@ -use crate::{PX_SHIFT, PixelRegion, Px, PxVec2, REL_SHIFT, UiRegion, UiSpan}; - -use super::Len; - -/// How many bits of a box a [`WideLen`] keeps, and how many of a pixel. Both -/// are the grid's own shift plus the room an `i64` has left over: a `Rel` is -/// a fraction of a box and needs eight bits above the point, a `Px` counts up -/// to two million of them and needs twenty-two. -const WIDE_REL: u32 = 48; -const WIDE_PX: u32 = 32; -const REL_GAIN: u32 = WIDE_REL - REL_SHIFT; -const PX_GAIN: u32 = WIDE_PX - PX_SHIFT; - -/// A length part-way through a composition, on a finer grid than the [`Len`] -/// it came from and will go back to. -/// -/// Composing a box through its ancestors is four multiplies a level, and in -/// `Len` every one of them lands back on the grid before the next starts, so -/// the error grows with the depth of the tree. Stepping through this instead -/// rounds once, at the end -- which matters because the same box is reached -/// two ways, composed down the chain and measured against the window, and a -/// structural layout decision turns on the two being one number. -/// -/// Twenty-four extra bits of a box and twenty-two of a pixel are far more -/// than a chain can spend: the residue is `2^-48` of a box a level, against -/// `2^-24` before. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WideLen { - /// Counts `1/2^WIDE_REL` of the box this length is a part of. - rel: i64, - /// Counts `1/2^WIDE_PX` of a pixel. - px: i64, -} - -impl WideLen { - pub const fn of(len: Len) -> Self { - Self { - rel: (len.rel.raw() as i64) << REL_GAIN, - px: (len.px.raw() as i64) << PX_GAIN, - } - } - - /// This length composed through the box it sits in, which is the same - /// expression [`Len::within`] uses with the rounding left out. The parent - /// is a stored box and so is on the ordinary grid; only the part being - /// carried needs the room. - pub const fn within(self, parent: &UiSpan) -> Self { - let rel_span = (parent.end.rel.raw() - parent.start.rel.raw()) as i128; - let px_span = (parent.end.px.raw() - parent.start.px.raw()) as i128; - let rel = ((parent.start.rel.raw() as i64) << REL_GAIN) - + ((rel_span * self.rel as i128) >> REL_SHIFT) as i64; - // A pixel span of the parent, taken at this length's fraction of it: - // the product is `WIDE_REL + PX_SHIFT` bits under the point and the - // answer is `WIDE_PX`, so what comes off is the difference. - let px = self.px - + ((parent.start.px.raw() as i64) << PX_GAIN) - + ((px_span * self.rel as i128) >> (WIDE_REL + PX_SHIFT - WIDE_PX)) as i64; - Self { rel, px } - } - - /// Back onto the grid against a box of `len` pixels, which is the one - /// rounding a whole composition gets. Both parts are put over - /// `WIDE_REL + PX_SHIFT` first so that it is one and not two. - pub const fn to_px(self, len: Px) -> Px { - let px = (self.px as i128) << (WIDE_REL + PX_SHIFT - WIDE_PX); - let rel = self.rel as i128 * len.raw() as i128; - Px::from_raw(((px + rel) >> WIDE_REL) as i32) - } - - /// A *length* composed through the box it sits in, which is half of what - /// the two ends of that box cost: where the parent sits falls out of the - /// difference, so a length carries the parent's extent and its pixel span - /// and nothing else. Two multiplies a level rather than four. - pub const fn len_within(self, parent: &UiSpan) -> Self { - let rel_span = (parent.end.rel.raw() - parent.start.rel.raw()) as i128; - let px_span = (parent.end.px.raw() - parent.start.px.raw()) as i64; - Self { - rel: ((rel_span * self.rel as i128) >> REL_SHIFT) as i64, - // The pixel term takes the fraction on the ordinary grid, which - // keeps it inside an `i64`. Only the fraction itself compounds - // multiplicatively and so needs the room: an error of a `Rel` - // step here is that step times a pixel span, which is a ten - // thousandth of a pixel over a whole window. - px: self.px + ((px_span * (self.rel >> REL_GAIN)) >> (PX_SHIFT + REL_SHIFT - WIDE_PX)), - } - } -} - -/// The two ends of a box, part-way through a composition. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WideSpan { - pub start: WideLen, - pub end: WideLen, -} - -impl WideSpan { - pub const fn of(span: UiSpan) -> Self { - Self { - start: WideLen::of(span.start), - end: WideLen::of(span.end), - } - } - - pub const fn within(self, parent: &UiSpan) -> Self { - Self { - start: self.start.within(parent), - end: self.end.within(parent), - } - } -} - -impl WideSpan { - /// A box given as a part of this one: the same composition carried one - /// level further, with the part on the ordinary grid and the frame it - /// lands in already on the fine one. That is the way round a draw - /// descends -- a widget states its child's box as a part of its own -- - /// so composing this way never puts an intermediate back on the grid. - pub const fn select(self, part: &UiSpan) -> Self { - Self { - start: self.end_at(part.start), - end: self.end_at(part.end), - } - } - - /// How long such a part is, in half the multiplies its two ends cost: - /// where this box sits falls out of the difference. - pub const fn select_len(self, part: &UiSpan) -> WideLen { - let len = part.len(); - let rel_span = (self.end.rel - self.start.rel) as i128; - let px_span = (self.end.px - self.start.px) >> PX_GAIN; - WideLen { - rel: ((rel_span * len.rel.raw() as i128) >> REL_SHIFT) as i64, - px: ((len.px.raw() as i64) << PX_GAIN) - + ((px_span * len.rel.raw() as i64) >> (REL_SHIFT - PX_GAIN)), - } - } - - const fn end_at(self, at: Len) -> WideLen { - let rel_span = (self.end.rel - self.start.rel) as i128; - let px_span = (self.end.px - self.start.px) >> PX_GAIN; - WideLen { - rel: self.start.rel + ((rel_span * at.rel.raw() as i128) >> REL_SHIFT) as i64, - px: self.start.px - + ((at.px.raw() as i64) << PX_GAIN) - + ((px_span * at.rel.raw() as i64) >> (REL_SHIFT - PX_GAIN)), - } - } -} - -/// How long a box is on each axis, part-way through a composition. What -/// reads a box in pixels almost always wants this and not where it sits. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WideSize { - pub x: WideLen, - pub y: WideLen, -} - -impl WideSize { - pub const fn of(region: UiRegion) -> Self { - Self { - x: WideLen::of(region.x.len()), - y: WideLen::of(region.y.len()), - } - } - - pub const fn within(self, parent: &UiRegion) -> Self { - Self { - x: self.x.len_within(&parent.x), - y: self.y.len_within(&parent.y), - } - } - - pub fn to_px(self, window: PxVec2) -> PxVec2 { - PxVec2::new(self.x.to_px(window.x), self.y.to_px(window.y)) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WideRegion { - pub x: WideSpan, - pub y: WideSpan, -} - -impl WideRegion { - /// A box given as a part of this one, on both axes. - pub const fn select(self, part: &UiRegion) -> Self { - Self { - x: self.x.select(&part.x), - y: self.y.select(&part.y), - } - } - - /// How big such a part is, which is what reads a box in pixels. - pub const fn select_size(self, part: &UiRegion) -> WideSize { - WideSize { - x: self.x.select_len(&part.x), - y: self.y.select_len(&part.y), - } - } - - pub const fn of(region: UiRegion) -> Self { - Self { - x: WideSpan::of(region.x), - y: WideSpan::of(region.y), - } - } - - pub const fn within(self, parent: &UiRegion) -> Self { - Self { - x: self.x.within(&parent.x), - y: self.y.within(&parent.y), - } - } - - pub fn to_px(self, window: PxVec2) -> PixelRegion { - PixelRegion { - top_left: PxVec2::new(self.x.start.to_px(window.x), self.y.start.to_px(window.y)), - bot_right: PxVec2::new(self.x.end.to_px(window.x), self.y.end.to_px(window.y)), - } - } -} diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 0aec51f..98974c9 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, WidgetId, + UiRegion, UiVec2, WidgetId, }; /// What is kept of a widget its parent has asked about. `drawn` says whether @@ -11,11 +11,20 @@ pub struct ActiveData { pub id: WidgetId, /// The box its drawing is in, in `parent_move`'s coordinates. pub region: UiRegion, - /// The box its parent first asked about it in, as a part of the box the - /// parent was itself asked in. Any later box it was given was decided - /// knowing its answer, so this is where a question about it is asked - /// again -- and it is kept relative so that it follows the parent's. - pub offer: UiRegion, + /// The box its parent gave it, in the same coordinates: what it was + /// asked about, before its own answer placed its drawing inside it. + /// `region` is that placement, and a local redraw asks here. + pub given: UiRegion, + /// 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. + pub given_len: UiVec2, + /// The lengths of the box its parent first asked about it in, as + /// lengths of the box the parent was itself offered. Any later box it + /// was given was decided knowing its answer, so this is the question + /// 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]), /// What the widget said it used of its box, the last time it drew. diff --git a/core/src/ui/holds.rs b/core/src/ui/holds.rs index cd763db..79270f4 100644 --- a/core/src/ui/holds.rs +++ b/core/src/ui/holds.rs @@ -1,4 +1,4 @@ -use crate::{Len, Px, REL_SHIFT, Rel, fixed::div_toward, fixed::narrow}; +use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow}; use std::ops::RangeInclusive; /// The lengths of a box, in pixels, that one drawing of a widget holds for: @@ -10,9 +10,9 @@ use std::ops::RangeInclusive; /// /// The ends are lengths on the grid rather than floats with a tolerance /// around them: a box offered back at the length a widget reported comes back -/// as the same number, so a range means what it says. What widening there is -/// belongs to [`Self::through`], which has a rounding to undo, and is derived -/// from that rounding rather than chosen. +/// as the same number, so a range means what it says. The one place a range +/// is wider than the length it came from is [`Self::through`], and what it is +/// wider by is the floor that inverting a fraction undoes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Holds { pub lo: Px, @@ -41,63 +41,29 @@ impl Holds { } /// What a box has to be for a part of it, `len` of the box long, to stay - /// in this range. A part with no relative extent is a fixed length: it - /// was drawn at that length and any box keeps it there. + /// in this range: the exact preimage of `px + floor(rel * box)`, which is + /// the one way a box in pixels is reached. A part with no relative extent + /// is a fixed length -- it was drawn at that length and any box keeps it + /// there. /// - /// The way in is `px + rel * box` taken to the nearest step, so a part - /// of exactly `lo` came from anything within half a step of it and the - /// answer is an interval even where this range is one length. Inverting - /// the length alone instead gives a point that need not even contain the - /// box the part was drawn in, which is a range excluding the drawing it - /// was made for. + /// The answer is an interval even where this range is a single length, + /// because the multiply on the way in drops to the step below and many + /// boxes therefore give one length. That is a floor rather than an + /// allowance: inverting it is two divisions and nothing else, and the + /// whole of a box maps back to itself. pub const fn through(self, len: Len) -> Self { let rel = len.rel.raw() as i64; if rel == 0 { return Self::ANY; } - // In half steps, and both at the floor: one less on either and the - // `Holds` assertion in `draw_at` fires, because the range stops - // containing the box a drawing was made in. Wider is the unsound - // side -- it admits reusing a drawing where it does not hold -- and - // neither end buys any reuse, since tightening them moves none of - // the rig's work counters. - // - // `ROUTES` covers a box composed down the chain against the same box - // measured against the window. It was three half steps while - // composing rounded four multiplies a level; `Moves::compose` now - // rounds the whole walk once, which took one off. One more is - // arithmetically available -- the `Holds` assertion is quiet at one - // half step, and the whole-of-a-box case maps back to itself exactly - // -- and it is **not** taken, because a range that tight makes - // shrinker seed 220 lay out differently warm than cold. Too narrow - // is supposed to cost only a redraw; there it re-breaks a wrapping - // text, whose reported width then moves a `Branch` onto its other - // subtree. That is the unsettled-text family rather than a rounding - // question, and closing it is what would let this go lower. - // - // `way_in` covers the multiply this inverts, which drops a step and - // only ever downward, so it belongs at the top of the range alone. - // It is irreducible for the same reason a floor is not invertible: - // many boxes give one length. The whole of a box has no multiply in - // it, however many pixels were added to it, since multiplying by one - // is exact and taking the pixels off again is too -- allowing for it - // there anyway compounded, a step a level down a chain of widgets - // each taking the whole of its parent. - // - // Shifted by half of what a `Rel` counts in, to divide by the - // fraction: exact until the division takes it back to the grid. let px = len.px.raw() as i64; - let half_rel = REL_SHIFT - 1; - const ROUTES: i64 = 2; - let way_in = match rel == Rel::ONE.raw() as i64 { - true => 0, - false => 2, - }; - let lo = ((self.lo.raw() as i64 - px) * 2 - ROUTES) << half_rel; - let hi = ((self.hi.raw() as i64 - px) * 2 + ROUTES + way_in) << half_rel; - // Dividing by a negative turns the ends around, so which end each - // bound comes from is decided before dividing rather than by taking - // the min and max of four divisions. + // `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and + // `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`. + let lo = (self.lo.raw() as i64 - px) << REL_SHIFT; + let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1; + // Dividing by a negative fraction turns the ends around, so which + // bound each comes from is decided before dividing rather than by + // taking the min and max of four divisions. match rel > 0 { true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)), false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)), @@ -148,25 +114,37 @@ mod tests { } /// A widget handed the whole of its parent's box, with or without pixels - /// taken off it, brings no multiply of its own, so it allows for the two - /// routes and nothing else -- one step, where it was one a level of - /// nesting before `Moves::compose`. The identity is what the arithmetic - /// would allow; see `through` for why it is not taken. + /// taken off it, has no fraction to invert: multiplying by one is exact + /// and taking the pixels off again is too, so the box maps back to + /// itself. Allowing for anything here compounded a step a level down a + /// chain of widgets each taking the whole of its parent. #[test] - fn the_whole_of_a_box_widens_by_the_routes_alone() { + fn the_whole_of_a_box_maps_back_to_itself() { let at = Px::from_int(956); - let one_step = |len: Px| Holds { - lo: len - Px::STEP, - hi: len + Px::STEP, - }; - assert_eq!(Holds::at(at).through(Len::FULL), one_step(at)); + assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at)); let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8)); assert_eq!( Holds::at(at).through(less_eight), - one_step(at + Px::from_int(8)) + Holds::at(at + Px::from_int(8)) ); } + /// The range is the exact preimage at both ends, so a box one step + /// outside it really does give a length outside this range. What a wider + /// range costs is a drawing reused where it does not hold. + #[test] + fn a_box_one_step_outside_the_range_is_outside_it() { + let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146)); + let at = Px::from_int(300); + let holds = Holds::at(at).through(part); + for inside in [holds.lo, holds.hi] { + assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}"); + } + for outside in [holds.lo.next_down(), holds.hi.next_up()] { + assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}"); + } + } + /// A truncating multiply only ever drops, so the step it needs allowing /// for on the way in belongs at the top of the range and not the bottom. #[test] diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 20dbaff..3e8e01e 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -1,6 +1,6 @@ use crate::{ Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget, - WideRegion, WideSize, WidgetId, Widgets, + WidgetId, Widgets, util::{Arena, Id, TrackedArena}, }; @@ -68,32 +68,10 @@ impl Moves { } } - /// Composes a region held in `idx`'s coordinates down the chain, on a - /// grid fine enough that the whole walk rounds once. Composing in `Len` - /// rounds four multiplies a level, so the residue grew with the depth of - /// the tree -- and it is the box this produces that layout takes a - /// structural decision on. - pub fn compose(&self, idx: MoveIdx, local: UiRegion) -> WideRegion { - let mut region = WideRegion::of(local); - self.walk(idx, |entry| region = region.within(entry)); - region - } - - /// How long a region held in `idx`'s coordinates is, composed down the - /// chain on the same fine grid as [`Self::compose`] and for the same - /// reason. Half the multiplies of composing the box, since a length does - /// not need to know where the box sits -- and what reads a box in pixels - /// nearly always wants only this. - pub fn size_of(&self, idx: MoveIdx, local: UiRegion) -> WideSize { - let mut size = WideSize::of(local); - self.walk(idx, |entry| size = size.within(entry)); - size - } - /// The same walk the vertex shader does, in the same `Len` the shader is - /// handed. What layout decides on is [`Self::compose`]; this is for - /// asking where the drawing will actually land, which the shader works - /// out again in floats from these same entries. + /// handed, for asking where a drawing will actually land -- hit testing, + /// and nothing layout decides on. Layout threads its lengths down the + /// draw instead, so no box it compares is composed back up this chain. pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion { let mut region = local; self.walk(idx, |entry| region = region.within(entry)); diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index a3ca7a9..02400c9 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, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget, + Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, - WideRegion, WidgetId, Widgets, + WidgetId, Widgets, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -19,9 +19,11 @@ pub struct Painter<'a> { /// This widget's box, in the coordinates of `move_idx`. pub(super) region: UiRegion, - /// What this widget's slot composes to, so its own box and its children's - /// are a step further rather than a walk back up the chain. - pub(super) slot_wide: WideRegion, + /// That box in pixels, which its children's are a length of: threaded + /// down from the box this widget was given rather than composed back up + /// the chain, so every length in layout is one multiply from its + /// parent's and [`Holds::through`] inverts exactly that. + pub(super) px: PxVec2, pub(super) mask: MaskIdx, pub(super) textures: Vec, pub(super) primitives: Vec, @@ -29,10 +31,12 @@ pub struct Painter<'a> { /// The children asked about so far, so the first box each was asked in /// is the one recorded as its offer. pub(super) offered: Vec, - /// The box this widget was first asked about in, in pixels. + /// The lengths of the box this widget was first asked about in, in + /// pixels. Its children's offers are a fraction of it. pub(super) offered_px: PxVec2, - /// Whether this draw is in that box, which makes the questions it asks - /// the ones a cold layout asks and their answers the ones to keep. + /// Whether this draw is in a box of those lengths, which makes the + /// questions it asks the ones a cold layout asks and their answers the + /// ones to keep. pub(super) at_offer: bool, /// The children whose size this widget read while drawing. pub(super) size_deps: Vec, @@ -183,11 +187,20 @@ impl<'a> Painter<'a> { self.children.push(id.id()); } let first_ask = self.offer(id.id()); - let offer = match first_ask { - true => local, - false => self.state.active.get(&id.id()).map_or(local, |a| a.offer), + let given_len = local.size(); + let offer_len = match first_ask { + true => given_len, + false => self + .state + .active + .get(&id.id()) + .map_or(given_len, |a| a.offer_len), }; - let answers_offer = self.at_offer && local == offer; + 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. @@ -201,9 +214,10 @@ impl<'a> Painter<'a> { parent_move: self.move_idx, region_node, mask: self.mask, - offer, - offered_px: self.px_within_offer(offer), - slot_wide: self.slot_wide, + given_len, + offer_len, + px, + offered_px, decided, }, None, @@ -264,15 +278,14 @@ impl<'a> Painter<'a> { let declared = self.declared_lens(child); let align = self.rsc.widgets().alignment(child.id()); let local = declared_box(region, declared, align); - let within = local.within(&self.region); let first_ask = self.offer(child.id()); if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) { - active.offer = local; + active.offer_len = local.size(); } if let Some(hint) = self.size_hint(child, axis) { return Some(hint); } - let px = self.px_of(within); + let px = local.size().to_px(self.px); let (size, holds) = self.state .retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?; @@ -300,22 +313,6 @@ impl<'a> Painter<'a> { true } - /// The pixel size of a part of the box this widget was asked in. - fn px_within_offer(&self, local: UiRegion) -> PxVec2 { - let size = local.size(); - PxVec2::new( - size.x.to_px(self.offered_px.x), - size.y.to_px(self.offered_px.y), - ) - } - - /// A box stated as a part of this widget's slot, in pixels. - fn px_of(&self, region: UiRegion) -> PxVec2 { - self.slot_wide - .select_size(®ion) - .to_px(self.state.output_size()) - } - fn depend_on(&mut self, child: &StrongWidget) { if !self.size_deps.contains(&child.id()) { self.size_deps.push(child.id()); @@ -397,31 +394,25 @@ impl<'a> Painter<'a> { /// near edge. A container that reports one child's size gives every child /// this, so what it draws is inside what it says it occupies. pub fn box_of(&self, size: Size) -> UiRegion { - placed_box( - UiRegion::FULL, - size, - RegionAlign::NEAR, - [None; 2], - [false; 2], - ) + let lens = placed_lens(size, [None; 2], [false; 2]); + placed_box(UiRegion::FULL, lens, RegionAlign::NEAR) } /// This widget's box in pixels. Reading it makes the drawing one that /// holds for this box only, until `holds` says how far it goes. pub fn px_size(&mut self) -> PxVec2 { - let px = self.px_of(self.region); - for (own, len) in self.own.iter_mut().zip([px.x, px.y]) { + for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) { if *own == Holds::ANY { *own = Holds::at(len); } } - px + self.px } /// One axis of this widget's 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 len = self.px_of(self.region).axis(axis); + let len = self.px.axis(axis); let own = &mut self.own[axis as usize]; if *own == Holds::ANY { *own = Holds::at(len); @@ -436,7 +427,7 @@ impl<'a> Painter<'a> { pub fn holds(&mut self, axis: Axis, holds: impl Into) { let holds = holds.into(); debug_assert!( - holds.contains(self.px_of(self.region).axis(axis)), + holds.contains(self.px.axis(axis)), "'{}' ({:?}) says its drawing holds for lengths that leave out its own box", self.label(), self.id @@ -575,29 +566,42 @@ pub(crate) fn fills(reported: LayoutLen, declared: Option, decided: b reported.leftover != Weight::ZERO || declared.is_some() || decided } -/// The box a drawing occupies: the size the widget reported, on the side of -/// the box it was asked in that its alignment says, on every axis that is -/// not simply filled. +/// What of the box it was given a widget's drawing occupies, as lengths of +/// that box: the size it reported wherever that is a part to be placed, and +/// the whole of the box wherever the answer fills it. /// /// A reported fraction is a fraction of the box the widget drew in, where a /// declared one is a fraction of the box its parent handed down -- a span /// reporting `rel(1.0)` means all of what it was given, whatever that was a -/// fraction of. So this scales by the box rather than composing into it. -pub(crate) fn placed_box( - region: UiRegion, +/// fraction of. So this is a length of the box rather than a length composed +/// into it, and a box in pixels is this step from the given box's pixels. +pub(crate) fn placed_lens( size: Size, - align: RegionAlign, declared: [Option; 2], decided: [bool; 2], -) -> UiRegion { - let mut placed = region; +) -> UiVec2 { + let mut lens = UiVec2::FULL_SIZE; for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) { let reported = size.axis(axis); - if fills(reported, declared, decided) { + if !fills(reported, declared, decided) { + *lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px); + } + } + lens +} + +/// Where that drawing sits: those lengths taken of the box the widget was +/// asked in, on the side of it that the widget's alignment says. +pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion { + let mut placed = region; + for axis in AXES { + // The whole of the box is already where it sits, and the arithmetic + // below is the identity for it. + if lens.axis(axis) == Len::FULL { continue; } let span = placed.axis_mut(axis); - let len = span.len().scale(reported.rel) + Len::from_parts(Rel::ZERO, reported.px); + let len = lens.axis(axis).within_len(span.len()); span.start += (span.len() - len).scale(align.axis(axis).rel()); span.end = span.start + len; } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index a7a63f2..7ebe602 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,9 +1,9 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; -use crate::ui::painter::{declared_box, declared_lens, fills, placed_box}; +use crate::ui::painter::{declared_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, Weight, WideRegion, + PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, util::{HashMap, Vec2}, }; @@ -20,13 +20,16 @@ pub(super) struct DrawInfo { pub parent_move: MoveIdx, pub region_node: bool, pub mask: MaskIdx, - /// The box it was first asked about in, as a part of its parent's, and - /// that box in pixels. - pub offer: UiRegion, + /// The box its parent gave it, as lengths of the parent's own box, and + /// the lengths of the box it was first asked about in the same form. + /// Both describe the box the *parent* stated, so the second, placing ask + /// carries them unchanged while its own region is the placement inside. + pub given_len: UiVec2, + pub offer_len: UiVec2, + /// 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, pub offered_px: PxVec2, - /// The box `parent_move` composes to, on the fine grid, so a widget's own - /// box is one step further and not a walk back up the chain. - pub slot_wide: WideRegion, /// The axes along which the parent chose this box from the widget's own /// answer, so the answer is not placed inside it again. See /// [`Painter::widget_decided`]. @@ -85,7 +88,13 @@ impl UiRenderState { self.resized = true; } - fn root_info(&self) -> DrawInfo { + /// The root is asked about in the output: the window is where a fraction + /// becomes pixels rather than a box of its own, so the root's box is the + /// first length threaded down. Its own rules narrow that box, and where + /// they do the narrowed box is also the offer -- nothing above it chose + /// anything else. + fn root_info(&self, region: UiRegion) -> DrawInfo { + let px = region.size().to_px(self.output_size); DrawInfo { layer: 0, parent: None, @@ -93,9 +102,10 @@ impl UiRenderState { parent_move: MoveIdx::NONE, region_node: false, mask: MaskIdx::NONE, - offer: UiRegion::FULL, - offered_px: self.output_size, - slot_wide: WideRegion::of(UiRegion::FULL), + given_len: region.size(), + offer_len: UiVec2::FULL_SIZE, + px, + offered_px: px, decided: [false; 2], } } @@ -136,8 +146,8 @@ impl UiRenderState { // length, found the way every other box change is found. Before // anything dirty settles, so that whatever a new output draws // again is drawn once, in the box it will have. - let info = self.root_info(); let region = Self::root_region(root.id(), rsc.widgets()); + let info = self.root_info(region); let answer = self.draw_inner(root.id(), region, info, None, rsc); self.active.get_mut(&root.id()).unwrap().answer = answer; } @@ -154,8 +164,8 @@ impl UiRenderState { let _layout = diag::timer(TimerKind::FullLayout); self.clear(rsc); if let Some(id) = root { - let info = self.root_info(); let region = Self::root_region(id.id(), rsc.widgets()); + let info = self.root_info(region); self.draw_inner(id.id(), region, info, None, rsc); } } @@ -179,13 +189,7 @@ impl UiRenderState { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); - diag::draw_request( - id, - info.parent, - region, - self.px_of(info.parent_move, region), - info.region_node, - ); + diag::draw_request(id, info.parent, region, info.px, info.region_node); } let align = rsc.widgets().alignment(id); let replace_answer = self.answer_invalid.remove(&id) @@ -195,7 +199,7 @@ impl UiRenderState { let retained = match replace_answer { true => None, false => self - .retained_answer(id, region, info, rsc.widgets()) + .retained_answer(id, info, rsc.widgets()) .or_else(|| self.try_reuse(id, region, info, rsc)), }; let answer = retained.unwrap_or_else(|| { @@ -208,42 +212,33 @@ impl UiRenderState { let declared = declared_lens(rsc.widgets(), id); // The second, final ask is in a box chosen from the answer on both // axes, which is also what makes it terminate. - let placed = placed_box(region, answer.0, align, declared, info.decided); + let lens = placed_lens(answer.0, declared, info.decided); + let placed = placed_box(region, lens, align); let placed_info = DrawInfo { + px: lens.to_px(info.px), decided: [true; 2], ..info }; - // The symbolic box can be unchanged while its parent slot changed - // pixel size. Reuse checks the resolved box even in that case. - if self.try_reuse(id, placed, placed_info, rsc).is_none() { - #[cfg(feature = "layout-diagnostics")] - diag::bump(Counter::PlaceRedraws); - let old = self.remove(id, false, rsc); - self.draw_at(id, placed, placed_info, old, rsc); - } + self.place(id, placed, placed_info, rsc); // The answer is only reusable while both parts of the operation are: - // what the widget reported in the offered box, and what it drew in - // the box its report selected. Express the latter's contract back in - // terms of the offered box before handing it to the parent. + // what the widget reported in the box it was asked in, and what it + // drew in the box its report selected. Express the latter's contract + // back in terms of the box asked in before handing it to the parent. let drawing_holds = self.active[&id].holds; let mut settled = answer; for axis in AXES { - let reported = answer.0.axis(axis); - let placed_len = match fills( - reported, - declared[axis as usize], - info.decided[axis as usize], - ) { - true => Len::FULL, - false => Len::from_parts(reported.rel, reported.px), - }; settled.1[axis as usize] = - settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len)); + settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis))); } let active = self.active.get_mut(&id).unwrap(); - active.offer = info.offer; + // Whoever asked owns how the box was reached: the box it stated, and + // what of that box the answer then took. A local redraw asks the + // same question again from these. + active.given = region; + active.given_len = info.given_len; + active.offer_len = info.offer_len; active.answer = settled; active.decided = info.decided; active.own_align = align; @@ -251,6 +246,19 @@ impl UiRenderState { settled } + /// Draws a widget in the final box its answer chose, reusing the drawing + /// already there where its retained contract holds for that box. The + /// symbolic box can be unchanged while the box it sits in changed pixel + /// length, so what reuse checks is the box in pixels. + fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { + if self.try_reuse(id, placed, info, rsc).is_none() { + #[cfg(feature = "layout-diagnostics")] + diag::bump(Counter::PlaceRedraws); + let old = self.remove(id, false, rsc); + self.draw_at(id, placed, info, old, rsc); + } + } + /// Calls a widget's `draw` and keeps what it drew in `region`. fn draw_at( &mut self, @@ -260,37 +268,34 @@ impl UiRenderState { old: Option, rsc: &mut dyn UiRsc, ) -> (Size, [Holds; 2]) { - let (move_idx, local, retired_move, slot_wide) = match info.region_node { + 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 -- - // and that box is what its contents compose through. + // region's coordinates and its box is one entry to rewrite. true => ( self.move_slot(id, info.parent_move, region), UiRegion::FULL, None, - info.slot_wide.select(®ion), ), // Keep the old entry alive until every descendant has migrated. // Reusing its index sooner could make an old parent look current. - false => ( - info.parent_move, - region, - self.slots.remove(&id), - info.slot_wide, - ), + false => (info.parent_move, region, self.slots.remove(&id)), }; let (old_children, old_answer) = match old { Some(old) => (old.children, Some(old.answer)), None => (Vec::new(), None), }; rsc.widgets_mut().needs_redraw.remove(&id); - let px = slot_wide.select_size(&local).to_px(self.output_size); - let at_offer = same_px(px, info.offered_px); + // 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. + let px = info.px; + let at_offer = px == info.offered_px; let mut painter = Painter { state: self, region: local, - slot_wide, + px, mask: info.mask, layer: info.layer, own_layer: info.layer, @@ -324,7 +329,7 @@ impl UiRenderState { state: _, rsc: _, region: _, - slot_wide: _, + px: _, mask, textures, primitives, @@ -394,9 +399,10 @@ impl UiRenderState { parent_move: move_idx, region_node: false, mask, - offer: UiRegion::FULL, + given_len: UiVec2::FULL_SIZE, + offer_len: UiVec2::FULL_SIZE, + px, offered_px: px, - slot_wide, decided: [false; 2], }, rsc, @@ -408,7 +414,12 @@ impl UiRenderState { let active = ActiveData { id, region, - offer: info.offer, + // The box a placing ask draws in is a part of the one its parent + // gave, which `draw_inner` writes back over these once the + // placement is done. + given: region, + given_len: info.given_len, + offer_len: info.offer_len, // Whoever asked writes the answer, if this was the asking. answer: old_answer.unwrap_or((size, holds)), size, @@ -453,18 +464,6 @@ impl UiRenderState { } } - /// The pixel size of a region held in `slot`'s coordinates. - pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 { - self.moves.size_of(slot, region).to_px(self.output_size) - } - - /// Where a region held in `slot`'s coordinates lands on screen, to - /// compare one box against another. Both ends of it, where - /// [`Self::px_of`] wants only the length between them. - fn px_region(&self, slot: MoveIdx, region: UiRegion) -> PixelRegion { - self.moves.compose(slot, region).to_px(self.output_size) - } - /// A clean widget's retained answer, if that answer holds for a box of /// `px`. This does not move its drawing, which may already be in the box /// that answer placed it in. @@ -493,7 +492,6 @@ impl UiRenderState { fn retained_answer( &self, id: WidgetId, - region: UiRegion, info: DrawInfo, widgets: &Widgets, ) -> Option<(Size, [Holds; 2])> { @@ -508,9 +506,8 @@ impl UiRenderState { { return None; } - let px = info.slot_wide.select_size(®ion).to_px(self.output_size); let (size, holds) = active.answer; - (holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds)) + (holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds)) } /// Whether anything whose size this widget's own size was read from is @@ -525,31 +522,39 @@ impl UiRenderState { }) } - /// The first box a widget was asked about, re-expressed in the coordinate - /// space its drawing uses. Keeping the relative box and composing it - /// again avoids rebuilding a shifted box from rounded pixel lengths. - fn offered_region(&self, id: WidgetId) -> UiRegion { + /// The pixel lengths of the box a widget was given and of the box it was + /// first asked about, which is what a local redraw needs to ask the + /// question its parent asked. + /// + /// Both are threaded down from the window a length of a box at a time, + /// and this takes the same steps back up: a widget's box is a length of + /// the box its parent drew in, and its offer a length of the box its + /// parent was itself offered. Neither chain has a coordinate frame in it, + /// so neither breaks at a region node -- and both land on the numbers a + /// cold layout computes, rather than near them. + fn asked_px(&self, id: WidgetId) -> (PxVec2, PxVec2) { let active = &self.active[&id]; - let parent_region = match active.parent.and_then(|id| self.active.get(&id)) { - Some(parent) if parent.move_idx == active.parent_move => { - if parent.move_idx == parent.parent_move { - self.offered_region(parent.id) - } else { - UiRegion::FULL - } + // Nothing above the root: the window is where a fraction becomes + // pixels, which is also the whole of the box the root is given. + let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) { + Some(parent) => { + let (given, offer) = self.asked_px(parent.id); + let lens = placed_lens(parent.answer.0, parent.declared, parent.decided); + (lens.to_px(given), offer) } - _ => UiRegion::FULL, - }; - let mut offered = match active.offer == UiRegion::FULL { - true => parent_region, - false => active.offer.within(&parent_region), + None => (self.output_size, self.output_size), }; + let px = active.given_len.to_px(parent_px); + let mut offered = active.offer_len.to_px(parent_offer); for axis in AXES { + // A declared length is resolved by whoever drew the widget, in + // the box that widget drew in, so the box it has is the box it + // was asked about however the offer above it moved. if active.declared[axis as usize].is_some() { - *offered.axis_mut(axis) = *active.region.axis(axis); + *offered.axis_mut(axis) = px.axis(axis); } } - offered + (px, offered) } /// Reuses the actual drawing in a new box if its retained contract holds @@ -606,10 +611,10 @@ impl UiRenderState { } return None; } - // In pixels, because `region` is a fraction of a slot's box 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.slot_wide.select_size(®ion).to_px(self.output_size)) { + // 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) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseOutside); @@ -634,7 +639,9 @@ impl UiRenderState { } let active = self.active.get_mut(&id).unwrap(); active.region = region; - active.offer = info.offer; + active.given = region; + active.given_len = info.given_len; + active.offer_len = info.offer_len; active.depth = info.depth; #[cfg(feature = "layout-diagnostics")] { @@ -671,6 +678,7 @@ impl UiRenderState { rsc: &mut dyn UiRsc, ) { let active = self.active.get_mut(&id).unwrap(); + active.given = remap.apply(active.given); if active.move_idx != parent_move { let region = remap.apply(active.region); active.region = region; @@ -766,7 +774,9 @@ impl UiRenderState { ActiveData { id, region: UiRegion::FULL, - offer: UiRegion::FULL, + given: UiRegion::FULL, + given_len: UiVec2::FULL_SIZE, + offer_len: UiVec2::FULL_SIZE, answer: (size, [Holds::ANY; 2]), size, holds: [Holds::ANY; 2], @@ -898,12 +908,19 @@ impl UiRenderState { } /// Where a widget is on screen: its box composed through the boxes it - /// sits within. `None` for one that is not drawn. + /// sits within, the same walk the vertex shader does. `None` for one that + /// is not drawn. + /// + /// This is for asking where a drawing landed: hit testing, and a test + /// reading a box back. Layout decides on the lengths threaded down the + /// draw instead, and a position is not one of its inputs. pub fn window_region(&self, id: &impl IdLike) -> Option { let active = self.active.get(&id.id())?; - active - .drawn - .then(|| self.px_region(active.parent_move, active.region)) + active.drawn.then(|| { + self.moves + .resolve(active.parent_move, active.region) + .to_px(self.output_size) + }) } /// Settles a dirty widget: asks it again where its parent asked, and @@ -940,28 +957,28 @@ impl UiRenderState { if !active.drawn { return; } - let region = active.region; - let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id); - let asked_in = match active.parent { - Some(_) => self.offered_region(id), - None => Self::root_region(id, rsc.widgets()), + // Nothing above the root resolved its rules or its alignment, so its + // box is its own to work out again against the output. Every other + // widget was given one. + let Some(parent) = active.parent else { + let region = Self::root_region(id, rsc.widgets()); + let info = DrawInfo { + mask: active.mask, + ..self.root_info(region) + }; + #[cfg(feature = "layout-diagnostics")] + diag::bump(Counter::LocalRedraws); + let old = self.remove(id, false, rsc); + self.draw_inner(id, region, info, old, rsc); + return; }; - let offered_px = self.px_of(active.parent_move, asked_in); - // Whole boxes rather than lengths: an offer as long as the final box - // but somewhere else is a different box, and a region node drawing at - // its offer writes the box it drew in into its own entry. - let at_offer = same_pixel_region( - self.px_region(active.parent_move, region), - self.px_region(active.parent_move, asked_in), - ); - let decided = active.decided.contains(&true); - let parent_must_place = active.parent.is_some() && (!region_node || decided) && !at_offer; - // An independently positioned region node can redraw at its offer - // and move its slot to its own placement. Every other widget needs - // its parent to reproduce a different final position. - if let Some(parent) = active.parent - && parent_must_place - { + let (given_px, offered_px) = self.asked_px(id); + // Asked again in the box its parent gave it, which is the question + // its parent asked only while that box is as long as the offer. Any + // other box is a different question, so the parent asks it, with the + // mark left on. Lengths and not whole boxes: what a drawing depends + // on is its lengths, so the same lengths elsewhere is one question. + if given_px != offered_px { rsc.widgets_mut().needs_redraw.insert(id); self.redraw(parent, rsc); rsc.widgets_mut().needs_redraw.remove(&id); @@ -972,50 +989,31 @@ impl UiRenderState { parent: active.parent, depth: active.depth, parent_move: active.parent_move, - region_node, + region_node: rsc.widgets().is_region_node(id), mask: active.mask, - offer: active.offer, + given_len: active.given_len, + offer_len: active.offer_len, + px: given_px, offered_px, - slot_wide: self.moves.compose(active.parent_move, UiRegion::FULL), decided: active.decided, }; - let (was_answer, was) = (active.answer, (active.size, active.holds)); + let (given, was_answer) = (active.given, active.answer); #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); let old = self.remove(id, false, rsc); - let answer = self.draw_inner(id, asked_in, info, old, rsc); - self.active.get_mut(&id).unwrap().answer = answer; - let Some(parent) = info.parent else { - return; - }; + // `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 { - // Left where it was asked: the parent lays out again and chooses - // its final box. + // 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")] { diag::bump(Counter::SizeChanges); diag::bump(Counter::ReaderEdges); } rsc.widgets_mut().needs_redraw.insert(parent); - return; - } - if at_offer { - return; - } - // Then in the final box its parent chose from that answer. That box - // is already placed, so the near edge goes with it: applying the - // widget's own alignment to it again would place its content twice, - // the way it did for a region node under a `Stack` once the stack - // stopped overriding every child's alignment. - let placed_info = DrawInfo { - decided: [true; 2], - ..info - }; - self.draw_inner(id, region, placed_info, None, rsc); - let active = &self.active[&id]; - if (active.size, active.holds) != was { - rsc.widgets_mut().needs_redraw.insert(parent); } } } @@ -1029,16 +1027,6 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool { len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len } -/// The same box is the same number of steps, both of these being lengths on -/// the grid rather than floats to be compared for nearness. -fn same_px(a: PxVec2, b: PxVec2) -> bool { - a == b -} - -fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool { - same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right) -} - /// A retained region rewritten from one parent box into another. A fixed /// source extent can be translated but cannot recover fractions for a resize. #[derive(Clone, Copy)] diff --git a/tests/cases/unsettled.rs b/tests/cases/unsettled.rs index 4736d2e..ba4ab77 100644 --- a/tests/cases/unsettled.rs +++ b/tests/cases/unsettled.rs @@ -3,13 +3,15 @@ //! frame that had not settled: a wrapping text shaped at a width it was //! measured in rather than the one it was given. The rest are a widget //! measured again in a box its own answer had decided, where the old answer -//! is a fixed point whatever the content now says. The last two are neither: -//! one box length, composed two ways, landing either side of the boundary -//! that decided whether a child was drawn at all, and one box as long as the -//! box a widget was offered but somewhere else. +//! is a fixed point whatever the content now says. The last three are +//! neither: one box length, composed two ways, landing either side of the +//! boundary that decided whether a child was drawn at all, and two boxes +//! reached through a region node's own entry rather than through the offer +//! that node was given. use iris::harness::Harness; use iris::prelude::*; +use iris::random::Branch; /// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// the tree changes -- every widget is marked for redraw and the frame is @@ -428,12 +430,12 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec { vec![text.id(), inner.id(), filler.id(), span.id(), root.id()] } -/// A local redraw asks a dirty widget in the box its parent asked it in, and -/// then again in the box its parent chose from that answer. Skipping the -/// second ask because the two boxes are the same *length* left this inner -/// scroll, which owns a region node, drawn at its offer. The offer is the -/// outer scroll's whole viewport and the final box is 24px above it -- the -/// height of the sized child the outer scroll snaps to the end of -- so the +/// A local redraw asks a dirty widget in the box its parent gave it, and only +/// where that box is as long as the one it was offered; anything else is a +/// question its parent has to ask. This inner scroll's offer is the outer +/// scroll's whole viewport and the box it was given is 24px shorter -- the +/// height of the sized child the outer scroll snaps to the end of -- so what +/// it must not do is settle itself. It was drawn at its offer once, and the /// inner scroll and its text stayed 24px too low. #[test] fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { @@ -454,3 +456,101 @@ fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } + +/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220 +/// to. The pad owns a movable region and is the scroll's content, so the box +/// the scroll places it in is as long as that content while the box it was +/// offered is the viewport -- and with no padding to tell those two apart, +/// the span inside it looked like it was still at its offer. So everything +/// under the pad was asked again in the *placed* box, the offer resolving +/// against the node's own entry, which holds that box: the texts kept the +/// widths they had, the content stayed the length those widths make, and the +/// old answer confirmed itself. What the branch adds is a tree that differs +/// rather than a box that moved, since a probe measured at the wrong width +/// takes the other side. +fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec, [WeakWidget; 2]) { + let probe = rect(Color::RED).add(&mut h.rsc); + let wide = rect(Color::GREEN).add(&mut h.rsc); + let narrow = rect(Color::BLUE).add(&mut h.rsc); + let branch = Branch { + probe: probe.add_strong(&mut h.rsc), + wide: wide.add_strong(&mut h.rsc), + narrow: narrow.add_strong(&mut h.rsc), + threshold: 213.0, + } + .add(&mut h.rsc); + let wrapped = 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(&mut h.rsc); + let plain = wtext("one line, overflowing whatever it is given") + .size(16) + .wrap(false) + .add(&mut h.rsc); + let row = |h: &mut Harness, mut children: Vec| { + if swapped { + children.rotate_left(1); + } + Span { + children, + dir: Dir::RIGHT, + gap: Px::ZERO, + } + .add(&mut h.rsc) + }; + let texts: Vec = + vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)]; + let inner = row(h, texts); + let pair: Vec = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)]; + let outer = row(h, pair); + let pad = Pad { + padding: Padding::ZERO, + inner: outer.add_strong(&mut h.rsc), + } + .add(&mut h.rsc); + h.rsc.widgets_mut().set_region_node(pad.id(), true); + let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); + h.set_root(root); + ( + vec![ + probe.id(), + wide.id(), + narrow.id(), + branch.id(), + wrapped.id(), + plain.id(), + inner.id(), + outer.id(), + pad.id(), + root.id(), + ], + [outer, inner], + ) +} + +#[test] +fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() { + let mut warm = Harness::new((900, 1200)); + let (ids, spans) = plant_under_a_node(&mut warm, false); + warm.frame(); + for span in spans { + warm.rsc[span].children.rotate_left(1); + } + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let (cold_ids, _) = plant_under_a_node(&mut cold, true); + cold.frame(); + + let mut wrong = Vec::new(); + for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { + let (got, want) = (warm.region(&w), cold.region(&c)); + if got != want { + wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); + } + } + assert!(wrong.is_empty(), "{}", wrong.join("\n")); +} diff --git a/tests/scenario/mod.rs b/tests/scenario/mod.rs index d144b6f..746308b 100644 --- a/tests/scenario/mod.rs +++ b/tests/scenario/mod.rs @@ -42,11 +42,19 @@ const OUTER: (f32, f32) = (1920.0, 1200.0); const INNER: (f32, f32) = (640.0, 900.0); const STILL: (f32, f32) = (900.0, 1200.0); -/// The same box, to a step of the grid per level of nesting between the two -/// ways of reaching it. A move, a repaint and a row of shares land on the -/// same number; what is left is a box centred in a fraction of its parent -/// against the same box centred in its own pixels. A step is a thousandth of -/// a pixel, where this was a twentieth of one before any of it was on a grid. +/// The same box, to two steps of the grid between the two ways of reaching +/// it. A move, a repaint, a row of shares and every length in pixels land on +/// the same number. What needs the slack is a position: a box centred in a +/// fraction of its parent against the same box centred in its own pixels, +/// and a box re-expressed as a fraction of a parent that changed length. +/// A step is a thousandth of a pixel, where this was a twentieth of one +/// before any of it was on a grid. +/// +/// **One step is not enough**, tried 2026-09-17 once a length in pixels +/// stopped being composed: it passes the 100-seed oracle and fails the +/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So +/// what is left here is the resize path's own rounding rather than a length +/// reached two ways. const AGREE_STEPS: i32 = 2; /// A way of changing what a span holds. Each is a shape worth its own case: