A box in pixels was composed back up the move chain, on a grid fine enough that the walk rounded once, while a widget's offer was threaded down through its ancestors' offers. Two routes to one length, which is what `Holds::through` allowed for -- and the offer's route broke at a region node. `offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved that against the node's slot entry, which holds the box its parent *placed* the node in. Under a `Scroll` that is as long as the content rather than the viewport, so everything below was re-asked at a width its own answer had produced and the old answer confirmed itself: shrinker seed 220 on `reorder` left a widget 290px out. `ActiveData` now keeps a widget's box as lengths of its parent's box -- `given_len`, and `offer_len` for the box it was first asked about -- and `DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a time: the box its parent gave it, then the part of that box its own answer placed the drawing in, which `placed_lens` states once for both `placed_box` and the walk. `Painter::px_size` and `px_len` read that value, and `UiRenderState::asked_px` takes the same steps back up the parent chain where a local redraw starts part-way down the tree. Neither chain has a coordinate frame in it, so neither can break at a region node, and warm and cold reach every length by the same expression. Three things follow. `Holds::through` is the exact preimage of `px + floor(rel * box)` -- two divisions, no allowance, the whole of a box mapping back to itself. A local redraw asks in the box its parent gave it and only where that box is as long as the offer, which retires `redraw`'s third ask and the region-node exception beside it; `draw_inner` places the answer inside that box itself. And symbolic regions are left to the GPU, hit testing and remaps, where `Moves::resolve` is the only walk: `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region` and `slot_wide` are gone, 252 lines of `core/` net. `px` is deliberately not stored beside those lengths. A resize every widget's `Holds` admits redraws nothing, so a stored pixel length would be stale on every widget in the tree with nothing on it to say so, and refreshing it costs a walk down every reused subtree on the resize path. Instructions:u, medians of 21 runs, seed 1 at depth 8: | phase | before | after | | | --- | ---: | ---: | ---: | | `cold`, 200 frames | 313.1M | 312.9M | -0.04% | | `resize` | 408.1M | 405.6M | -0.61% | | `many` | 1,924M | 1,756M | -8.75% | | `scroll` | 357.3M | 323.4M | -9.49% | | `repaint` | 363.3M | 315.4M | -13.18% | `cold` and `resize` have all twenty-five work counters identical, so those two rows say the draw path costs the same threaded as composed. The other three do less work: `repaint` goes from 23 draw requests and 13 widget draws a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and 186 to 207 and 157. Primitive writes are unmoved in every phase. Verified: `view`, `minimal`, `random`, `tabs` and `text` render byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch replay before and after the gesture, and a live resize of `random` to 1280x800 is identical both to the old head's and to a cold render at that size. The oracle passes 100 seeds in release and 120 in debug -- the debug run is the one that exercises the `Holds` assertion -- and the fifteen shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`, which needs both halves of this to fail: the old chain with the old allowance passes it, and the old chain with the exact preimage does not. `AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the 400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the resize path re-expressing a part as a fraction of a box that changed length, not a length reached two ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
6.6 KiB
Rust
166 lines
6.6 KiB
Rust
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:
|
|
/// give the widget any box in this range and it draws the same thing and
|
|
/// reports the same size. A widget that never reads its box in pixels holds
|
|
/// for every length; one that does holds for the one it read unless it says
|
|
/// otherwise, and a parent holds for whatever keeps every child it asked
|
|
/// about or drew inside its own range.
|
|
///
|
|
/// 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. 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,
|
|
pub hi: Px,
|
|
}
|
|
|
|
impl Holds {
|
|
pub const ANY: Self = Self {
|
|
lo: Px::MIN,
|
|
hi: Px::MAX,
|
|
};
|
|
|
|
pub const fn at(len: Px) -> Self {
|
|
Self { lo: len, hi: len }
|
|
}
|
|
|
|
pub const fn contains(&self, len: Px) -> bool {
|
|
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
|
|
}
|
|
|
|
pub const fn and(self, other: Self) -> Self {
|
|
Self {
|
|
lo: self.lo.max(other.lo),
|
|
hi: self.hi.min(other.hi),
|
|
}
|
|
}
|
|
|
|
/// What a box has to be for a part of it, `len` of the box long, to stay
|
|
/// 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 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;
|
|
}
|
|
let px = len.px.raw() as i64;
|
|
// `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)),
|
|
}
|
|
}
|
|
|
|
const fn raws(lo: i64, hi: i64) -> Self {
|
|
Self {
|
|
lo: Px::from_raw(narrow(lo)),
|
|
hi: Px::from_raw(narrow(hi)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RangeInclusive<Px>> for Holds {
|
|
fn from(range: RangeInclusive<Px>) -> Self {
|
|
Self {
|
|
lo: *range.start(),
|
|
hi: *range.end(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::Rel;
|
|
|
|
#[test]
|
|
fn through_reverses_a_range_for_a_negative_fraction() {
|
|
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
|
|
let part = Len::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
|
|
let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
|
|
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
|
|
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
|
|
}
|
|
|
|
/// The case the widening is for: a part that holds only for the length it
|
|
/// was drawn at has to hold for the box it was drawn in, and a third of a
|
|
/// box is not a whole number of steps.
|
|
#[test]
|
|
fn a_part_maps_back_onto_the_box_it_was_measured_in() {
|
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
|
for box_len in (440..460).map(Px::from_int) {
|
|
let holds = Holds::at(part.to_px(box_len)).through(part);
|
|
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
|
|
}
|
|
}
|
|
|
|
/// A widget handed the whole of its parent's box, with or without pixels
|
|
/// 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_maps_back_to_itself() {
|
|
let at = Px::from_int(956);
|
|
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),
|
|
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]
|
|
fn a_fraction_widens_further_up_than_down() {
|
|
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
|
|
let holds = Holds::at(Px::from_int(100)).through(half);
|
|
let box_len = Px::from_int(200);
|
|
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_boundary_the_next_step_along_does_not_admit_it() {
|
|
let boundary = Px::from_int(10);
|
|
let above = Holds::from(boundary.next_up()..=Px::MAX);
|
|
assert!(!above.contains(boundary));
|
|
assert!(above.contains(boundary.next_up()));
|
|
}
|
|
}
|