Thread a box in pixels down the draw, one multiply from its parent's
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>
This commit is contained in:
1 parent
5b7800264d
commit
32542d0c0b
9 files changed
+399
-557
No files matched your search
@@ -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::*;
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-6
@@ -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.
|
||||
|
||||
+43
-65
@@ -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]
|
||||
|
||||
+4
-26
@@ -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));
|
||||
|
||||
+61
-57
@@ -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<TextureHandle>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
@@ -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<WidgetId>,
|
||||
/// 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<WidgetId>,
|
||||
@@ -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<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
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<Holds>) {
|
||||
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<LayoutLen>, 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<LayoutLen>; 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;
|
||||
}
|
||||
|
||||
+153
-165
@@ -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<ActiveData>,
|
||||
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<PixelRegion> {
|
||||
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)]
|
||||
|
||||
Reference in new issue
Block a user