Compare commits

..
Author SHA1 Message Date
iris-aiandClaude Opus 5 2807a925af Make a declared length one that cannot carry a share
`declared_lens` filtered `leftover` out of both its sources and every
consumer then re-dropped it, so the rule lived in two filters and a comment.
A declaration is a `Len`: `LayoutLen::declared` states the rule once and both
sources go through it, and `Declared` replaces the bare two-element array on
`ActiveData` and in four signatures.

The two sources stay one value deliberately. A rule decides the child's box;
a hint only promises what it will report -- but `size_hint` is by contract an
exact answer with no painter context, and `hints_agree` fails a widget that
draws something else, so narrowing the box to a hint cannot change what is
drawn. Every consumer asks about the length, never which said it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:06:31 -04:00
iris-aiandClaude Opus 5 55df32a33c Say layout's operations by name, and index a pair by its axis
Four rounds over the same idea: an expression that needed a comment to say
what it computed wanted to be a named operation.

The placement description is built by chaining off the value that says it.
`UiSpan::within_desc`/`shifted_desc` and `Len::as_desc` replace the
`PlaceDescAxis::` constructors, `PlaceDescAxis::axis` lifts one axis into a
pair with the whole box across it, and `PlaceDesc::per_axis` covers the case
where the two axes differ. `beside` is dropped: `from_axis` already said it.

Seven module-level functions become methods on the value each took first --
`Widgets::declared_lens`, `LayoutLen::fills`, `PlaceDesc::placement` and
`::rel_base_and_region`, `Size::within_box`, `UiRegion::at_origin` and
`::as_translation`.

`UiSpan::place` is the aligned-placement rule, which was written out three
times; `LayoutLen::without_leftover` is the sibling `apply_leftover` never
had, at six sites; `is_px` and `is_only_leftover` name field comparisons the
surrounding comments had to translate; `Holds::covers` was interval
containment spelled out by hand. A span's `shared` loses the two arguments
that did not vary across its loop.

`LayoutHolds` was four two-element arrays where every other pair here is a
struct of two per-axis values, so nothing it did could be written once.
It becomes `AxisHolds` on `x` and `y`, and `and`, `covers` and `contains`
lose their loops.

Every pair gets `Index<Axis>`/`IndexMut<Axis>` through one macro, and the
eighteen `axis`/`axis_mut` methods go. `const_index` keeps the accessors
usable in const context.

Cold layout is unchanged: `layout_dump` over 400 depth-5 trees is identical
to 58ce74d byte for byte, across all 34,492 boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:56:59 -04:00
iris-ai 58ce74dd7d One argument says where a child goes and what its fractions are of
`Place` was a product written as a sum -- a `Part` and a fill flag -- and
`Part` named three operations the geometry already had, under words that did
not match them. `Of` was `UiSpan::within`, `From` was `UiSpan::shift`, and
`Sized` was `placement`'s own body with the length given rather than
reported. Both enums are gone.

`PlaceDescAxis` says one axis, named after the operation it performs:
`within`, `shifted`, `sized`, and `WHOLE`. What is optional is a builder --
`fills` and `rel_base` -- so a caller writes only what it decided, and the
rel base it does not write follows the constructor: a span composed into the
caller's box narrows it, a span along a cursor does not, a decided length is
it. That was the one rule a caller could get wrong with nothing failing.

`PlaceDesc` says both axes with named fields, so `axis`, `axis_mut` and
`from_axis` work the way they do on every other pair here, and the joint
work -- resolving a region, reading the fill flags -- is written once rather
than per axis. `widget_at` and `place_at` take `impl Into<PlaceDesc>`, so a
wrapper passes a `UiRegion` and says nothing else. `widget_within` and
`ActiveData::narrow_rel_base` are deleted; `asked` and `placed` carry the
rel base their ask stated.

Cold layout is byte-identical to `84dad21`.
2026-09-19 17:59:26 -04:00
iris-ai c55be21761 Put place before the rel base, and say what a Place decides
An argument that is usually `None` goes last. So `widget_at` and `place_at`
take the place first, and `narrow_rel_base` after it.

`Place` and `Part` also now say which of the two boxes they decide, since
that is the question a caller has to answer to pick between them: `Part` is
the child's region, said as a part of the caller's own, and `Within`/`Fill`
is what becomes of the placement in it.
2026-09-19 17:16:41 -04:00
iris-ai beb138632a Say narrow_rel_base, and let a container pass None for it
`narrow` said what the argument did to a value it never named, so a reader
had to go and find out which value. It is `narrow_rel_base`, and the
resolved one stays the bare `rel_base` -- which is also the only one in
`Painter`, `Placing` and `LayoutHolds`, where there is nothing to tell it
apart from.

It takes `impl Into<Option<[Option<Len>; 2]>>`, so a container that does not
narrow anything writes `None` rather than `[None; 2]`, and `Span` builds the
one case that does with a `then` instead of a mutable array.

Cold layout is byte-identical to `84dad21`.
2026-09-19 16:55:48 -04:00
iris-ai aeb60e50f5 Say rel base, and give containers back a box to hand over
`frame` named a length, not a rectangle, which was the one word in the
layout vocabulary that lied about its own shape. It is `rel_base`: what a
fraction a widget declares or reports is a fraction of.

Three API changes with it, all for containers that do one simple thing:

- `widget_within(id, region)` returns, taking a box in the widget's own
  coordinates and deriving the child's rel base from it. `Offset` and `Pad`
  are one call each again. `Offset` also stops reading `region_len`, which
  pinned its drawing to a box length it does not care about.
- `place_at` takes the rel base, returns the answer, and asks the child
  where there is no answer to re-express. Which of the two happens is the
  painter's to work out, so `Span`'s second pass is one call and its
  `drawn_across` bookkeeping is gone.
- `Part::All` is a `Part::WHOLE` constant rather than a variant, since it
  was exactly `Of(UiSpan::FULL)` and bought a separate arm in two matches.
  Measured at 0.07% of instructions retired against 0.04% run-to-run noise.

Cold layout is byte-identical to `84dad21` over 400 depth-5 trees.
2026-09-19 16:33:49 -04:00
iris-ai a904cf4f36 TODO: transforms on a move entry, for stretch and rotation 2026-09-19 15:07:51 -04:00
iris-ai 5642f2010a Say region and placement, not extent
The split box was named `region` and `placement` on 2026-09-17; `frame`
came back as a length and survived, `extent` did not. It stayed as the
name for both halves, distinguished only by prose: `draw_at` bound the
caller's `part` to a parameter called `extent`, and `ActiveData` held two
`UiRegion`s that `draw_at` wrote `part: extent` from.

The box a parent asks a widget in is now the region, and where its
drawing ends up is its placement. `Painter`'s four holds accumulators
become the one `LayoutHolds` they were assembled into, which also drops
the name mapping between them.

The cold dump of 400 depth-5 trees is byte-identical across the change.
2026-09-19 14:49:56 -04:00
iris-ai 84dad211f5 Avoid repeated plan generation and unused diagnostics in layout fuzzers 2026-09-19 13:43:11 -04:00
iris-ai add6774980 Keep retained masks and reparented drawings alive, and advance collapsed slots 2026-09-19 13:43:11 -04:00
iris-ai cadfba05dd Keep only what a room drawing is still needed for
A span measuring a child in the room kept its whole `Size`, of which the
along axis is already in `lens` and only the across one is read again when
the drawing is placed. Keep that length alone, which also retires the
rebinding of the match's result and the one in the placing loop. The
placement comment already says what becomes of a drawing made in the room,
so the measuring pass no longer says it a second time.
2026-09-19 02:51:08 -04:00
iris-ai 38b3a81053 Pin a frame by the fraction the child declared
`size_hint` resolves a child's hint against the asking widget's frame and
pins that frame, so a later draw cannot reuse a resolution made against a
different one. It asked the *resolved* hint whether it still had a fraction,
which is false whenever the frame is itself pixels -- a slot of a row, or the
box a stack's sizing child decided -- and the pin was dropped there. Ask the
declared hint, which is what made this draw depend on the frame, and what
`ruled` in `render_state` already asks for a rule.

No generated tree distinguishes the two: the fuzzer grows no `rel` rules, and
a frame that changes almost always changes a box the other pins catch. Kept
for the reason the `frame_len` pin beside it is kept -- "these two
invalidations always coincide" is an assumption nothing states.
2026-09-19 02:51:08 -04:00
28 changed files with 1245 additions and 970 deletions

No files matched your search

+10
View File
@@ -14,3 +14,13 @@ WidgetRef<W> or smth instead of Id
vecs for each widget type? vecs for each widget type?
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..?? POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
transforms on a move entry (scale + rotation)
an entry is a translation today; composing through one scales the rel
part and passes px through untouched, so fixed-size content and glyphs
do not follow a shortened entry
want a real transform per entry, resolved in resolve_move the way the
translation already is, so a whole subtree transforms with one buffer
write and no redraw
wanted for compose-style stretch at the end of a scroll area, and for
rotation generally
+5 -5
View File
@@ -53,12 +53,12 @@ pub(crate) enum Counter {
TextBreaks, TextBreaks,
GlyphPlacements, GlyphPlacements,
OutsidePinnedLen, OutsidePinnedLen,
OutsideFrame, OutsideRelBase,
OutsideExtent, OutsideRegion,
} }
impl Counter { impl Counter {
const COUNT: usize = Self::OutsideExtent as usize + 1; const COUNT: usize = Self::OutsideRegion as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"updates", "updates",
@@ -89,8 +89,8 @@ impl Counter {
"text line breaks", "text line breaks",
"glyph placements", "glyph placements",
"reuse outside: the length it was pinned to", "reuse outside: the length it was pinned to",
"reuse outside: a frame length", "reuse outside: a rel base",
"reuse outside: an extent length", "reuse outside: a region length",
]; ];
} }
+1
View File
@@ -9,6 +9,7 @@
#![feature(unsize)] #![feature(unsize)]
#![feature(coerce_unsized)] #![feature(coerce_unsized)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#![feature(const_index)]
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
pub mod layout_diagnostics; pub mod layout_diagnostics;
+3 -14
View File
@@ -1,3 +1,4 @@
use crate::util::impl_axis_index;
use crate::{Px, Rel}; use crate::{Px, Rel};
use super::*; use super::*;
@@ -89,20 +90,6 @@ impl RegionAlign {
x: AxisAlign::NEG, x: AxisAlign::NEG,
y: AxisAlign::NEG, y: AxisAlign::NEG,
}; };
pub fn axis(&self, axis: Axis) -> AxisAlign {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl RegionAlign { impl RegionAlign {
@@ -231,3 +218,5 @@ impl RegionAlign {
UiVec2::from(self) UiVec2::from(self)
} }
} }
impl_axis_index!(RegionAlign => AxisAlign);
+4 -28
View File
@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::util::impl_axis_index;
use crate::{Fixed, FixedVec2}; use crate::{Fixed, FixedVec2};
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
@@ -53,20 +54,6 @@ pub enum Sign {
} }
impl<const SHIFT: u32> FixedVec2<SHIFT> { impl<const SHIFT: u32> FixedVec2<SHIFT> {
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self { pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
match axis { match axis {
Axis::X => Self::new(aligned, ortho), Axis::X => Self::new(aligned, ortho),
@@ -76,20 +63,6 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
} }
impl Vec2 { impl Vec2 {
pub fn axis(&self, axis: Axis) -> f32 {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut f32 {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self { pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
Self { Self {
x: match axis { x: match axis {
@@ -148,3 +121,6 @@ impl<T> BothAxis<T> {
} }
} }
} }
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
impl_axis_index!(Vec2 => f32);
+32 -15
View File
@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::util::impl_axis_index;
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op}; use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq)]
@@ -118,20 +119,6 @@ impl Size {
}, },
} }
} }
pub fn axis(&self, axis: Axis) -> LayoutLen {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl LayoutLen { impl LayoutLen {
@@ -158,11 +145,39 @@ impl LayoutLen {
Len::from_parts(self.rel.add(share), self.px) Len::from_parts(self.rel.add(share), self.px)
} }
/// Only pixels: the same number of them whatever box it lands in, and
/// whatever anyone else in the row asks for. A length that is any part
/// of a box or of what is left over is not one.
pub fn is_px(self) -> bool {
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
}
/// Nothing but a claim on what is left over, so there is no length here
/// at all where nothing is.
pub fn is_only_leftover(self) -> bool {
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
}
/// This as a length of a box, where it is one. `leftover` is not: a
/// share of what is left over is a length only to whoever divides one,
/// so it passes up in the reported size instead and is resolved there.
pub fn declared(self) -> Option<Len> {
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
}
/// What this takes whatever is left over: the reading of a length for
/// anyone not dividing a box between siblings, where a share is a claim
/// on someone else's room rather than a length of its own.
/// [`Self::apply_leftover`] is the opposite reading of the same value.
pub const fn without_leftover(self) -> Len {
Len::from_parts(self.rel, self.px)
}
/// This length, given as a part of a box `len` long, as a part of the /// This length, given as a part of a box `len` long, as a part of the
/// box `len` is itself a part of. The share is untouched: it is a claim /// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything. /// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self { pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len); let part = self.without_leftover().within_len(len);
Self { Self {
px: part.px, px: part.px,
rel: part.rel, rel: part.rel,
@@ -236,3 +251,5 @@ impl std::fmt::Display for LayoutLen {
Ok(()) Ok(())
} }
} }
impl_axis_index!(Size => LayoutLen);
+13 -28
View File
@@ -1,3 +1,4 @@
use crate::util::impl_axis_index;
use std::{fmt::Display, marker::Destruct}; use std::{fmt::Display, marker::Destruct};
use super::*; use super::*;
@@ -61,20 +62,6 @@ impl UiVec2 {
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub fn axis(&self, axis: Axis) -> Len {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
/// Resolved against a box of `size`, which is where a fraction stops /// Resolved against a box of `size`, which is where a fraction stops
/// being one and becomes a place. /// being one and becomes a place.
pub fn to_px(&self, size: PxVec2) -> PxVec2 { pub fn to_px(&self, size: PxVec2) -> PxVec2 {
@@ -294,6 +281,15 @@ impl UiSpan {
} }
} }
/// A box `len` long inside this one, on the side `align` says. Both must
/// be lengths of the same rel base: it subtracts one from the other
/// rather than composing it in, which is what keeps a fraction the same
/// fraction however long this box turns out to be.
pub const fn place(self, len: Len, align: AxisAlign) -> Self {
let start = self.start + (self.len() - len).scale(align.rel());
Self::new(start, start + len)
}
pub const fn len(&self) -> Len { pub const fn len(&self) -> Len {
self.end - self.start self.end - self.start
} }
@@ -348,20 +344,6 @@ impl UiRegion {
y: self.y.within(&parent.y), y: self.y.within(&parent.y),
} }
} }
pub const fn axis(&self, axis: Axis) -> &UiSpan {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut UiSpan {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn flip(&mut self, axis: Axis) { pub const fn flip(&mut self, axis: Axis) {
match axis { match axis {
Axis::X => self.x.flip(), Axis::X => self.x.flip(),
@@ -462,3 +444,6 @@ impl Display for PixelRegion {
write!(f, "{} -> {}", self.top_left, self.bot_right) write!(f, "{} -> {}", self.top_left, self.bot_right)
} }
} }
impl_axis_index!(UiVec2 => Len);
impl_axis_index!(UiRegion => UiSpan);
+19 -23
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
@@ -10,26 +10,21 @@ use crate::{
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// Where its drawing goes, in its region node's coordinates. /// Where its drawing goes, in its region node's coordinates.
pub extent: UiRegion, pub placement: UiRegion,
/// What a fraction declared or reported under this widget is a fraction /// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window. /// of, as a length of the window.
pub frame: UiVec2, pub rel_base: UiVec2,
/// A frame its parent decided for it on each axis -- a row's slot, or /// Where its drawing was put, and where it was asked. The two differ
/// padding's frame less its pixels -- as a length of the window. `None` /// where a container asks in one place and puts the answer in another --
/// forwards the parent's frame. What it declared is kept separately in /// a row measures from its cursor and puts the child in its slot. Each
/// `declared` and is a fraction of whichever of the two reached it. /// carries the rel base that ask stated, so asking again from either is
pub narrow: [Option<Len>; 2], /// the same question it was.
/// Where its drawing was put, and where it was asked, each as a part of pub placed: PlaceDesc,
/// its parent's box. The two differ where a container asks in one place pub asked: PlaceDesc,
/// and puts the answer in another -- a row measures from its cursor and
/// puts the child in its slot. A part is a length from the box's start,
/// so a box that moved re-places every child by re-adding that start.
pub placed: [Place; 2],
pub asked: [Place; 2],
/// The box it was asked in, in the parent's region-node coordinates: the /// The box it was asked in, in the parent's region-node coordinates: the
/// box its drawing was made in and the one its contract is about. Its /// box its drawing was made in and the one its contract is about. Its
/// drawing is placed elsewhere by re-expression, never by asking again. /// drawing is placed elsewhere by re-expression, never by asking again.
pub part: UiRegion, pub region: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or /// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet. /// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>, pub answer: Option<(Size, LayoutHolds)>,
@@ -40,8 +35,8 @@ pub struct ActiveData {
pub re_asked: bool, pub re_asked: bool,
/// What the widget reported, in window-unit lengths. /// What the widget reported, in window-unit lengths.
pub size: Size, pub size: Size,
/// The window and extent reads that this drawing holds for, and the /// The window and region reads that this drawing holds for, and the
/// frame and box it pinned. /// rel base and region it pinned.
pub holds: LayoutHolds, pub holds: LayoutHolds,
pub drawn: bool, pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
@@ -51,8 +46,9 @@ pub struct ActiveData {
pub depth: usize, pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
/// Its primitives, each keeping the box it was written in -- in this /// Its primitives, each keeping the box it was written in -- in this
/// widget's extent coordinates, which is what a move recomposes from. /// widget's placement coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>, pub primitives: Vec<RetainedPrimitive>,
/// An owned mask holds one reference independently of its primitives.
pub mask_region: Option<UiRegion>, pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
@@ -60,15 +56,15 @@ pub struct ActiveData {
/// The movable region its primitives are positioned through: its own when /// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's. /// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
/// The declared lengths whoever drew this widget resolved into its frame. /// The declared lengths whoever drew this widget resolved into its rel base.
/// A change to one moves a box this widget cannot fix by drawing again, /// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so. /// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2], pub declared: Declared,
/// Its alignment when it was last drawn, which a change to the property /// Its alignment when it was last drawn, which a change to the property
/// is found against. /// is found against.
pub own_align: RegionAlign, pub own_align: RegionAlign,
/// The movable region whose coordinates `extent` uses when this widget /// The movable region whose coordinates its placement is in when this
/// does not own a region node. /// widget does not own a region node.
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it /// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it. /// inherited from whoever drew it.
+6 -15
View File
@@ -33,6 +33,12 @@ impl Holds {
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw() len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
} }
/// Every length `other` holds for is one this holds for, so a drawing
/// made under this range is still good wherever `other` is.
pub const fn covers(self, other: Self) -> bool {
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
}
pub const fn and(self, other: Self) -> Self { pub const fn and(self, other: Self) -> Self {
Self { Self {
lo: self.lo.max(other.lo), lo: self.lo.max(other.lo),
@@ -73,21 +79,6 @@ impl Holds {
} }
} }
/// What a box has to be for a part of it, this many pixels shorter, to
/// stay in this range: the range moved by that much, an end that was
/// unbounded staying so.
pub const fn longer_by(self, px: Px) -> Self {
let lo = match self.lo.raw() == Px::MIN.raw() {
true => self.lo,
false => self.lo.add(px),
};
let hi = match self.hi.raw() == Px::MAX.raw() {
true => self.hi,
false => self.hi.add(px),
};
Self { lo, hi }
}
const fn raws(lo: i64, hi: i64) -> Self { const fn raws(lo: i64, hi: i64) -> Self {
Self { Self {
lo: Px::from_raw(narrow(lo)), lo: Px::from_raw(narrow(lo)),
+81 -51
View File
@@ -1,79 +1,109 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2}; use crate::util::impl_axis_index;
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2};
const AXES: [Axis; 2] = [Axis::X, Axis::Y]; const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on: the window lengths its reads /// What one evaluation of a widget depends on along one axis: the window
/// hold for, the pixel lengths of its own box, and the symbolic lengths of /// lengths its reads hold for, the pixel lengths of its own box, and the
/// that box and of its frame where either one is what it was expressed in. /// symbolic lengths of that box and of its rel base where either one is what
/// it was expressed in.
/// ///
/// The symbolic lengths are pins rather than ranges: a container places its /// The symbolic lengths are pins rather than ranges: a container places its
/// children as lengths of its frame measured from where its own box starts, /// children as lengths of its rel base measured from where its own box starts,
/// so what it draws turns on that box's length and on nothing about where it /// so what it draws turns on that box's length and on nothing about where it
/// is. A box pin reaches the parent only where the box it pinned is the /// is. A box pin reaches the parent only where the box it pinned is the
/// parent's own; anywhere else the parent chose that length itself, and a /// parent's own; anywhere else the parent chose that length itself, and a
/// widget pinned this way is checked when it is re-placed. /// widget pinned this way is checked when it is re-placed.
/// ///
/// A frame pin says the answer or the drawing is a fraction of the frame, /// A rel base pin says the answer or the drawing is a fraction of the rel base,
/// which is a different length wherever the frame is a different one -- at /// which is a different length wherever the rel base is a different one -- at
/// the same window size, so no range of window pixels can say it. A length /// the same window size, so no range of window pixels can say it. A length
/// of the frame that is only pixels is not one: it is that many pixels /// of the rel base that is only pixels is not one: it is that many pixels
/// whatever the frame turns out to be. /// whatever the rel base turns out to be.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AxisHolds {
pub window: Holds,
pub rel_base: Option<Len>,
pub region: Holds,
pub region_len: Option<Len>,
}
impl AxisHolds {
pub const ANY: Self = Self {
window: Holds::ANY,
rel_base: None,
region: Holds::ANY,
region_len: None,
};
pub fn and(self, other: Self) -> Self {
// Two pins of the same length disagreeing would mean one drawing was
// a fraction of two different lengths at once.
debug_assert!(
self.region_len.is_none()
|| other.region_len.is_none()
|| self.region_len == other.region_len
);
debug_assert!(
self.rel_base.is_none() || other.rel_base.is_none() || self.rel_base == other.rel_base
);
Self {
window: self.window.and(other.window),
rel_base: self.rel_base.or(other.rel_base),
region: self.region.and(other.region),
region_len: self.region_len.or(other.region_len),
}
}
pub fn covers(self, other: Self) -> bool {
self.window.covers(other.window)
&& self.region.covers(other.region)
&& self
.region_len
.is_none_or(|len| other.region_len == Some(len))
&& self.rel_base.is_none_or(|len| other.rel_base == Some(len))
}
/// Whether a widget in a box `len` long, with that rel base, in that
/// window, is one this drawing holds for.
pub fn contains(self, window: Px, rel_base: Len, len: Len) -> bool {
self.window.contains(window)
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
&& self.region.contains(len.to_px(window))
&& self.region_len.is_none_or(|pinned| pinned == len)
}
}
/// [`AxisHolds`] on both axes. Every question asked of it is asked of one
/// axis at a time, since a widget that read one length holds for any length
/// of the other.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds { pub struct LayoutHolds {
pub window: [Holds; 2], pub x: AxisHolds,
pub frame_len: [Option<Len>; 2], pub y: AxisHolds,
pub extent: [Holds; 2],
pub extent_len: [Option<Len>; 2],
} }
impl LayoutHolds { impl LayoutHolds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
window: [Holds::ANY; 2], x: AxisHolds::ANY,
frame_len: [None; 2], y: AxisHolds::ANY,
extent: [Holds::ANY; 2],
extent_len: [None; 2],
}; };
pub fn and(self, other: Self) -> Self { pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY; Self {
for n in 0..2 { x: self.x.and(other.x),
result.window[n] = self.window[n].and(other.window[n]); y: self.y.and(other.y),
result.extent[n] = self.extent[n].and(other.extent[n]);
debug_assert!(
self.extent_len[n].is_none()
|| other.extent_len[n].is_none()
|| self.extent_len[n] == other.extent_len[n]
);
debug_assert!(
self.frame_len[n].is_none()
|| other.frame_len[n].is_none()
|| self.frame_len[n] == other.frame_len[n]
);
result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]);
result.frame_len[n] = self.frame_len[n].or(other.frame_len[n]);
} }
result
} }
pub fn covers(self, other: Self) -> bool { pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| { self.x.covers(other.x) && self.y.covers(other.y)
self.window[n].lo <= other.window[n].lo
&& self.window[n].hi >= other.window[n].hi
&& self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi
&& self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len))
&& self.frame_len[n].is_none_or(|len| other.frame_len[n] == Some(len))
})
} }
pub fn contains(self, window: PxVec2, frame: UiVec2, extent: UiRegion) -> bool { pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
AXES.into_iter().all(|axis| { AXES.into_iter()
let n = axis as usize; .all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
let len = extent.axis(axis).len();
self.window[n].contains(window.axis(axis))
&& self.frame_len[n].is_none_or(|pinned| pinned == frame.axis(axis))
&& self.extent[n].contains(len.to_px(window.axis(axis)))
&& self.extent_len[n].is_none_or(|pinned| pinned == len)
})
} }
} }
impl_axis_index!(LayoutHolds => AxisHolds);
+269 -277
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, Rel, Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive, TexturePrimitive,
@@ -17,17 +17,14 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's frame, per axis: a length of the window, and what a /// This widget's rel base, per axis: a length of the window, and what a
/// fraction it or anything under it declares or reports is a fraction /// fraction it or anything under it declares or reports is a fraction
/// of. A length rather than a box, so padding can take from both the /// of. A length rather than a box, so padding can take from both the
/// frame and the box without either becoming the other. /// rel base and the box without either becoming the other.
pub(super) frame: UiVec2, pub(super) rel_base: UiVec2,
/// Where this widget's drawing goes, in its region node's coordinates. /// The box this widget was asked in, in its region node's coordinates:
pub(super) extent: UiRegion, /// what it draws in, and what its children's places are parts of.
/// The extent's symbolic length where this draw read it, which makes the pub(super) region: UiRegion,
/// drawing one that holds for that length alone -- the way reading a
/// length in pixels makes it hold for that number of pixels.
pub(super) extent_len: [Option<Len>; 2],
/// The window in pixels. Frames and boxes become pixels against this one /// The window in pixels. Frames and boxes become pixels against this one
/// unit, regardless of region-node boundaries. /// unit, regardless of region-node boundaries.
pub(super) window: PxVec2, pub(super) window: PxVec2,
@@ -35,19 +32,19 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<UiRegion>, pub(super) mask_region: Option<UiRegion>,
/// The previous drawing's owned mask, available for this draw to reclaim.
pub(super) mask_slot: Option<MaskIdx>,
/// Only children whose answers were read constrain this widget's answer. /// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds, pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of the window in pixels, per axis: every /// What this draw itself reads, as against what its children's drawings
/// window until it reads one, then that one, unless it says otherwise. /// hold for: every window and every length of its own region until it
pub(super) window_own: [Holds; 2], /// reads one, then that one unless it says otherwise, and the rel base or
/// Its frame's symbolic length where this draw read it, which makes the /// region length it read symbolically, each of which makes the drawing
/// drawing one that holds for that frame alone. /// hold for that length alone.
pub(super) frame_own_len: [Option<Len>; 2], pub(super) own: LayoutHolds,
/// The window reads' equivalent for its own box.
pub(super) extent_own: [Holds; 2],
/// What each child's drawing depends on. Asking a child again replaces /// What each child's drawing depends on. Asking a child again replaces
/// its drawing, so it replaces this too rather than narrowing it. /// its drawing, so it replaces this too rather than narrowing it.
pub(super) under: Vec<(WidgetId, LayoutHolds)>, pub(super) under: Vec<(WidgetId, LayoutHolds)>,
@@ -73,10 +70,10 @@ impl<'a> Painter<'a> {
self.write_resolved(kind, primitive, region, self.resolve(region)); self.write_resolved(kind, primitive, region, self.resolve(region));
} }
/// A box in this widget's extent coordinates, composed into its region /// A box in this widget's region, composed into its region node's
/// node's coordinates. /// coordinates.
fn resolve(&self, region: UiRegion) -> UiRegion { fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent) region.within(&self.region)
} }
fn write_resolved<P: Primitive>( fn write_resolved<P: Primitive>(
@@ -104,7 +101,6 @@ impl<'a> Painter<'a> {
fn push_primitive(&mut self, h: RetainedPrimitive) { fn push_primitive(&mut self, h: RetainedPrimitive) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
} }
self.primitives.push(h); self.primitives.push(h);
@@ -129,58 +125,79 @@ impl<'a> Painter<'a> {
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
let resolved = self.resolve(region); let resolved = self.resolve(region);
let move_idx = self.move_idx; let move_idx = self.move_idx;
self.mask = self.rsc.ui_mut().masks.push(Mask { let mask = Mask {
region: resolved, region: resolved,
move_idx, move_idx,
}); };
let masks = &mut self.rsc.ui_mut().masks;
self.mask = match self.mask_slot.take() {
Some(idx) => {
*masks.get_mut(idx) = mask;
idx
}
None => {
let idx = masks.push(mask);
// The owner keeps the slot alive even with no primitives.
masks.push_ref(idx);
idx
}
};
} }
/// Draws a widget in the whole of this widget's own box, with the frame /// Draws a widget in the whole of this widget's own box, with the rel
/// forwarded unchanged: what a container that is only a wrapper around /// base forwarded unchanged: what a container that is only a wrapper
/// one child wants, and what every transparent container passes for the /// around one child wants.
/// frame.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, [None; 2], [Place::Within(Part::All); 2]) self.widget_at(id, UiRegion::FULL)
}
/// Resolves what the place says about the child's rel base into a length,
/// where that is this widget's own narrowed the way the region is. An
/// axis the region leaves whole is not read at all, so a wrapper that
/// only moves its child does not pin its drawing to a rel base.
fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
for axis in AXES {
if let Some(span) = place[axis].narrows_rel_base() {
let len = span.len();
let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis)));
place[axis] = place[axis].with_rel_base(stated);
}
}
place
} }
/// Asks a child, saying what its fractions are of and where it is asked. /// Asks a child, saying what its fractions are of and where it is asked.
/// ///
/// `narrow` is a length this widget decided for the child's frame, per /// `place` says where the child goes and what its fractions are of:
/// axis, as a length of this widget's own frame: a resolved share, or a /// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which
/// box a sibling's answer decided. `None` forwards this widget's frame, /// is a box of this widget's own with the answer placed inside it.
/// which is what a container that only divides room passes, so a
/// fraction under it means the same wherever it sits and however deeply
/// it is nested. A declared length narrows the frame here whatever the
/// caller says. A narrowed frame is placed in the part by the child's
/// alignment and is the box the child is asked in.
/// ///
/// `place` is where the child is asked, per axis, as a part of this /// The child draws once, in the region that comes of it, and its answer
/// widget's box: see [`Place`]. The child draws once, in that box, and /// is placed inside that region by re-expressing the drawing. Nothing is
/// its answer is placed inside it by re-expressing the drawing. Nothing /// drawn again in a box an answer chose; a container that puts the
/// is drawn again in a box an answer chose; a container that puts the
/// answer somewhere else says so with [`Self::place_at`]. /// answer somewhere else says so with [`Self::place_at`].
pub fn widget_at<'s, W: ?Sized>( pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
narrow: [Option<Len>; 2], place: impl Into<PlaceDesc>,
place: [Place; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
let (frame, extent) = let (rel_base, region) =
frame_and_extent(self.extent, self.frame, place, narrow, declared, align); place.rel_base_and_region(self.region, self.rel_base, declared, align);
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
if region_node { if region_node {
diag::bump(Counter::RegionNodeDraws); diag::bump(Counter::RegionNodeDraws);
diag::region_node(id.id(), self.id, extent); diag::region_node(id.id(), self.id, region);
} }
// A child listed twice would be moved twice. // A child listed twice would be moved twice.
let re_asked = self.children.contains(&id.id()); let re_asked = self.children.contains(&id.id());
if !re_asked { if !re_asked {
self.children.push(id.id()); self.children.push(id.id());
} }
let px = frame.to_px(self.window); let px = rel_base.to_px(self.window);
let (size, answer_holds, holds) = self.state.draw_inner( let (size, answer_holds, holds) = self.state.draw_inner(
id.id(), id.id(),
DrawInfo { DrawInfo {
@@ -190,19 +207,18 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
frame, rel_base,
part: extent, region,
placed: place, placed: place,
asked: place, asked: place,
narrow,
re_asked, re_asked,
px, px,
}, },
None, None,
self.rsc, self.rsc,
); );
let holds = self.in_parent(holds, extent, place, narrow, declared); let holds = self.in_parent(holds, region, place, declared);
let answer_holds = self.in_parent(answer_holds, extent, place, narrow, declared); let answer_holds = self.in_parent(answer_holds, region, place, declared);
match self.under.iter_mut().find(|(child, _)| *child == id.id()) { match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
Some((_, kept)) => *kept = holds, Some((_, kept)) => *kept = holds,
None => self.under.push((id.id(), holds)), None => self.under.push((id.id(), holds)),
@@ -224,26 +240,47 @@ impl<'a> Painter<'a> {
self.state.undraw_rec(id.id(), self.rsc); self.state.undraw_rec(id.id(), self.rsc);
} }
/// Puts a child asked about in this draw somewhere else in this /// Puts a child in `place` of this widget's box, where that box is the
/// widget's box: its answer, placed in this part instead. The drawing /// answer the child already gave: the drawing is re-expressed there
/// is re-expressed there rather than made again -- what a row does once /// rather than made again -- what a row does once it knows every slot,
/// it knows every slot, having measured each child from its cursor. /// having measured each child from its cursor.
pub fn place_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, place: [Place; 2]) { ///
debug_assert!( /// A child this draw has not asked about, and one whose rel base this
self.children.contains(&id.id()), /// narrows, is asked here instead: there is no answer to re-express, or
"'{}' placed a child it did not ask about in this draw", /// the question has changed. So a container that places every child the
self.label() /// same way says it once, and which of the two happens is this widget's
); /// business rather than the caller's.
pub fn place_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
let states_rel_base = AXES
.iter()
.any(|&axis| place[axis].stated_rel_base().is_some());
if states_rel_base || !self.children.contains(&id.id()) {
return self.widget_at(id, place);
}
let at = self.placing(); let at = self.placing();
self.state.place_in(id.id(), &at, place, self.rsc); self.state.place_in(id.id(), &at, place, self.rsc);
let active = &self.state.active[&id.id()];
let size = active.measured().unwrap_or(active.size);
DrawResult {
child: id,
painter: self,
size,
// Read where it was asked; moving it is not a second answer.
answer_holds: LayoutHolds::ANY,
}
} }
/// This widget as the thing its children are placed within. /// This widget as the thing its children are placed within.
fn placing(&self) -> Placing { fn placing(&self) -> Placing {
Placing { Placing {
id: self.id, id: self.id,
extent: self.extent, region: self.region,
frame: self.frame, rel_base: self.rel_base,
window: self.window, window: self.window,
depth: self.depth, depth: self.depth,
move_idx: self.move_idx, move_idx: self.move_idx,
@@ -252,51 +289,47 @@ impl<'a> Painter<'a> {
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// What a widget's rules declare its lengths to be, which whoever draws
/// it resolves into its frame. Reading them depends on nothing -- the box /// it resolves into its rel base. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it /// that comes of them is kept on the child, and `redraw` compares it
/// there. /// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] { fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> Declared {
declared_lens(self.rsc.widgets(), id.id()) self.rsc.widgets().declared_lens(id.id())
} }
/// What a child says its length is without being drawn, if it can say, /// What a child says its length is without being drawn, if it can say,
/// as the length its draw would report: a fraction in it is resolved /// as the length its draw would report: a fraction in it is resolved
/// against this widget's frame, which is the frame a child asked with /// against this widget's rel base, which is the rel base a child asked with
/// nothing narrowed gets. Asking counts as reading its size. /// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let widgets = self.rsc.widgets(); let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the // A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too. // widget would draw, so it has to win over what the widget says too.
let hint = widgets let hint = widgets.size_rules(id.id())[axis].exact().or_else(|| {
.size_rules(id.id()) widgets
.axis(axis) .get_dyn(id.id())
.exact() .and_then(|widget| widget.size_hint(axis))
.or_else(|| { });
widgets let rel_base = self.rel_base[axis];
.get_dyn(id.id()) let resolved = hint.map(|hint| hint.within_len(rel_base));
.and_then(|widget| widget.size_hint(axis))
})
.map(|hint| hint.within_len(self.frame.axis(axis)));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint); {
match hint { diag::hint_read(id.id(), self.id, axis, resolved);
Some(hint) => { diag::bump(match resolved {
#[cfg(feature = "layout-diagnostics")] Some(_) => Counter::HintHits,
diag::bump(Counter::HintHits); None => Counter::HintMisses,
self.depend_on(id); });
// A fraction was just resolved against this frame, so what }
// this draw does with it is a function of the frame's length. if let Some(hint) = hint {
if hint.rel != Rel::ZERO { self.depend_on(id);
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis)); // Resolving a fraction against this rel base makes this draw a
} // function of the rel base's length. The fraction to ask about is
Some(hint) // the child's own: resolved against a rel base of pixels, none is
} // left to see it by.
None => { if hint.rel != Rel::ZERO {
#[cfg(feature = "layout-diagnostics")] self.own[axis].rel_base = Some(rel_base);
diag::bump(Counter::HintMisses);
None
} }
} }
resolved
} }
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
@@ -317,11 +350,11 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// Writes glyphs in the selected frame or extent coordinates. /// Writes glyphs in the selected rel base or region coordinates.
// TODO: merge the text methods into the primitive ones. // TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
// Glyph offsets and sizes are pixels, which compose additively. // Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs composing through the extent. // Only the shared origin needs composing through the region.
let resolved = self.resolve(origin); let resolved = self.resolve(origin);
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
@@ -353,25 +386,25 @@ impl<'a> Painter<'a> {
} }
/// The symbolic length of this widget's own box along one axis, in the /// The symbolic length of this widget's own box along one axis, in the
/// lengths of its frame that it places its children in. Reading it pins /// lengths of its rel base that it places its children in. Reading it pins
/// the drawing to that length -- and to nothing about where the box /// the drawing to that length -- and to nothing about where the box
/// starts, which is what lets a container move without being drawn /// starts, which is what lets a container move without being drawn
/// again. One axis at a time, because a container that divides one axis /// again. One axis at a time, because a container that divides one axis
/// holds for any length of the other. /// holds for any length of the other.
pub fn extent_len(&mut self, axis: Axis) -> Len { pub fn region_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len(); let len = self.region[axis].len();
self.extent_len[axis as usize] = Some(len); self.own[axis].region_len = Some(len);
len len
} }
/// The symbolic length of this widget's frame along one axis: what a /// This widget's rel base along one axis: what a fraction it or anything
/// fraction it or anything under it declares is a fraction of. A /// under it declares or reports is a fraction of. A container reads it
/// container reads it to hand a length of it down -- padding, which /// to hand a length of it down -- padding, which takes its pixels off.
/// takes its pixels off. Reading it pins the drawing to that frame, the /// Reading it pins the drawing to that rel base, the way
/// way [`Self::extent_len`] pins it to the box. /// [`Self::region_len`] pins it to the box.
pub fn frame_len(&mut self, axis: Axis) -> Len { pub fn rel_base(&mut self, axis: Axis) -> Len {
let len = self.frame.axis(axis); let len = self.rel_base[axis];
self.frame_own_len[axis as usize] = Some(len); self.own[axis].rel_base = Some(len);
len len
} }
@@ -392,10 +425,7 @@ impl<'a> Painter<'a> {
/// worth anything, since reading one is also what makes its own size /// worth anything, since reading one is also what makes its own size
/// depend on it. /// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool { pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc self.rsc.widgets().size_rules(self.id)[axis]
.widgets()
.size_rules(self.id)
.axis(axis)
.exact() .exact()
.is_some() .is_some()
} }
@@ -409,13 +439,13 @@ impl<'a> Painter<'a> {
/// One axis of this widget's own box in pixels. Prefer this to /// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px { pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.extent.axis(axis).len(); let len = self.region[axis].len();
let len = part.to_px(self.window.axis(axis)); let px = len.to_px(self.window[axis]);
let own = &mut self.extent_own[axis as usize]; let own = &mut self.own[axis].region;
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(px);
} }
len px
} }
/// The lengths of this widget's own box on `axis` that what it is drawing /// The lengths of this widget's own box on `axis` that what it is drawing
@@ -423,15 +453,15 @@ impl<'a> Painter<'a> {
/// of the box, and the same reported size. A widget that read its length /// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise. /// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) { pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let part = self.extent.axis(axis).len(); let len = self.region[axis].len();
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(part.to_px(self.window.axis(axis))), holds.contains(len.to_px(self.window[axis])),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box", "'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(), self.label(),
self.id self.id
); );
self.extent_own[axis as usize] = holds; self.own[axis].region = holds;
} }
/// A window length in pixels, which is what every length in layout is /// A window length in pixels, which is what every length in layout is
@@ -439,9 +469,9 @@ impl<'a> Painter<'a> {
/// length is a fraction of it; one that is only pixels is that many /// length is a fraction of it; one that is only pixels is that many
/// pixels in any window and pins nothing. /// pixels in any window and pins nothing.
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px { pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let window = self.window.axis(axis); let window = self.window[axis];
if len.rel != Rel::ZERO { if len.rel != Rel::ZERO {
let own = &mut self.window_own[axis as usize]; let own = &mut self.own[axis].window;
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(window); *own = Holds::at(window);
} }
@@ -456,12 +486,12 @@ impl<'a> Painter<'a> {
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) { pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.window.axis(axis)), holds.contains(self.window[axis]),
"'{}' ({:?}) says its drawing holds for windows that leave out this one", "'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(), self.label(),
self.id self.id
); );
self.window_own[axis as usize] = holds; self.own[axis].window = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -520,7 +550,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
} }
pub fn len(self, axis: Axis) -> LayoutLen { pub fn len(self, axis: Axis) -> LayoutLen {
self.size().axis(axis) self.size()[axis]
} }
} }
@@ -554,76 +584,58 @@ impl PrimitiveLike for &TextureHandle {
/// only what the child was asked with. /// only what the child was asked with.
impl Painter<'_> { impl Painter<'_> {
/// Window ranges are already about the one unit and combine directly. /// Window ranges are already about the one unit and combine directly.
/// A frame pin becomes this widget's own frame wherever a length of it /// A rel base pin becomes this widget's own rel base wherever a length of it
/// is what reached the child; where only pixels did, no length of this /// is what reached the child; where only pixels did, no length of this
/// frame can change the child's and the pin stops here. /// rel base can change the child's and the pin stops here.
/// ///
/// Extent validity maps back through the part of this widget's box, /// A child's validity maps back through the part of this widget's box,
/// where the box the child was asked in is that part; a declared length /// where the box the child was asked in is that part; a declared length
/// places the box inside the part instead, and then only that length /// places the box inside the part instead, and then only that length
/// reaches the child. A narrowed frame is not one of these: it decides /// reaches the child. A narrowed rel base is not one of these: it decides
/// what fractions under the child mean and leaves the box the part it /// what fractions under the child mean and leaves the box the part it
/// was given. /// was given.
fn in_parent( fn in_parent(
&self, &self,
holds: LayoutHolds, holds: LayoutHolds,
extent: UiRegion, region: UiRegion,
place: [Place; 2], place: PlaceDesc,
narrow: [Option<Len>; 2], declared: Declared,
declared: [Option<LayoutLen>; 2],
) -> LayoutHolds { ) -> LayoutHolds {
let mut result = LayoutHolds::ANY; let mut result = LayoutHolds::ANY;
for axis in AXES { for axis in AXES {
let n = axis as usize; let declared = declared[axis];
let holds = holds[axis];
let result = &mut result[axis];
// Every read became pixels against the window, so a range on // Every read became pixels against the window, so a range on
// it is already in this widget's terms. // it is already in this widget's terms.
result.window[n] = holds.window[n]; result.window = holds.window;
let reaches = narrow[n].is_none() let at = place[axis];
&& !matches!(place[n].part(), Part::Sized(_)) let reaches = at.stated_rel_base().is_none()
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO); && !at.is_sized()
result.frame_len[n] = holds.frame_len[n].and(reaches.then(|| self.frame.axis(axis))); && declared.is_none_or(|len| len.rel != Rel::ZERO);
match (place[n].part(), declared[n].is_some()) { result.rel_base = holds.rel_base.and(reaches.then(|| self.rel_base[axis]));
// Its box is this widget's own, or a part of it in that match (at.within_span(), declared.is_some()) {
// box's own lengths: so what it holds for is a range on this // Its box is a part of this widget's own box, in that box's
// widget's own box, which is what lets that box move without // own lengths, so what it holds for maps back through that
// a redraw. A length it pinned is this widget's length // part into a range on this widget's box. A length it pinned
// wherever the part is the whole of it, and pins the same // is this widget's length less the part's pixels where the
// way. // part is the whole of the box less pixels, which is the one
(Part::All, false) => { // shape that inverts exactly; any other part pins this
result.extent[n] = holds.extent[n]; // widget's own length.
result.extent_len[n] = holds.extent_len[n]; (Some(span), false) => {
} let part_len = span.len();
// Its box is this widget's own less the inset. Where that is result.region = holds.region.through(part_len);
// pixels, its box is exactly that many shorter in any window, result.region_len = holds.region_len.map(|pinned| match part_len.rel {
// so what it holds for is a range on this widget's box moved Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
// by them, and a length it pinned is this widget's length _ => self.region[axis].len(),
// less them. An inset with a fraction in it is a different });
// number of pixels in each window, and taking it off a length
// rounds once more than taking it off pixels does: there the
// child's box is a fixed expression of this one, so this
// widget's length is pinned and the range goes on the window
// through the child's box, the way a slot's does.
(Part::Inset { lead, trail }, false) => {
let inset = lead + trail;
match inset.rel == Rel::ZERO {
true => {
result.extent[n] = holds.extent[n].longer_by(inset.px);
result.extent_len[n] = holds.extent_len[n].map(|pinned| pinned + inset);
}
false => {
result.window[n] = result.window[n]
.and(holds.extent[n].through(extent.axis(axis).len()));
result.extent_len[n] = Some(self.extent.axis(axis).len());
}
}
} }
// Its box is a length this widget decided, from its own // Its box is a length this widget decided, from its own
// frame or from a sibling's answer: no length of this // rel base or from a sibling's answer: no length of this
// widget's box reaches it, so what it holds for is a range // widget's box reaches it, so what it holds for is a range
// on the window and none of it on that box. // on the window and none of it on that box.
_ => { _ => {
result.window[n] = result.window = result.window.and(holds.region.through(region[axis].len()));
result.window[n].and(holds.extent[n].through(extent.axis(axis).len()));
} }
} }
} }
@@ -631,120 +643,100 @@ impl Painter<'_> {
} }
} }
/// What a widget declares a length of its box to be. `leftover` is not one: a impl Widgets {
/// share of what is left over is only a length to the widget dividing one, /// What a widget's box is where a rule or its own hint says so outright.
/// so it passes up in the size instead. pub(crate) fn declared_lens(&self, id: WidgetId) -> Declared {
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] { let rules = self.size_rules(id);
let rules = widgets.size_rules(id); let widget = self.get_dyn(id);
let widget = widgets.get_dyn(id); Declared::per_axis(|axis| {
AXES.map(|axis| { rules[axis].declared().or_else(|| {
rules.axis(axis).declared().or_else(|| { // A hint still narrows the box where no rule does, which is
// A hint still narrows the box where no rule does, which is how a // how a widget with a natural pixel size -- an image, a gap
// widget with a natural pixel size -- an image, a gap -- gets that // -- gets that size rather than the whole offer. That is the
// size rather than the whole offer. That is the offer's business // offer's business rather than a declaration's, and this
// rather than a declaration's, and this falls away once a widget // falls away once a widget occupies its reported size inside
// occupies its reported size inside the box it was offered. // the box it was offered.
widget widget
.and_then(|widget| widget.size_hint(axis)) .and_then(|widget| widget.size_hint(axis))
.filter(|len| len.leftover == Weight::ZERO) .and_then(LayoutLen::declared)
})
}) })
}) }
} }
/// Whether what a widget reported along an axis is the whole of the box it impl LayoutLen {
/// is in rather than a part to be placed inside it. A share fills, because a /// Whether what a widget reported along an axis is the whole of the box
/// share is a length only to whoever divides one, and whoever did is the one /// it is in rather than a part to be placed inside it. A share fills,
/// that handed down this box. A declared axis does too: the rule already gave /// because a share is a length only to whoever divides one, and whoever
/// the region its length, and the rule's length is what the widget reports /// did is the one that handed down this box. A declared axis does too:
/// there. And an axis the parent decided from the answer is /// the rule already gave the region its length, and the rule's length is
/// the answer already. /// what the widget reports there. And an axis the parent decided from
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool { /// the answer is the answer already.
reported.leftover != Weight::ZERO || declared.is_some() || decided pub(crate) fn fills(self, declared: Option<Len>, decided: bool) -> bool {
self.leftover != Weight::ZERO || declared.is_some() || decided
}
} }
/// Where a widget's drawing goes inside the part its parent gave it: what /// Where a widget's drawing goes inside the part its parent gave it: what
/// it reported, on the side of the part its alignment says, and the whole /// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it. /// part wherever the answer fills it.
/// ///
/// The length it reported is a length of its frame, and the part is one too, /// The length it reported is a length of its rel base, and the part is one too,
/// so this takes one from the other rather than composing it into the part. /// so this takes one from the other rather than composing it into the part.
/// That is what makes a fraction the same fraction wherever the part it is /// That is what makes a fraction the same fraction wherever the part it is
/// placed in sits and however long it is -- the fraction is resolved once, /// placed in sits and however long it is -- the fraction is resolved once,
/// here, against the frame it was reported of. /// here, against the rel base it was reported of.
pub(crate) fn placed_extent( impl PlaceDesc {
part: UiRegion, pub(crate) fn placement(
size: Size, self,
declared: [Option<LayoutLen>; 2], region: UiRegion,
fill: [bool; 2], size: Size,
align: RegionAlign, declared: Declared,
) -> UiRegion { align: RegionAlign,
let mut placed = part; ) -> UiRegion {
for axis in AXES { let mut placed = region;
let n = axis as usize; for axis in AXES {
let reported = size.axis(axis); let reported = size[axis];
if fills(reported, declared[n], fill[n]) { if reported.fills(declared[axis], self[axis].does_fill()) {
continue; continue;
}
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
} }
let len = Len::from_parts(reported.rel, reported.px); placed
let span = placed.axis_mut(axis);
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
} }
placed
}
/// The frame length and the box a child is asked in, in the coordinates the /// The rel base length and the box a child is asked in, in the coordinates the
/// widget asking draws in. /// widget asking draws in.
/// ///
/// `own` is that widget's own box, and `place` what of it the child is /// `own` is that widget's own box, and `place` what of it the child is
/// given. `narrow` is a frame the container decided for the child -- a row's /// given, including any rel base it states -- a row's slot, or padding's rel
/// slot, or padding's frame less its pixels -- and [`Part::Sized`] one a /// base less its pixels. That is a window length, like every other length
/// sibling's answer decided; both are window lengths, like every other /// here, since a slot of a row is not a fraction of anything the row can
/// length here, since a slot of a row is not a fraction of anything the row /// name. The child's declaration is a fraction of whichever reached it, and
/// can name. The child's declaration is a fraction of whichever reached it, /// is the only one that also places the box: a box the caller decided is
/// and is the only one of the three that also places the box: a box the /// what `place` names.
/// caller decided is what `place` names. pub(crate) fn rel_base_and_region(
pub(crate) fn frame_and_extent( self,
own: UiRegion, own: UiRegion,
parent_frame: UiVec2, parent_rel_base: UiVec2,
place: [Place; 2], declared: Declared,
narrow: [Option<Len>; 2], align: RegionAlign,
declared: [Option<LayoutLen>; 2], ) -> (UiVec2, UiRegion) {
align: RegionAlign, let given = self.of(own, align);
) -> (UiVec2, UiRegion) { let mut rel_base = parent_rel_base;
let part = part_of(own, place, align); let mut region = given;
let mut frame = parent_frame; for axis in AXES {
let mut extent = part; let base = self[axis]
for axis in AXES { .stated_rel_base()
let n = axis as usize; .unwrap_or_else(|| parent_rel_base[axis]);
let sized = match place[n].part() { let len = declared[axis]
Part::Sized(len) => Some(len), .map(|len| len.within_len(base))
_ => None, .unwrap_or(base);
}; rel_base[axis] = len;
let base = sized if declared[axis].is_some() {
.or(narrow[n]) region[axis] = given[axis].place(len, align[axis]);
.unwrap_or_else(|| parent_frame.axis(axis)); }
let len = declared[n]
.map(|len| Len::from_parts(len.rel, len.px).within_len(base))
.unwrap_or(base);
*frame.axis_mut(axis) = len;
if declared[n].is_some() {
let slot = part.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*extent.axis_mut(axis) = UiSpan::new(start, start + len);
} }
(rel_base, region)
} }
(frame, extent)
}
/// The part of a widget's own box a `place` names, in the coordinates that
/// box is in.
fn part_of(extent: UiRegion, place: [Place; 2], align: RegionAlign) -> UiRegion {
let mut part = extent;
for axis in AXES {
*part.axis_mut(axis) = place[axis as usize]
.part()
.of(*extent.axis(axis), align.axis(axis));
}
part
} }
+231 -53
View File
@@ -1,67 +1,243 @@
use crate::{AxisAlign, Len, PrimitiveHandle, UiRegion, UiSpan}; use crate::util::impl_axis_index;
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
/// What of a widget's own box a child is given, along one axis. /// How a child's region along one axis comes from the region of the widget
/// asking, and what its fractions are of.
///
/// The three ways of saying a region are the three the geometry already has:
/// a span composed into the caller's box, a span shifted to where that box
/// starts, and a length placed in it by alignment. Which one is meant cannot
/// be read off the numbers, since two of them take the same span and apply
/// it differently, so it is said here.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum Part { pub struct PlaceDescAxis {
/// The whole of it. span: PlaceSpan,
All, fills: bool,
/// Window lengths from where the box starts, which is what a container rel_base: RelBase,
/// dividing room among its children speaks: a child's report is a window }
/// length, so the cursor that sums those reports is one too. A moved box
/// re-places every child by re-adding its start, exactly. A fraction #[derive(Clone, Copy, Debug, PartialEq)]
/// here is a fraction of the window and not of the box -- the whole of a enum PlaceSpan {
/// box is [`Self::All`], not a `rel(1.0)` span. Within(UiSpan),
From(UiSpan), Shifted(UiSpan),
/// The box less a window length at each end, which is what a container
/// that insets one speaks -- padding, or a row asking a child in the
/// room left from its cursor. Neither end names the box's length, so a
/// container can say "from here to my end" without reading how long it
/// is, and a box chosen from its own answer does not feed back into the
/// answer.
Inset { lead: Len, trail: Len },
/// A box of this length, wherever in the parent's box the child's own
/// alignment puts it, and that same length as its frame. Unlike `From`,
/// it is a length decided from above rather than a place along a
/// container's cursor -- what a stack's sizing child decides for the
/// rest.
Sized(Len), Sized(Len),
} }
impl Part { /// What a child's fractions are of, where the caller has not named a length.
/// Where it lands in the coordinates `extent` is in.
pub(crate) fn of(self, extent: UiSpan, align: AxisAlign) -> UiSpan {
match self {
Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Inset { lead, trail } => UiSpan::new(extent.start + lead, extent.end - trail),
Self::Sized(len) => {
let start = extent.start + (extent.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
}
}
}
/// Where a child goes along one axis, as a part of this widget's box.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum Place { enum RelBase {
/// The child's answer, aligned inside the part by the child's alignment. /// The caller's own, unchanged.
Within(Part), Inherit,
/// Exactly the part; the answer is not placed inside it again. /// The caller's own, narrowed the way the region is.
Fill(Part), WithRegion,
/// This length of the window.
Len(Len),
} }
impl Place { impl PlaceDescAxis {
pub(crate) fn part(self) -> Part { /// The whole of the caller's box.
match self { pub const WHOLE: Self = UiSpan::FULL.within_desc();
Self::Within(part) | Self::Fill(part) => part,
/// This region is the child's placement: its answer is not placed inside
/// it again. A container uses it where it hands back exactly what the
/// child asked for -- a row placing a child at the length it reported.
pub const fn fills(mut self) -> Self {
self.fills = true;
self
}
/// This along `axis`, and the whole of the caller's box across it: what
/// a container dividing one axis says, since nothing divides the other.
/// [`PlaceDesc::from_axis`] says the across one where it is not the
/// whole.
pub const fn axis(self, axis: Axis) -> PlaceDesc {
PlaceDesc::from_axis(axis, self, Self::WHOLE)
}
/// What the child's fractions are of, as a length of the window: a
/// resolved share, or a box a sibling's answer decided.
pub const fn rel_base(mut self, len: Len) -> Self {
self.rel_base = RelBase::Len(len);
self
}
/// Whether the region is the placement outright, rather than a box the
/// answer is placed inside.
pub(crate) const fn does_fill(self) -> bool {
self.fills
}
/// Where it lands in the coordinates `own` is in.
pub(crate) fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan {
match self.span {
PlaceSpan::Within(span) => span.within(&own),
PlaceSpan::Shifted(mut span) => {
span.shift(own.start);
span
}
PlaceSpan::Sized(len) => own.place(len, align),
} }
} }
/// Whether the part is the drawing's box outright, rather than the box /// The child's rel base, where this says one outright. `None` forwards
/// the answer is placed inside. /// the caller's own, and [`RelBase::WithRegion`] is resolved by whoever
pub(crate) fn fills(self) -> bool { /// can read that rel base, so it does not reach here.
matches!(self, Self::Fill(_)) pub(crate) const fn stated_rel_base(self) -> Option<Len> {
match self.rel_base {
RelBase::Len(len) => Some(len),
_ => None,
}
}
/// The length this narrows the caller's rel base by, where it does.
/// `None` leaves that rel base alone, and reading it is then a
/// dependency the caller does not take.
pub(crate) const fn narrows_rel_base(self) -> Option<UiSpan> {
match (self.rel_base, self.span) {
(RelBase::WithRegion, PlaceSpan::Within(span)) => Some(span),
_ => None,
}
}
/// The span it composes into the caller's box, where that is what it
/// does: the one case whose validity maps back through the part.
pub(crate) const fn within_span(self) -> Option<UiSpan> {
match self.span {
PlaceSpan::Within(span) => Some(span),
_ => None,
}
}
/// Whether the caller decided this length rather than a place along its
/// own box, which is what stops its length reaching the child at all.
pub(crate) const fn is_sized(self) -> bool {
matches!(self.span, PlaceSpan::Sized(_))
}
/// The same, with its rel base stated outright.
pub(crate) const fn with_rel_base(mut self, len: Option<Len>) -> Self {
self.rel_base = match len {
Some(len) => RelBase::Len(len),
None => RelBase::Inherit,
};
self
}
}
/// Where a child is asked, on both axes. A [`UiRegion`] converts into the
/// common case: that box of the caller's own, the answer placed inside it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDesc {
pub x: PlaceDescAxis,
pub y: PlaceDescAxis,
}
impl PlaceDesc {
/// The whole of the caller's box, on both axes.
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
Self { x, y }
}
/// The same on both axes.
pub const fn splat(place: PlaceDescAxis) -> Self {
Self { x: place, y: place }
}
/// A description per axis, where the two differ and neither is the
/// axis a container divides.
pub fn per_axis(f: impl Fn(Axis) -> PlaceDescAxis) -> Self {
Self::new(f(Axis::X), f(Axis::Y))
}
/// `aligned` on `axis` and `ortho` on the other, which is how a
/// container that divides one axis says what it is doing.
pub const fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
Axis::Y => Self::new(ortho, aligned),
}
}
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
pub const fn fills(self) -> Self {
Self::new(self.x.fills(), self.y.fills())
}
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
self[axis] = self[axis].rel_base(len);
self
}
/// The box each axis names, in the coordinates `own` is in.
pub(crate) fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
}
}
impl UiSpan {
/// This span composed into the caller's own box, so it moves and scales
/// with it: [`UiSpan::within`], which is what a container that insets
/// one speaks. Taking eleven pixels off the end needs no length, where
/// saying the same thing in window lengths would make the container read
/// its own box -- and a box chosen from its own answer then feeds back
/// into the answer.
///
/// The child's rel base is narrowed the same way, so padding takes its
/// pixels off both and `rel(1)` under it fills the caller rather than
/// overflowing it.
pub const fn within_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Within(self),
fills: false,
rel_base: RelBase::WithRegion,
}
}
/// This span shifted to where the caller's own box starts: window
/// lengths along a cursor, which is what a container dividing room among
/// its children speaks. A child's report is a window length, so the
/// cursor that sums those reports is one too, and a moved box re-places
/// every child by re-adding its start, exactly.
///
/// The child's rel base passes through: how far along the cursor a child
/// sits says nothing about what a fraction under it is of. The same span
/// says [`Self::within_desc`] as a part of that box instead, and which is
/// meant cannot be read off the numbers.
pub const fn shifted_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Shifted(self),
fills: false,
rel_base: RelBase::Inherit,
}
}
}
impl Len {
/// A box this long, placed in the caller's own by the child's alignment:
/// the rule that places an answer, with the length given from above
/// rather than reported. What a stack's sizing child decides for the
/// rest. It is the child's rel base too.
pub const fn as_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Sized(self),
fills: false,
rel_base: RelBase::Len(self),
}
}
}
impl From<UiRegion> for PlaceDesc {
fn from(region: UiRegion) -> Self {
Self::new(region.x.within_desc(), region.y.within_desc())
}
}
impl From<PlaceDescAxis> for PlaceDesc {
fn from(place: PlaceDescAxis) -> Self {
Self::splat(place)
} }
} }
@@ -72,3 +248,5 @@ pub struct RetainedPrimitive {
pub handle: PrimitiveHandle, pub handle: PrimitiveHandle,
pub region: UiRegion, pub region: UiRegion,
} }
impl_axis_index!(PlaceDesc => PlaceDescAxis);
+193 -215
View File
@@ -1,9 +1,8 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_lens, frame_and_extent, placed_extent};
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, ActiveData, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
Moves, Painter, Part, PixelRegion, Place, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc,
UiSpan, UiVec2, Weight, WidgetId, Widgets, UiSpan, UiVec2, Weight, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -22,38 +21,27 @@ pub(super) struct DrawInfo {
pub mask: MaskIdx, pub mask: MaskIdx,
/// What a fraction declared or reported under this widget is a fraction /// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window. /// of, as a length of the window.
pub frame: UiVec2, pub rel_base: UiVec2,
/// The box the widget is asked in, in its parent region node's /// The box the widget is asked in, in its parent region node's
/// coordinates. /// coordinates.
pub part: UiRegion, pub region: UiRegion,
/// Where the widget is put, and where it was asked, as parts of the /// Where the widget is put, and where it was asked, as parts of the
/// parent's box. See [`Place`]. The two are one ask's place until the /// parent's box. See [`PlaceDesc`]. The two are one ask's place until the
/// parent puts the answer somewhere else. /// parent puts the answer somewhere else.
pub placed: [Place; 2], pub placed: PlaceDesc,
pub asked: [Place; 2], pub asked: PlaceDesc,
/// A frame the parent decided for it on each axis, as a length of the
/// window, which the widget's own declaration is a fraction of.
pub narrow: [Option<Len>; 2],
/// Whether the parent already asked about this widget in this draw. /// Whether the parent already asked about this widget in this draw.
pub re_asked: bool, pub re_asked: bool,
/// The frame in pixels, resolved once against the window. /// The rel base in pixels, resolved once against the window.
pub px: PxVec2, pub px: PxVec2,
} }
impl DrawInfo {
/// The axes where the part is the drawing's box outright, which are the
/// axes the answer is not placed inside it again.
fn fill(&self) -> [bool; 2] {
self.placed.map(Place::fills)
}
}
/// What a widget's children are placed in: its own box, the coordinates its /// What a widget's children are placed in: its own box, the coordinates its
/// drawing is in, and what else one ask of a child is decided from. /// drawing is in, and what else one ask of a child is decided from.
pub(super) struct Placing { pub(super) struct Placing {
pub id: WidgetId, pub id: WidgetId,
pub extent: UiRegion, pub region: UiRegion,
pub frame: UiVec2, pub rel_base: UiVec2,
pub window: PxVec2, pub window: PxVec2,
pub depth: usize, pub depth: usize,
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
@@ -120,13 +108,13 @@ impl UiRenderState {
let Some(root) = self.old_root else { return }; let Some(root) = self.old_root else { return };
let stands = self.active.get(&root).is_some_and(|active| { let stands = self.active.get(&root).is_some_and(|active| {
// Nothing above the root chose anything, so the box it was first // Nothing above the root chose anything, so the box it was first
// asked about is the whole of its frame. Both its answer and its // asked about is the whole of its rel base. Both its answer and its
// drawing have to stand in the new window, since nothing above // drawing have to stand in the new window, since nothing above
// it will ask either again. // it will ask either again.
let answer = active let answer = active
.answer .answer
.is_some_and(|(_, holds)| holds.contains(size, active.frame, active.part)); .is_some_and(|(_, holds)| holds.contains(size, active.rel_base, active.region));
answer && active.holds.contains(size, active.frame, active.part) answer && active.holds.contains(size, active.rel_base, active.region)
}); });
if !stands { if !stands {
widgets.needs_redraw.insert(root); widgets.needs_redraw.insert(root);
@@ -134,9 +122,9 @@ impl UiRenderState {
} }
/// The root is asked about in the output. Its own rules narrow both its /// The root is asked about in the output. Its own rules narrow both its
/// frame and box; nothing above it chose a different one. /// rel base and box; nothing above it chose a different one.
fn root_info(&self, frame: UiVec2, extent: UiRegion) -> DrawInfo { fn root_info(&self, rel_base: UiVec2, region: UiRegion) -> DrawInfo {
let px = frame.to_px(self.output_size); let px = rel_base.to_px(self.output_size);
DrawInfo { DrawInfo {
layer: 0, layer: 0,
parent: None, parent: None,
@@ -144,11 +132,10 @@ impl UiRenderState {
parent_move: MoveIdx::NONE, parent_move: MoveIdx::NONE,
region_node: false, region_node: false,
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
frame, rel_base,
part: extent, region,
placed: [Place::Within(Part::All); 2], placed: PlaceDesc::WHOLE,
asked: [Place::Within(Part::All); 2], asked: PlaceDesc::WHOLE,
narrow: [None; 2],
re_asked: false, re_asked: false,
px, px,
} }
@@ -196,22 +183,20 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout); let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc); self.clear(rsc);
if let Some(id) = root { if let Some(id) = root {
let (frame, extent) = Self::root_layout(id.id(), rsc.widgets()); let (rel_base, region) = Self::root_layout(id.id(), rsc.widgets());
let info = self.root_info(frame, extent); let info = self.root_info(rel_base, region);
self.draw_inner(id.id(), info, None, rsc); self.draw_inner(id.id(), info, None, rsc);
} }
} }
/// The root's frame and box: the window, taken in by the root's own /// The root's rel base and box: the window, taken in by the root's own
/// rules. Nothing above it narrowed anything or chose where it goes, so /// rules. Nothing above it narrowed anything or chose where it goes, so
/// its declaration is the whole of what decides either. /// its declaration is the whole of what decides either.
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) { fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
frame_and_extent( PlaceDesc::WHOLE.rel_base_and_region(
UiRegion::FULL, UiRegion::FULL,
UiVec2::FULL_SIZE, UiVec2::FULL_SIZE,
[Place::Within(Part::All); 2], widgets.declared_lens(id),
[None; 2],
declared_lens(widgets, id),
widgets.alignment(id), widgets.alignment(id),
) )
} }
@@ -223,14 +208,18 @@ impl UiRenderState {
mut old: Option<ActiveData>, mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds, LayoutHolds) { ) -> (Size, LayoutHolds, LayoutHolds) {
let part = info.part; let old_parent = old
.as_ref()
.or_else(|| self.active.get(&id))
.and_then(|a| a.parent);
let region = info.region;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::DrawRequests); diag::bump(Counter::DrawRequests);
diag::draw_request(id, info.parent, part, info.px, info.region_node); diag::draw_request(id, info.parent, region, info.px, info.region_node);
} }
let align = rsc.widgets().alignment(id); let align = rsc.widgets().alignment(id);
let declared = declared_lens(rsc.widgets(), id); let declared = rsc.widgets().declared_lens(id);
// Nothing this widget measured can be dirty while it draws: layout is // Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to // one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked. // its own parent, and a deferred one leaves that parent marked.
@@ -241,46 +230,43 @@ impl UiRenderState {
// an answer is kept only with the drawing that gave it, and both // an answer is kept only with the drawing that gave it, and both
// have to hold for the box asked about. // have to hold for the box asked about.
let reused = (!stale) let reused = (!stale)
.then(|| self.retained_answer(id, part, info)) .then(|| self.retained_answer(id, region, info))
.flatten() .flatten()
.and_then(|answer| { .and_then(|answer| {
let extent = placed_extent(part, answer.0, declared, info.fill(), align); let placed = info.placed.placement(region, answer.0, declared, align);
self.try_reuse(id, part, extent, info, rsc).map(|()| answer) self.try_reuse(id, region, placed, info, rsc)
.map(|()| answer)
}); });
let answer = reused.unwrap_or_else(|| { let answer = reused.unwrap_or_else(|| {
if old.is_none() { if old.is_none() {
old = self.remove(id, false, rsc); old = self.remove(id, false, rsc);
} }
let answer = self.draw_at(id, part, info, old.take(), rsc); let answer = self.draw_at(id, region, info, old.take(), rsc);
// Where the drawing goes: the part its parent gave it, with the // Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left // answer placed inside that part on any axis the parent left
// open. // open.
let extent = placed_extent(part, answer.0, declared, info.fill(), align); let placed = info.placed.placement(region, answer.0, declared, align);
if extent != part { if placed != region {
self.relocate(id, extent, info, rsc); self.relocate(id, placed, info, rsc);
} }
answer answer
}); });
let drawing_holds = self.active[&id].holds; let drawing_holds = self.active[&id].holds;
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
// Whoever asked owns how the boxes were reached: the frame it stated, // Whoever asked owns how the boxes were reached: the rel base it stated,
// and what of its own box it asked in. A local redraw asks the same // and what of its own box it asked in. A local redraw asks the same
// question again from these. // question again from these.
active.frame = info.frame; active.rel_base = info.rel_base;
active.narrow = info.narrow;
active.re_asked = info.re_asked; active.re_asked = info.re_asked;
active.answer = Some(answer); active.answer = Some(answer);
active.asked = info.asked; active.asked = info.asked;
active.part = part; active.region = region;
active.placed = info.placed; active.placed = info.placed;
active.own_align = align; active.own_align = align;
// A subtree can be reused whole under a different parent -- same box, // The previous parent must stop owning the subtree before it can
// same layer, same region node -- and nothing in the drawing says it // undraw it, whether changing hands reused the drawing or replaced it.
// changed hands. Two things read who its parent is: a deferral, which active.parent = info.parent;
// marks whoever has it to draw, and the old parent's list of children,
// which its next draw undraws whatever is missing from.
let old_parent = std::mem::replace(&mut active.parent, info.parent);
if old_parent != info.parent if old_parent != info.parent
&& let Some(old_parent) = old_parent && let Some(old_parent) = old_parent
&& let Some(old_parent) = self.active.get_mut(&old_parent) && let Some(old_parent) = self.active.get_mut(&old_parent)
@@ -290,28 +276,31 @@ impl UiRenderState {
(answer.0, answer.1, drawing_holds) (answer.0, answer.1, drawing_holds)
} }
/// Calls a widget's `draw` and keeps what it drew in `extent` of `frame`. /// Calls a widget's `draw` and keeps what it drew in `region`.
fn draw_at( fn draw_at(
&mut self, &mut self,
id: WidgetId, id: WidgetId,
extent: UiRegion, region: UiRegion,
info: DrawInfo, info: DrawInfo,
old: Option<ActiveData>, old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) { ) -> (Size, LayoutHolds) {
let frame = info.frame; let rel_base = info.rel_base;
let (move_idx, extent, retired_move) = match info.region_node { let (move_idx, region, retired_move) = match info.region_node {
// A node entry is only a translation. Its local box keeps the // A node entry is only a translation. Its local box keeps the
// same window-unit length as the box in its parent's node. // same window-unit length as the box in its parent's node.
true => ( true => (
self.move_slot(id, info.parent_move, translation(extent)), self.move_slot(id, info.parent_move, region.as_translation()),
local_region(extent), region.at_origin(),
None, None,
), ),
// Keep the old entry alive until every descendant has migrated. // Keep the old entry alive until every descendant has migrated.
// Reusing its index sooner could make an old parent look current. // Reusing its index sooner could make an old parent look current.
false => (info.parent_move, extent, self.slots.remove(&id)), false => (info.parent_move, region, self.slots.remove(&id)),
}; };
let mask_slot = old
.as_ref()
.and_then(|old| old.mask_region.map(|_| old.mask));
let old_children = old.map_or_else(Vec::new, |old| old.children); let old_children = old.map_or_else(Vec::new, |old| old.children);
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
let px = info.px; let px = info.px;
@@ -319,9 +308,8 @@ impl UiRenderState {
let window = self.output_size; let window = self.output_size;
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
frame, rel_base,
extent, region,
extent_len: [None; 2],
window, window,
mask: info.mask, mask: info.mask,
layer: info.layer, layer: info.layer,
@@ -330,12 +318,11 @@ impl UiRenderState {
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None, mask_region: None,
mask_slot,
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
window_own: [Holds::ANY; 2], own: LayoutHolds::ANY,
frame_own_len: [None; 2],
under: Vec::new(), under: Vec::new(),
extent_own: [Holds::ANY; 2],
answer_under: LayoutHolds::ANY, answer_under: LayoutHolds::ANY,
depth: info.depth, depth: info.depth,
move_idx, move_idx,
@@ -356,20 +343,18 @@ impl UiRenderState {
let Painter { let Painter {
state: _, state: _,
rsc: _, rsc: _,
frame: _, rel_base: _,
extent: _, region: _,
window: _, window: _,
mask, mask,
textures, textures,
primitives, primitives,
mask_region, mask_region,
extent_own, mask_slot,
extent_len, own,
answer_under, answer_under,
children, children,
size_deps, size_deps,
window_own,
frame_own_len,
under, under,
move_idx, move_idx,
layer, layer,
@@ -386,18 +371,18 @@ impl UiRenderState {
// A rule wins on the axis it names, and the draw answers the rest. // A rule wins on the axis it names, and the draw answers the rest.
// Applied here so it is one place rather than every widget that could // Applied here so it is one place rather than every widget that could
// carry one, and so the widget under a rule never learns of it. The // carry one, and so the widget under a rule never learns of it. The
// frame is the answer where the rule gave a length outright: it was // rel base is the answer where the rule gave a length outright: it was
// resolved into the frame when the child was asked, and resolving it // resolved into the rel base when the child was asked, and resolving it
// again here would take the fraction of a fraction. // again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id); let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules.axis(axis).exact() { let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
None => reported, None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen { Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.frame.axis(axis).rel, rel: info.rel_base[axis].rel,
px: info.frame.axis(axis).px, px: info.rel_base[axis].px,
leftover: Weight::ZERO, leftover: Weight::ZERO,
}, },
Some(len) => len.within_len(info.frame.axis(axis)), Some(len) => len.within_len(info.rel_base[axis]),
}; };
let size = Size { let size = Size {
x: ruled(Axis::X, size.x), x: ruled(Axis::X, size.x),
@@ -412,7 +397,7 @@ impl UiRenderState {
mask == info.mask mask == info.mask
|| AXES || AXES
.into_iter() .into_iter()
.all(|axis| within_box(size, extent, self.output_size, axis)), .all(|axis| size.within_box(region, self.output_size, axis)),
"'{}' ({id:?}) clips to {px:?} and reports {size}", "'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id), rsc.widgets().label(id),
); );
@@ -421,35 +406,29 @@ impl UiRenderState {
self.undraw_rec(*c, rsc); self.undraw_rec(*c, rsc);
} }
} }
if let Some(idx) = mask_slot {
rsc.ui_mut().masks.remove(idx);
}
if let Some(idx) = retired_move { if let Some(idx) = retired_move {
self.moves.remove(idx); self.moves.remove(idx);
} }
// A rule that is a fraction of the frame is answered with the // A rule that is a fraction of the rel base is answered with the
// frame's own length, so the answer is that frame's and not just // rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read // that many pixels of this window -- the same pin a widget that read
// its frame took for its drawing. // its rel base took for its drawing.
let frame_len = AXES.map(|axis| { let mut own_holds = own;
let fraction = rules for axis in AXES {
.axis(axis) let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
.exact() if fraction {
.is_some_and(|len| len.rel != Rel::ZERO); own_holds[axis].rel_base = Some(info.rel_base[axis]);
match fraction {
true => Some(info.frame.axis(axis)),
false => frame_own_len[axis as usize],
} }
}); }
let own_holds = LayoutHolds {
window: window_own,
frame_len,
extent: extent_own,
extent_len,
};
let answer_holds = own_holds.and(answer_under); let answer_holds = own_holds.and(answer_under);
let holds = under let holds = under
.into_iter() .into_iter()
.fold(answer_holds, |holds, (_, child)| holds.and(child)); .fold(answer_holds, |holds, (_, child)| holds.and(child));
debug_assert!( debug_assert!(
holds.contains(self.output_size, info.frame, extent), holds.contains(self.output_size, info.rel_base, region),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
rsc.widgets().label(id), rsc.widgets().label(id),
); );
@@ -467,11 +446,10 @@ impl UiRenderState {
parent_move: move_idx, parent_move: move_idx,
region_node: false, region_node: false,
mask, mask,
frame: UiVec2::FULL_SIZE, rel_base: UiVec2::FULL_SIZE,
part: UiRegion::FULL, region: UiRegion::FULL,
placed: [Place::Within(Part::All); 2], placed: PlaceDesc::WHOLE,
asked: [Place::Within(Part::All); 2], asked: PlaceDesc::WHOLE,
narrow: [None; 2],
re_asked: false, re_asked: false,
px, px,
}, },
@@ -483,12 +461,11 @@ impl UiRenderState {
let active = ActiveData { let active = ActiveData {
id, id,
extent, placement: region,
frame: info.frame, rel_base: info.rel_base,
narrow: info.narrow,
placed: info.placed, placed: info.placed,
asked: info.asked, asked: info.asked,
part: extent, region,
// Whoever asked writes the answer. // Whoever asked writes the answer.
answer: None, answer: None,
re_asked: info.re_asked, re_asked: info.re_asked,
@@ -502,7 +479,7 @@ impl UiRenderState {
mask_region, mask_region,
children, children,
size_deps, size_deps,
declared: declared_lens(rsc.widgets(), id), declared: rsc.widgets().declared_lens(id),
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
move_idx, move_idx,
parent_move: info.parent_move, parent_move: info.parent_move,
@@ -542,7 +519,7 @@ impl UiRenderState {
fn retained_answer( fn retained_answer(
&self, &self,
id: WidgetId, id: WidgetId,
part: UiRegion, region: UiRegion,
info: DrawInfo, info: DrawInfo,
) -> Option<(Size, LayoutHolds)> { ) -> Option<(Size, LayoutHolds)> {
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
@@ -556,17 +533,17 @@ impl UiRenderState {
let answer = active.answer?; let answer = active.answer?;
answer answer
.1 .1
.contains(self.output_size, info.frame, part) .contains(self.output_size, info.rel_base, region)
.then_some(answer) .then_some(answer)
} }
/// Keeps the retained drawing if its contract holds for `part`, the box /// Keeps the retained drawing if its contract holds for `part`, the box
/// asked about, and puts it at `extent`, where the answer places it. /// asked about, and puts it at `placed`, where the answer places it.
fn try_reuse( fn try_reuse(
&mut self, &mut self,
id: WidgetId, id: WidgetId,
part: UiRegion, region: UiRegion,
extent: UiRegion, placed: UiRegion,
info: DrawInfo, info: DrawInfo,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Option<()> { ) -> Option<()> {
@@ -605,6 +582,9 @@ impl UiRenderState {
} }
return None; return None;
} }
if active.parent_mask != info.mask {
return None;
}
// Drawn somewhere else in the tree: its box is in coordinates it no // Drawn somewhere else in the tree: its box is in coordinates it no
// longer sits in, and its slot names the wrong parent. // longer sits in, and its slot names the wrong parent.
if active.parent_move != info.parent_move { if active.parent_move != info.parent_move {
@@ -618,27 +598,31 @@ impl UiRenderState {
// In pixels, because the box is a fraction of the window and that // In pixels, because the box is a fraction of the window and that
// may be what changed -- an unchanged fraction of a window half the // may be what changed -- an unchanged fraction of a window half the
// size is half the widget. // size is half the widget.
if !active.holds.contains(self.output_size, info.frame, part) { if !active
.holds
.contains(self.output_size, info.rel_base, region)
{
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
// Which of the three said no, so a frame that redraws more // Which of the three said no, so a rel base that redraws more
// than it should says where to look. They overlap: a drawing // than it should says where to look. They overlap: a drawing
// can be outside two of them at once. // can be outside two of them at once.
let holds = active.holds;
for axis in AXES { for axis in AXES {
let n = axis as usize; let holds = active.holds[axis];
if holds.extent_len[n].is_some_and(|pinned| pinned != part.axis(axis).len()) { let len = region[axis].len();
let window = self.output_size[axis];
if holds.region_len.is_some_and(|pinned| pinned != len) {
diag::bump(Counter::OutsidePinnedLen); diag::bump(Counter::OutsidePinnedLen);
} }
if !holds.window[n].contains(self.output_size.axis(axis)) if !holds.window.contains(window)
|| holds.frame_len[n].is_some_and(|pinned| pinned != info.frame.axis(axis)) || holds
.rel_base
.is_some_and(|pinned| pinned != info.rel_base[axis])
{ {
diag::bump(Counter::OutsideFrame); diag::bump(Counter::OutsideRelBase);
} }
if !holds.extent[n] if !holds.region.contains(len.to_px(window)) {
.contains(part.axis(axis).len().to_px(self.output_size.axis(axis))) diag::bump(Counter::OutsideRegion);
{
diag::bump(Counter::OutsideExtent);
} }
} }
diag::bump(Counter::ReuseOutside); diag::bump(Counter::ReuseOutside);
@@ -646,14 +630,14 @@ impl UiRenderState {
} }
return None; return None;
} }
self.relocate(id, extent, info, rsc); self.relocate(id, placed, info, rsc);
Some(()) Some(())
} }
/// Puts a retained drawing where its parent now has it, without drawing: /// Puts a retained drawing where its parent now has it, without drawing:
/// a widget with a node of its own writes that node's translation, and /// a widget with a node of its own writes that node's translation, and
/// one without re-expresses its own drawing and everything inside it. /// one without re-expresses its own drawing and everything inside it.
fn relocate(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { fn relocate(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
let active = &self.active[&id]; let active = &self.active[&id];
debug_assert!( debug_assert!(
!rsc.widgets().needs_redraw.contains(&id), !rsc.widgets().needs_redraw.contains(&id),
@@ -662,20 +646,20 @@ impl UiRenderState {
); );
let has_region_node = active.move_idx != active.parent_move; let has_region_node = active.move_idx != active.parent_move;
let local = match has_region_node { let local = match has_region_node {
true => local_region(extent), true => placed.at_origin(),
false => extent, false => placed,
}; };
let moved = active.extent != local; let moved = active.placement != local;
let slot = active.move_idx; let slot = active.move_idx;
if has_region_node { if has_region_node {
self.moves.set(slot, translation(extent)); self.moves.set(slot, placed.as_translation());
} }
if moved { if moved {
self.reposition(id, local, info, rsc); self.reposition(id, local, info, rsc);
} }
self.redepth(id, info.depth); self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.frame = info.frame; active.rel_base = info.rel_base;
active.placed = info.placed; active.placed = info.placed;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
@@ -712,16 +696,15 @@ impl UiRenderState {
&mut self, &mut self,
child: WidgetId, child: WidgetId,
at: &Placing, at: &Placing,
place: [Place; 2], place: PlaceDesc,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) { ) {
let active = &self.active[&child]; let active = &self.active[&child];
let (frame, part) = Self::ask_again(active, at, place); let (rel_base, region) = Self::ask_again(active, at, place);
let extent = placed_extent( let placed = place.placement(
part, region,
active.measured().unwrap_or(active.size), active.measured().unwrap_or(active.size),
active.declared, active.declared,
place.map(Place::fills),
active.own_align, active.own_align,
); );
let info = DrawInfo { let info = DrawInfo {
@@ -731,50 +714,42 @@ impl UiRenderState {
parent_move: at.move_idx, parent_move: at.move_idx,
region_node: active.move_idx != active.parent_move, region_node: active.move_idx != active.parent_move,
mask: at.mask, mask: at.mask,
frame, rel_base,
part, region,
placed: place, placed: place,
asked: active.asked, asked: active.asked,
narrow: active.narrow,
re_asked: active.re_asked, re_asked: active.re_asked,
px: frame.to_px(at.window), px: rel_base.to_px(at.window),
}; };
self.relocate(child, extent, info, rsc); self.relocate(child, placed, info, rsc);
} }
/// The frame and the box a widget already drawn is given at `place` of /// The rel base and the box a widget already drawn is given at `place` of
/// the box its parent is being taken as. What narrowed its frame and what /// the box its parent is being taken as. What narrowed its rel base and what
/// it declared are its own record's, so both are resolved against that /// it declared are its own record's, so both are resolved against that
/// parent's frame again exactly as the first ask resolved them. /// parent's rel base again exactly as the first ask resolved them.
fn ask_again(active: &ActiveData, at: &Placing, place: [Place; 2]) -> (UiVec2, UiRegion) { fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
frame_and_extent( place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
at.extent,
at.frame,
place,
active.narrow,
active.declared,
active.own_align,
)
} }
/// Re-places everything inside a widget whose own box moved. Every child /// Re-places everything inside a widget whose own box moved. Every child
/// is placed as a part of that box, so each one's new box is its retained /// is placed as a part of that box, so each one's new box is its retained
/// part re-added to the new start -- and a child whose own box then did /// part re-added to the new start -- and a child whose own box then did
/// not change is not touched at all. /// not change is not touched at all.
fn reposition(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { fn reposition(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.extent = extent; active.placement = placed;
for primitive in &active.primitives { for primitive in &active.primitives {
let handle = &primitive.handle; let handle = &primitive.handle;
*self.layers[handle.layer].region_mut(handle) = primitive.region.within(&extent); *self.layers[handle.layer].region_mut(handle) = primitive.region.within(&placed);
} }
if let Some(mask_region) = active.mask_region { if let Some(mask_region) = active.mask_region {
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&extent); rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed);
} }
let at = Placing { let at = Placing {
id, id,
extent, region: placed,
frame: info.frame, rel_base: info.rel_base,
window: self.output_size, window: self.output_size,
depth: info.depth, depth: info.depth,
move_idx: active.move_idx, move_idx: active.move_idx,
@@ -809,11 +784,8 @@ impl UiRenderState {
let Some(widget) = rsc.widgets().get_dyn(id) else { let Some(widget) = rsc.widgets().get_dyn(id) else {
return true; return true;
}; };
AXES.into_iter().all(|axis| { AXES.into_iter()
widget .all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis]))
.size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis))
})
} }
/// Takes a widget's record out and frees what it drew. /// Takes a widget's record out and frees what it drew.
@@ -826,6 +798,9 @@ impl UiRenderState {
rsc.ui_mut().masks.remove(mask); rsc.ui_mut().masks.remove(mask);
} }
} }
if undraw && active.mask_region.take().is_some() {
rsc.ui_mut().masks.remove(active.mask);
}
active.primitives.clear(); active.primitives.clear();
active.textures.clear(); active.textures.clear();
rsc.ui_mut().textures.free(); rsc.ui_mut().textures.free();
@@ -875,12 +850,11 @@ impl UiRenderState {
id, id,
ActiveData { ActiveData {
id, id,
extent: UiRegion::FULL, placement: UiRegion::FULL,
frame: UiVec2::FULL_SIZE, rel_base: UiVec2::FULL_SIZE,
narrow: [None; 2], placed: PlaceDesc::WHOLE,
placed: [Place::Within(Part::All); 2], asked: PlaceDesc::WHOLE,
asked: [Place::Within(Part::All); 2], region: UiRegion::FULL,
part: UiRegion::FULL,
answer: None, answer: None,
re_asked: false, re_asked: false,
size, size,
@@ -894,7 +868,7 @@ impl UiRenderState {
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
move_idx: info.parent_move, move_idx: info.parent_move,
declared: [None; 2], declared: Declared::NONE,
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
parent_move: info.parent_move, parent_move: info.parent_move,
mask: info.mask, mask: info.mask,
@@ -913,6 +887,7 @@ impl UiRenderState {
self.slots.clear(); self.slots.clear();
self.moves.clear(); self.moves.clear();
self.layers.clear(); self.layers.clear();
rsc.ui_mut().masks = Default::default();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
self.free(rsc); self.free(rsc);
} }
@@ -1064,7 +1039,7 @@ impl UiRenderState {
let active = self.active.get(&id.id())?; let active = self.active.get(&id.id())?;
active.drawn.then(|| { active.drawn.then(|| {
self.moves self.moves
.resolve(active.move_idx, active.extent) .resolve(active.move_idx, active.placement)
.to_px(self.output_size) .to_px(self.output_size)
}) })
} }
@@ -1083,7 +1058,7 @@ impl UiRenderState {
// to draw -- with the mark left on, so the parent draws it rather // to draw -- with the mark left on, so the parent draws it rather
// than keeping it. So is a widget the parent asked twice: its // than keeping it. So is a widget the parent asked twice: its
// layout rests on an answer this widget cannot give again alone. // layout rests on an answer this widget cannot give again alone.
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared; let declared_changed = rsc.widgets().declared_lens(id) != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align; let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent if let Some(parent) = active.parent
&& (declared_changed && (declared_changed
@@ -1106,10 +1081,10 @@ impl UiRenderState {
// box is its own to work out again against the output. Every other // box is its own to work out again against the output. Every other
// widget was given one. // widget was given one.
let Some(parent) = active.parent else { let Some(parent) = active.parent else {
let (frame, extent) = Self::root_layout(id, rsc.widgets()); let (rel_base, region) = Self::root_layout(id, rsc.widgets());
let info = DrawInfo { let info = DrawInfo {
mask: active.parent_mask, mask: active.parent_mask,
..self.root_info(frame, extent) ..self.root_info(rel_base, region)
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
@@ -1123,8 +1098,8 @@ impl UiRenderState {
// draw ran in and what its children's parts are of. Where the // draw ran in and what its children's parts are of. Where the
// parent's answer put its own drawing is not a question anybody // parent's answer put its own drawing is not a question anybody
// asked, and nothing is asked in it here either. // asked, and nothing is asked in it here either.
let parent_at = self.placing_of(parent, self.active[&parent].part); let parent_at = self.placing_of(parent, self.active[&parent].region);
let (frame, part) = Self::ask_again(active, &parent_at, active.asked); let (rel_base, region) = Self::ask_again(active, &parent_at, active.asked);
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: active.parent, parent: active.parent,
@@ -1132,13 +1107,12 @@ impl UiRenderState {
parent_move: active.parent_move, parent_move: active.parent_move,
region_node: rsc.widgets().is_region_node(id), region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask, mask: active.parent_mask,
frame, rel_base,
part, region,
placed: active.asked, placed: active.asked,
asked: active.asked, asked: active.asked,
narrow: active.narrow,
re_asked: false, re_asked: false,
px: frame.to_px(self.output_size), px: rel_base.to_px(self.output_size),
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
@@ -1155,7 +1129,7 @@ impl UiRenderState {
active.answer = was_answer; active.answer = was_answer;
} }
if active.holds.covers(was_holds) if active.holds.covers(was_holds)
&& was_holds.contains(self.output_size, active.frame, active.extent) && was_holds.contains(self.output_size, active.rel_base, active.placement)
{ {
active.holds = was_holds; active.holds = was_holds;
} }
@@ -1172,21 +1146,21 @@ impl UiRenderState {
// The answer stands, so where the parent put it stands: the // The answer stands, so where the parent put it stands: the
// fresh drawing goes back there -- the same place, of the box // fresh drawing goes back there -- the same place, of the box
// the parent's answer chose rather than the one it was asked in. // the parent's answer chose rather than the one it was asked in.
let at = self.placing_of(parent, self.active[&parent].extent); let at = self.placing_of(parent, self.active[&parent].placement);
self.place_in(id, &at, was_place, rsc); self.place_in(id, &at, was_place, rsc);
} }
true true
} }
/// A drawn widget as the thing its children are placed within, with /// A drawn widget as the thing its children are placed within, with
/// `extent` as the box their parts are of: the box it was asked in for /// `region` as the box their parts are of: the box it was asked in for
/// asking one of them again, the box its answer chose for placing one. /// asking one of them again, the box its answer chose for placing one.
fn placing_of(&self, id: WidgetId, extent: UiRegion) -> Placing { fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing {
let active = &self.active[&id]; let active = &self.active[&id];
Placing { Placing {
id, id,
extent, region,
frame: active.frame, rel_base: active.rel_base,
window: self.output_size, window: self.output_size,
depth: active.depth, depth: active.depth,
move_idx: active.move_idx, move_idx: active.move_idx,
@@ -1199,29 +1173,33 @@ impl UiRenderState {
/// Both are lengths of the window, so the comparison is in its pixels. A /// Both are lengths of the window, so the comparison is in its pixels. A
/// share is a length only to whoever divides one, so it is not a claim about /// share is a length only to whoever divides one, so it is not a claim about
/// this box and cannot exceed it. /// this box and cannot exceed it.
fn within_box(size: Size, extent: UiRegion, window: PxVec2, axis: Axis) -> bool { impl Size {
let len = size.axis(axis); fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let window = window.axis(axis); let len = self[axis];
len.leftover != Weight::ZERO let window = window[axis];
|| Len::from_parts(len.rel, len.px).to_px(window) <= extent.axis(axis).len().to_px(window) len.leftover != Weight::ZERO
|| len.without_leftover().to_px(window) <= region[axis].len().to_px(window)
}
} }
/// A box in a fresh region node keeps its window-unit length and starts at impl UiRegion {
/// that node's origin. /// A box in a fresh region node keeps its window-unit length and starts
fn local_region(region: UiRegion) -> UiRegion { /// at that node's origin.
let size = region.size(); fn at_origin(self) -> UiRegion {
UiRegion::new( let size = self.size();
UiSpan::new(Len::ZERO, size.x), UiRegion::new(
UiSpan::new(Len::ZERO, size.y), UiSpan::new(Len::ZERO, size.x),
) UiSpan::new(Len::ZERO, size.y),
} )
}
/// A region node changes only the origin. A full relative span anchored at /// A region node changes only the origin. A full relative span anchored at
/// the box start composes as that translation in both the CPU and shader. /// the box start composes as that translation in both the CPU and shader.
fn translation(region: UiRegion) -> UiRegion { fn as_translation(self) -> UiRegion {
UiRegion { UiRegion {
x: UiSpan::new(region.x.start, region.x.start + Len::FULL), x: UiSpan::new(self.x.start, self.x.start + Len::FULL),
y: UiSpan::new(region.y.start, region.y.start + Len::FULL), y: UiSpan::new(self.y.start, self.y.start + Len::FULL),
}
} }
} }
+28
View File
@@ -93,3 +93,31 @@ macro_rules! impl_op {
} }
pub(crate) use impl_op; pub(crate) use impl_op;
/// `Index<Axis>` for a pair, which is how every pair here is read by axis.
/// The generics clause is given in braces where the type has one.
macro_rules! impl_axis_index {
($({$($gen:tt)*})? $T:ty => $Out:ty) => {
const impl $(<$($gen)*>)? std::ops::Index<crate::Axis> for $T {
type Output = $Out;
fn index(&self, axis: crate::Axis) -> &$Out {
match axis {
crate::Axis::X => &self.x,
crate::Axis::Y => &self.y,
}
}
}
const impl $(<$($gen)*>)? std::ops::IndexMut<crate::Axis> for $T {
fn index_mut(&mut self, axis: crate::Axis) -> &mut $Out {
match axis {
crate::Axis::X => &mut self.x,
crate::Axis::Y => &mut self.y,
}
}
}
};
}
pub(crate) use impl_axis_index;
+27 -20
View File
@@ -1,4 +1,5 @@
use crate::{Axis, LayoutLen, Weight}; use crate::util::impl_axis_index;
use crate::{Axis, LayoutLen, Len};
/// What a widget's length on one axis is, as a rule its parent applies where /// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself. /// it draws it rather than an answer the widget gives about itself.
@@ -19,14 +20,9 @@ pub enum SizeRule {
impl SizeRule { impl SizeRule {
/// The length this rule gives without the widget being drawn, if it can /// The length this rule gives without the widget being drawn, if it can
/// give one. `leftover` is never among them: a share is a length only to /// give one.
/// whoever divides one, so it passes up in the reported size instead and pub fn declared(&self) -> Option<Len> {
/// is resolved there. self.exact().and_then(LayoutLen::declared)
pub fn declared(&self) -> Option<LayoutLen> {
match self {
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
_ => None,
}
} }
/// The length this rule gives outright, whatever the widget reports -- /// The length this rule gives outright, whatever the widget reports --
@@ -70,18 +66,29 @@ pub struct SizeRules {
pub y: SizeRule, pub y: SizeRule,
} }
impl SizeRules { impl_axis_index!(SizeRules => SizeRule);
pub fn axis(&self, axis: Axis) -> SizeRule {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule { /// What a widget's box is on each axis where something says so outright,
match axis { /// before it is drawn: a rule beside it, or a hint it gives about itself.
Axis::X => &mut self.x, /// Whoever draws the widget resolves these against its rel base.
Axis::Y => &mut self.y, ///
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
/// -- see [`LayoutLen::declared`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Declared {
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Declared {
pub const NONE: Self = Self { x: None, y: None };
pub fn per_axis(f: impl Fn(Axis) -> Option<Len>) -> Self {
Self {
x: f(Axis::X),
y: f(Axis::Y),
} }
} }
} }
impl_axis_index!(Declared => Option<Len>);
+4 -4
View File
@@ -130,10 +130,10 @@ impl Widgets {
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) { pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
let id = id.id(); let id = id.id();
let data = self.data_mut(id).unwrap(); let data = self.data_mut(id).unwrap();
if *data.size.axis_mut(axis) == rule { if data.size[axis] == rule {
return; return;
} }
*data.size.axis_mut(axis) = rule; data.size[axis] = rule;
self.needs_redraw.insert(id); self.needs_redraw.insert(id);
} }
@@ -147,10 +147,10 @@ impl Widgets {
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) { pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
let id = id.id(); let id = id.id();
let data = self.data_mut(id).unwrap(); let data = self.data_mut(id).unwrap();
if *data.align.axis_mut(axis) == align { if data.align[axis] == align {
return; return;
} }
*data.align.axis_mut(axis) = align; data.align[axis] = align;
self.needs_redraw.insert(id); self.needs_redraw.insert(id);
} }
+6 -6
View File
@@ -118,9 +118,9 @@ pub struct Branch {
impl Widget for Branch { impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40)); let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut))); let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
let measured = painter let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .widget_at(&self.probe, top.axis(Axis::Y))
.len(Axis::X); .len(Axis::X);
let len = measured.apply_leftover(); let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X); let px = painter.to_px(len, Axis::X);
@@ -134,11 +134,11 @@ impl Widget for Branch {
}; };
painter.window_holds(Axis::X, holds.through(len)); painter.window_holds(Axis::X, holds.through(len));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
let place = [Place::Within(Part::All), below]; let place = below.axis(Axis::Y);
match px > threshold { match px > threshold {
true => painter.widget_at(&self.wide, [None; 2], place), true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, [None; 2], place), false => painter.widget_at(&self.narrow, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
+1 -1
View File
@@ -12,7 +12,7 @@ impl Widget for Image {
} }
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> { fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(self.handle.size().axis(axis))) Some(LayoutLen::px(self.handle.size()[axis]))
} }
} }
+3 -9
View File
@@ -7,14 +7,8 @@ pub struct Offset {
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// The whole of this widget's box, moved: the frame passes through, so painter
// what the child declares or reports means the same as it would .widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
// without the offset. .size()
let moved = |len: Len, amt: Len| Place::Within(Part::From(UiSpan::new(amt, len + amt)));
let place = [
moved(painter.extent_len(Axis::X), self.amt.x),
moved(painter.extent_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, [None; 2], place).size()
} }
} }
+2 -21
View File
@@ -14,31 +14,12 @@ impl Widget for Pad {
// widget -- the slack is the inner's to sit in, and forcing the near // widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for. // edge pinned it to a corner it had not asked for.
// //
// Padding is an inset of both: it comes off the frame, so `rel(1)` // Padding is an inset of both: it comes off the rel base, so `rel(1)`
// under it fills this widget rather than overflowing it by the // under it fills this widget rather than overflowing it by the
// padding, and it comes off the box, so what is drawn sits inside. // padding, and it comes off the box, so what is drawn sits inside.
// The two stay distinct -- the box can be narrower still, where a row // The two stay distinct -- the box can be narrower still, where a row
// asked this widget in the room left, and a text wraps at that. // asked this widget in the room left, and a text wraps at that.
let inset = |lead: Px, trail: Px| { let inner = painter.widget_at(&self.inner, self.padding.region()).size();
Place::Within(Part::Inset {
lead: Len::from_parts(Rel::ZERO, lead),
trail: Len::from_parts(Rel::ZERO, trail),
})
};
let place = [
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
// Read from this widget's own frame rather than written as a
// fraction of it: a frame is a length of the window like everything
// else here, and taking the padding off is the whole of what this
// widget does to it.
let narrow = [
(Axis::X, self.padding.left + self.padding.right),
(Axis::Y, self.padding.top + self.padding.bottom),
]
.map(|(axis, pixels)| Some(painter.frame_len(axis) - Len::from_parts(Rel::ZERO, pixels)));
let inner = painter.widget_at(&self.inner, narrow, place).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
+8 -11
View File
@@ -14,9 +14,9 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis); let container_len = painter.px_len(self.axis);
// Asked in the whole viewport, then put at the scrolled offset. // Asked in the whole viewport, then put at the scrolled offset.
let answer_len = painter let answer_len = painter
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2]) .widget_at(&self.inner, PlaceDesc::WHOLE.fills())
.len(self.axis); .len(self.axis);
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis); let fixed = painter.to_px(answer_len.without_leftover(), self.axis);
self.container_len = container_len; self.container_len = container_len;
self.content_len = fixed.max(container_len); self.content_len = fixed.max(container_len);
@@ -24,14 +24,14 @@ impl Widget for Scroll {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
} }
self.update_amt(); self.update_amt();
let align = painter.alignment().axis(self.axis); let align = painter.alignment()[self.axis];
// Content of a fixed length that fits sits at the start of any box it // Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of // fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and // the room left over, so it moves with every length the box takes and
// the drawing holds for that length alone. One scrolled part way sits // the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to // where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length. // the end, it moves with every length.
let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO; let fixed_len = answer_len.is_px();
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, fixed..=Px::MAX); painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end { } else if fixed_len && !self.snap_end {
@@ -54,18 +54,15 @@ impl Widget for Scroll {
let content = match moved || self.content_len != self.container_len { let content = match moved || self.content_len != self.container_len {
true => { true => {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt); let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
Part::From(UiSpan::new(start, start.offset(self.content_len))) UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
} }
false => Part::All, false => PlaceDescAxis::WHOLE,
}; };
// The viewport is the inner's frame, so a fraction it declares or // The viewport is the inner's rel base, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the // reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it goes is the content // content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there. // box, scrolled: its drawing moved there, not made again there.
painter.place_at( painter.place_at(&self.inner, content.axis(self.axis).fills());
&self.inner,
self.axis.pair(Place::Fill(content), Place::Fill(Part::All)),
);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
+32 -81
View File
@@ -10,51 +10,33 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The room left from the cursor to the row's end, said without the // The row: this span's own box, as a length of the rel base its children
// row's length: a child measured in it does not make this drawing // are laid out against. Its start is nothing's business -- a slot is
// depend on how long the row is. // a length from it -- so what this reads is the length alone.
let room_from = |cursor: Len| match self.dir.sign { let far = painter.region_len(axis);
Sign::Pos => Part::Inset { let along = |from: Len, to: Len| match self.dir.sign {
lead: cursor, Sign::Pos => UiSpan::new(from, to),
trail: Len::ZERO, Sign::Neg => UiSpan::new(far - to, far - from),
},
Sign::Neg => Part::Inset {
lead: Len::ZERO,
trail: cursor,
},
}; };
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = Place::Within(Part::All);
// A length for every child before their final slots are chosen: from // A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The frame passes // a hint where one says, and from drawing otherwise. The rel base passes
// through unchanged, so `rel(0.5)` is half the area this span was // through unchanged, so `rel(0.5)` is half the area this span was
// given whatever else is in it and wherever this child sits among // given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the // them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there. // cursor, because a text has to wrap at the width actually there.
// This is the one ask a drawn fixed child gets: its slot is its
// answer, and the drawing is moved there once the shares are known.
// A hinted child is asked once, in its slot.
let mut cursor = Len::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
let mut measured = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let size = match painter.size_hint(child, axis) { let len = match painter.size_hint(child, axis) {
Some(len) => { Some(len) => len,
measured.push(None);
len
}
None => { None => {
let room = Place::Within(room_from(cursor)); // Across itself the child sits where its own alignment
let size = painter // says, in the whole of the row: a span is what contains
.widget_at(child, [None; 2], axis.pair(room, across)) // its children there, and nothing divides that axis.
.size(); let room = along(cursor, far).shifted_desc().axis(axis);
measured.push(Some(size)); painter.widget_at(child, room).len(axis)
size.axis(axis)
} }
}; };
let len = size;
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
lens.push(len); lens.push(len);
@@ -71,25 +53,9 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// The row: this span's own box as a length of the window, read only
// where a slot depends on it -- shares divide what is left of it,
// and a negative row counts from its end. Reading it pins the
// drawing to this length; a positive row of fixed children is not
// pinned and holds for any length its children do. Its start is
// nothing's business: a slot is a length from it.
let far = (total.leftover > Weight::ZERO || self.dir.sign == Sign::Neg)
.then(|| painter.extent_len(axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => {
let far = far.expect("a negative row reads its length");
UiSpan::new(far - to, far - from)
}
};
// What is left for the shares to divide: the row less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the frame rather than a number of pixels. // fixed, as a length of the rel base rather than a number of pixels.
// Nothing where there are no shares, and nothing reads it there. let room = far - total.without_leftover();
let room = far.map_or(Len::ZERO, |far| far - Len::from_parts(total.rel, total.px));
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room` // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the // itself, and answered back through the same expression, so the
@@ -125,57 +91,52 @@ impl Widget for Span {
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let mut start = Len::rel_min(); let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
for ((child, len), measured) in self.children.iter().zip(&lens).zip(&measured) { let shared = |fixed: Len, taken: Weight| match taken == Weight::ZERO {
let len = *len; true => fixed,
false => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
for (child, &len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over, // A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for // when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows. // pixels or a fraction keeps those and overflows.
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares if len.is_only_leftover() && !shares {
{
painter.undraw(child); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
start = shared(fixed, taken);
continue; continue;
} }
let from = start; let from = start;
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
taken += len.leftover; taken += len.leftover;
} }
fixed.px += len.px; fixed += len.without_leftover();
fixed.rel += len.rel; start = shared(fixed, taken);
start = shared(fixed, taken, total.leftover, room);
// Along the row the span says where the child goes, and that slot // Along the row the span says where the child goes, and that slot
// is the child's box outright rather than something to place an // is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere // answer inside again. A share is decided here and nowhere
// else: its slot narrows its frame, and the child is asked in // else: its slot narrows its rel base, and the child is asked in
// it, since a text wraps at the width it is actually given. A // it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so a drawing made in the // fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here. // room is put there as it is, and one not made yet is made here.
let slot = along(from, start); let slot = along(from, start);
let place = axis.pair(Place::Fill(Part::From(slot)), across); let mut place = slot.shifted_desc().fills().axis(axis);
let mut narrow = [None; 2];
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
narrow[axis as usize] = Some(slot.len()); place = place.rel_base(axis, slot.len());
} }
let used = match (measured, narrow[axis as usize]) { let used = painter.place_at(child, place).len(!axis);
(Some(size), None) => {
painter.place_at(child, place);
size.axis(!axis)
}
_ => painter.widget_at(child, narrow, place).len(!axis),
};
if shrinks { if shrinks {
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points. // span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too; // A scalable child therefore makes Children scalable too;
// only fixed children are compared with one another. // only fixed children are compared with one another.
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO { if !used.is_px() {
ortho = LayoutLen::LEFTOVER; ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO { } else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px); ortho.px = ortho.px.max(used.px);
} }
} }
fixed.px += self.gap; fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room); start = shared(fixed, taken);
} }
// Carried whole rather than collapsed to one share: a span that sizes // Carried whole rather than collapsed to one share: a span that sizes
@@ -194,16 +155,6 @@ impl Widget for Span {
} }
} }
/// Where a row has reached: everything fixed before this point, which is a
/// sum and exact, plus the share of the room the weights so far are worth,
/// which is one rounding wherever it is asked for.
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
if taken == Weight::ZERO {
return fixed;
}
fixed + room.scale(Rel::ratio(taken, weight))
}
impl Span { impl Span {
pub fn empty(dir: Dir) -> Self { pub fn empty(dir: Dir) -> Self {
Self { Self {
+6 -8
View File
@@ -22,9 +22,7 @@ impl Widget for Stack {
// drawing belongs to the layer it was made on. // drawing belongs to the layer it was made on.
Some((i, child)) => { Some((i, child)) => {
painter.child_layer_at(i); painter.child_layer_at(i);
painter painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
.widget_at(child, [None; 2], [Place::Fill(Part::All); 2])
.size()
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
@@ -33,11 +31,11 @@ impl Widget for Stack {
// fraction under them is a fraction of it. A share leaves the axis // fraction under them is a fraction of it. A share leaves the axis
// to whoever gave the stack its box. Where a child sits in a box // to whoever gave the stack its box. Where a child sits in a box
// bigger than itself is its own business. // bigger than itself is its own business.
let place = [Axis::X, Axis::Y].map(|axis| { let place = PlaceDesc::per_axis(|axis| {
let len = size.axis(axis); let len = size[axis];
match len.leftover == Weight::ZERO { match len.leftover == Weight::ZERO {
true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))), true => len.without_leftover().as_desc().fills(),
false => Place::Within(Part::All), false => PlaceDescAxis::WHOLE,
} }
}); });
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
@@ -45,7 +43,7 @@ impl Widget for Stack {
continue; continue;
} }
painter.child_layer_at(i); painter.child_layer_at(i);
painter.widget_at(child, [None; 2], place); painter.widget_at(child, place);
} }
size size
} }
+6 -6
View File
@@ -22,17 +22,17 @@ struct BranchesOnMeasurement {
impl Widget for BranchesOnMeasurement { impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40)); let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut))); let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
let measured = painter let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .widget_at(&self.probe, top.axis(Axis::Y))
.len(Axis::X); .len(Axis::X);
let px = painter.to_px(measured.apply_leftover(), Axis::X); let px = painter.to_px(measured.apply_leftover(), Axis::X);
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
let place = [Place::Within(Part::All), below]; let place = below.axis(Axis::Y);
match px > Px::from_f32(self.threshold) { match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, [None; 2], place), true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, [None; 2], place), false => painter.widget_at(&self.narrow, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
+46 -9
View File
@@ -108,7 +108,7 @@ const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the bo
/// the icon's width, while a wrapping text beside it is asked in the room /// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there. /// left, 900 - 24 - 32, and wraps there.
#[test] #[test]
fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() { fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200)); let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc); let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc); let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
@@ -124,8 +124,8 @@ fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0))); h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()]; let active = &h.render.active[&text.id()];
let window = h.render.output_size().x; let window = h.render.output_size().x;
let asked = active.part.x.len().to_px(window); let asked = active.region.x.len().to_px(window);
assert_eq!(active.frame.x.to_px(window), Px::from_int(868)); assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844)); assert_eq!(asked, Px::from_int(844));
} }
@@ -155,7 +155,7 @@ fn a_share_inside_padding_fills_the_slot_it_was_given() {
/// The same padding in a share instead: the slot is 450, so both the /// The same padding in a share instead: the slot is 450, so both the
/// fraction and the wrap are the slot less the padding, and the two agree. /// fraction and the wrap are the slot less the padding, and the two agree.
#[test] #[test]
fn padding_narrows_both_frame_and_box_inside_a_share() { fn padding_narrows_both_rel_base_and_box_inside_a_share() {
let mut h = Harness::new((900, 200)); let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc); let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc); let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
@@ -170,8 +170,8 @@ fn padding_narrows_both_frame_and_box_inside_a_share() {
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0))); h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()]; let active = &h.render.active[&text.id()];
let window = h.render.output_size().x; let window = h.render.output_size().x;
assert_eq!(active.frame.x.to_px(window), Px::from_int(418)); assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
assert_eq!(active.part.x.len().to_px(window), Px::from_int(418)); assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
} }
#[test] #[test]
@@ -500,11 +500,11 @@ fn a_row_of_equal_shares_fills_it_exactly() {
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`. /// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.move_idx, active.extent); let region = h.render.moves.resolve(active.move_idx, active.placement);
let dim = h.size().axis(axis); let dim = h.size()[axis];
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor(); let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32()); let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis); let span = region[axis];
(edge(span.start), edge(span.end)) (edge(span.start), edge(span.end))
} }
@@ -821,3 +821,40 @@ fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
h.set_root(root); h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450)); assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
} }
#[test]
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
for collapsed in [1, 2] {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).add(&mut h.rsc);
h.set_len(head, dir.axis, 200);
let tail = rect(Color::BLUE).add(&mut h.rsc);
let tail_len = 200 - 10 * (collapsed + 1);
h.set_len(tail, dir.axis, tail_len);
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
let mut shares = Vec::new();
for _ in 0..collapsed {
let share = rect(Color::GREEN).add(&mut h.rsc);
shares.push(share);
children.push(share.add_strong(&mut h.rsc));
}
children.push(tail.add_strong(&mut h.rsc));
h.set_root(Span {
children,
dir,
gap: Px::from_int(10),
});
for share in shares {
assert!(h.region(&share).is_none());
}
let region = h.region(&tail).unwrap();
let (from, to) = match dir.sign {
Sign::Pos => (400 - tail_len, 400),
Sign::Neg => (0, tail_len),
};
assert_eq!(region.top_left[dir.axis], Px::from_int(from));
assert_eq!(region.bot_right[dir.axis], Px::from_int(to));
}
}
}
+193 -58
View File
@@ -214,11 +214,7 @@ impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap(); let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px)); let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
painter.widget_at( painter.widget_at(&self.inner, top.shifted_desc().axis(Axis::Y));
&self.inner,
[None; 2],
[Place::Within(Part::All), Place::Within(Part::From(top))],
);
Size::LEFTOVER Size::LEFTOVER
} }
} }
@@ -797,7 +793,7 @@ fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
} }
#[test] #[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() { fn changing_an_inherited_region_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) { fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
let first = rect(Color::RED).width(width).add(&mut h.rsc); let first = rect(Color::RED).width(width).add(&mut h.rsc);
let words = wtext(text).size(20).wrap(true).add(&mut h.rsc); let words = wtext(text).size(20).wrap(true).add(&mut h.rsc);
@@ -877,11 +873,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
[None; 2], PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
); );
Size::LEFTOVER Size::LEFTOVER
} }
@@ -959,18 +951,18 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
} }
struct Frame { struct Frame {
child: StrongWidget, child: StrongWidget,
frame: UiRegion,
region: UiRegion, region: UiRegion,
extent: UiRegion,
} }
impl Widget for Frame { impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
[Some(self.region.x.len()), None], PlaceDesc::new(
[ self.region.x.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.x)), self.region.y.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.y)), )
], .rel_base(Axis::X, self.frame.x.len()),
); );
Size::LEFTOVER Size::LEFTOVER
} }
@@ -986,15 +978,15 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
h.rsc.widgets_mut().set_region_node(text, node); h.rsc.widgets_mut().set_region_node(text, node);
let root = Frame { let root = Frame {
child: text.add_strong(&mut h.rsc), child: text.add_strong(&mut h.rsc),
frame: UiRegion::FULL,
region: UiRegion::FULL, region: UiRegion::FULL,
extent: UiRegion::FULL,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
h.set_root(root); h.set_root(root);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] { for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let before = draws.get(); let before = draws.get();
h.rsc[root].region.x = UiSpan::new(Len::px(13.125), Len::px(287.375)); h.rsc[root].frame.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
h.rsc[root].extent = UiRegion::new( h.rsc[root].region = UiRegion::new(
UiSpan::new(Len::rel(start), Len::rel(end)), UiSpan::new(Len::rel(start), Len::rel(end)),
UiSpan::new(Len::px(7.25), Len::rel(end)), UiSpan::new(Len::px(7.25), Len::rel(end)),
); );
@@ -1117,7 +1109,7 @@ fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
assert_eq!(leaf_draws.get(), settled + 1); assert_eq!(leaf_draws.get(), settled + 1);
} }
#[test] #[test]
fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() { fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
struct Observed<W> { struct Observed<W> {
widget: W, widget: W,
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
@@ -1130,23 +1122,22 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
} }
struct Frame { struct Frame {
child: StrongWidget, child: StrongWidget,
extent: UiRegion, region: UiRegion,
} }
impl Widget for Frame { impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
[None; 2], PlaceDesc::new(
[ self.region.x.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.x)), self.region.y.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.y)), ),
],
); );
Size::LEFTOVER Size::LEFTOVER
} }
} }
for node in [false, true] { for node in [false, true] {
let plant = |h: &mut Harness, extent| { let plant = |h: &mut Harness, region| {
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = rect(Color::BLUE).masked().add(&mut h.rsc); let leaf = rect(Color::BLUE).masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node); h.rsc.widgets_mut().set_region_node(leaf, node);
@@ -1167,7 +1158,7 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
draws: draws.clone(), draws: draws.clone(),
} }
.add_strong(&mut h.rsc); .add_strong(&mut h.rsc);
let root = Frame { child: pad, extent }.add(&mut h.rsc); let root = Frame { child: pad, region }.add(&mut h.rsc);
h.set_root(root); h.set_root(root);
(root, leaf, fixed, draws) (root, leaf, fixed, draws)
}; };
@@ -1182,13 +1173,13 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
let mut warm = Harness::new((403, 211)); let mut warm = Harness::new((403, 211));
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13)); let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
for start in [0.13, -0.17, 0.31] { for start in [0.13, -0.17, 0.31] {
let extent = at(start); let region = at(start);
let before = draws.get(); let before = draws.get();
warm.rsc[root].extent = extent; warm.rsc[root].region = region;
warm.frame(); warm.frame();
assert_eq!(draws.get(), before); assert_eq!(draws.get(), before);
let mut cold = Harness::new((403, 211)); let mut cold = Harness::new((403, 211));
let (_, other, other_fixed, _) = plant(&mut cold, extent); let (_, other, other_fixed, _) = plant(&mut cold, region);
for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] { for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] {
assert_eq!(warm.region(&a), cold.region(&b)); assert_eq!(warm.region(&a), cold.region(&b));
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b)); assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
@@ -1207,7 +1198,7 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
} }
#[test] #[test]
fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() { fn moving_a_childs_region_preserves_the_slot_chosen_from_its_measurement() {
struct Measured; struct Measured;
impl Widget for Measured { impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
@@ -1224,14 +1215,12 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
[None; 2], PlaceDesc::new(
[ UiSpan::new(Len::px(self.start), Len::px(self.start + 200.0))
Place::Fill(Part::From(UiSpan::new( .shifted_desc()
Len::px(self.start), .fills(),
Len::px(self.start + 200.0), UiSpan::FULL.shifted_desc().fills(),
))), ),
Place::Fill(Part::From(UiSpan::FULL)),
],
); );
Size::LEFTOVER Size::LEFTOVER
} }
@@ -1256,7 +1245,7 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
} }
#[test] #[test]
fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() { fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() {
struct Container { struct Container {
child: StrongWidget, child: StrongWidget,
region: UiRegion, region: UiRegion,
@@ -1266,18 +1255,14 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter painter
.widget_at( .widget_at(
&self.child, &self.child,
[None; 2], PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
) )
.size() .size()
} }
} }
struct Frame { struct Frame {
child: StrongWidget, child: StrongWidget,
extent: UiRegion, region: UiRegion,
answer: Rc<Cell<Size>>, answer: Rc<Cell<Size>>,
} }
impl Widget for Frame { impl Widget for Frame {
@@ -1286,11 +1271,10 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter painter
.widget_at( .widget_at(
&self.child, &self.child,
[None; 2], PlaceDesc::new(
[ self.region.x.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.x)), self.region.y.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.y)), ),
],
) )
.size(), .size(),
); );
@@ -1302,7 +1286,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
UiRegion::FULL, UiRegion::FULL,
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL), UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
] { ] {
let plant = |h: &mut Harness, extent| { let plant = |h: &mut Harness, outer| {
let size = if fractional { let size = if fractional {
Size { Size {
x: rel(0.5), x: rel(0.5),
@@ -1320,7 +1304,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
let answer = Rc::new(Cell::new(Size::ZERO)); let answer = Rc::new(Cell::new(Size::ZERO));
let root = Frame { let root = Frame {
child, child,
extent, region: outer,
answer: answer.clone(), answer: answer.clone(),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
@@ -1330,15 +1314,166 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
let mut warm = Harness::new((403, 211)); let mut warm = Harness::new((403, 211));
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL); let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
for width in [191.125, 297.25, 83.75] { for width in [191.125, 297.25, 83.75] {
let extent = let region =
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL); UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
warm.rsc[root].extent = extent; warm.rsc[root].region = region;
warm.frame(); warm.frame();
let mut cold = Harness::new((403, 211)); let mut cold = Harness::new((403, 211));
let (_, other, other_answer) = plant(&mut cold, extent); let (_, other, other_answer) = plant(&mut cold, region);
assert_eq!(answer.get(), other_answer.get()); assert_eq!(answer.get(), other_answer.get());
assert_eq!(warm.region(&leaf), cold.region(&other)); assert_eq!(warm.region(&leaf), cold.region(&other));
} }
} }
} }
} }
struct OptionalMask {
inner: StrongWidget,
enabled: bool,
}
impl Widget for OptionalMask {
fn draw(&mut self, painter: &mut Painter) -> Size {
if self.enabled {
painter.set_mask(UiRegion::FULL);
}
painter.widget(&self.inner);
Size::LEFTOVER
}
}
fn primitive_masks(h: &Harness, id: WidgetId) -> Vec<MaskIdx> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx]
.mask_idx
})
.collect()
}
#[test]
fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::RED).height(50).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let child = Stretchy {
inner: inner.add_strong(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
let masked = child.masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(masked, node);
h.set_root((first, masked).span(Dir::DOWN));
let mask = h.render.active[&masked.id()].mask;
let settled = draws.get();
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
assert_eq!(h.render.active[&masked.id()].mask, mask);
h.set_len(first, Axis::Y, 10);
h.frame();
let clip = h.rsc.ui().masks[mask.idx()];
let clip = h
.render
.moves
.resolve(clip.move_idx, clip.region)
.to_px(h.render.output_size());
assert_eq!(clip, h.region(&masked).unwrap());
assert_corners!(h, inner, (0, 10), (400, 200));
}
}
#[test]
fn adding_and_removing_a_mask_updates_existing_primitives() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = OptionalMask {
inner: inner.add_strong(&mut h.rsc),
enabled: false,
}
.add(&mut h.rsc);
h.set_root(masked);
for enabled in [true, false, true, false] {
h.rsc[masked].enabled = enabled;
h.frame();
let mask = h.render.active[&masked.id()].mask;
assert_eq!(mask == MaskIdx::NONE, !enabled);
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
}
assert_eq!(h.rsc.ui().masks.len(), 1, "retired slots must be reusable");
}
#[test]
fn an_empty_masks_slot_is_released_when_the_mask_is_removed_or_undrawn() {
let mut h = Harness::new((400, 200));
let (inner, _) = counted(&mut h, Size::LEFTOVER, false);
let masked = OptionalMask {
inner: inner.add_strong(&mut h.rsc),
enabled: true,
}
.add(&mut h.rsc);
let row = (masked,).span(Dir::DOWN).add(&mut h.rsc);
h.set_root(row);
for _ in 0..3 {
h.rsc[masked].enabled = false;
h.frame();
h.rsc[masked].enabled = true;
h.frame();
let child = h.rsc[row].pop().unwrap();
h.frame();
h.rsc[row].push(child);
h.frame();
}
assert_eq!(h.rsc.ui().masks.len(), 1);
}
struct SharedChild(Rc<StrongWidget>);
impl Widget for SharedChild {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(self.0.as_ref()).size()
}
}
struct SwitchParent {
choices: [StrongWidget; 2],
choice: usize,
}
impl Widget for SwitchParent {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(&self.choices[self.choice]).size()
}
}
#[test]
fn a_redrawn_subtree_is_not_undrawn_by_the_parent_it_left() {
for node in [false, true] {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::RED).width(40).add(&mut h.rsc);
let held: StrongWidget = leaf.add_strong(&mut h.rsc);
let shared = Rc::new(held);
let first = SharedChild(shared.clone()).add_strong(&mut h.rsc);
let second = SharedChild(shared).add_strong(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(&second, node);
let root = SwitchParent {
choices: [first, second],
choice: 0,
}
.add(&mut h.rsc);
h.set_root(root);
let before = h.region(&leaf);
h.rsc[root].choice = 1;
h.frame();
assert_eq!(h.region(&leaf), before);
}
}
+8 -4
View File
@@ -14,7 +14,7 @@
#[path = "scenario/mod.rs"] #[path = "scenario/mod.rs"]
mod scenario; mod scenario;
use iris::random::{Edits, plan}; use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds}; use scenario::{ALL, Case, diverges, env, over_seeds};
/// How deep the generator branches. The generator widens two to four ways per /// How deep the generator branches. The generator widens two to four ways per
@@ -32,8 +32,11 @@ fn depth() -> usize {
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98]; const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
fn check(seed: u64, depth: usize, case: Case) { fn check(seed: u64, depth: usize, case: Case) {
let grown = plan(seed, depth, &Edits::default()); check_plan(&plan(seed, depth, &Edits::default()), seed, depth, case);
if let Some(how) = diverges(&grown, case, seed) { }
fn check_plan(grown: &Plan, seed: u64, depth: usize, case: Case) {
if let Some(how) = diverges(grown, case, seed) {
panic!( panic!(
"seed {seed} at depth {depth} differs after {}: {how}\n\ "seed {seed} at depth {depth} differs after {}: {how}\n\
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \ reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
@@ -119,8 +122,9 @@ fn a_long_run_of_seeds_agrees() {
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(), None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
}; };
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default());
for case in ALL { for case in ALL {
check(seed, depth, case); check_plan(&grown, seed, depth, case);
} }
}); });
} }
+8 -8
View File
@@ -406,8 +406,8 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
fn record(id: WidgetId, h: &Harness) -> String { fn record(id: WidgetId, h: &Harness) -> String {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
format!( format!(
"frame {} ask {} box {} size {}", "rel_base {} region {} placement {} size {}",
active.frame, active.part, active.extent, active.size, active.rel_base, active.region, active.placement, active.size,
) )
} }
@@ -444,12 +444,6 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
cold.state.root = Some(root); cold.state.root = Some(root);
cold.frame(); cold.frame();
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let mut drawn = 0; let mut drawn = 0;
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() { for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
@@ -457,6 +451,12 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
if got == want { if got == want {
continue; continue;
} }
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
// Where two trees disagree is rarely where the cause is, so the // Where two trees disagree is rarely where the cause is, so the
// ancestry comes with it, marking the widgets that own a region. // ancestry comes with it, marking the widgets that own a region.
let mut chain = Vec::new(); let mut chain = Vec::new();