Make the frame a length of the window and the box a region

There is one coordinate unit, the window. Every box in the tree is a
region in window units and a widget's frame is a length in the same
units, which is only what fractions resolve against, so the box need not
be the frame and padding can take from both without either becoming the
other. A region node's entry is a translation -- a rel 1 region anchored
where its box starts -- rather than a box, so nothing composes a frame
back up a chain and a node that moves is one entry write.

Padding is then an inset of both: its pixels come off the frame, so
rel(1.0) under it fills the padded widget rather than overflowing it,
and off the box, so what is drawn sits inside. A length a container
decides for a child's frame is a length of the window like everything
else here -- a row's slot, padding's frame less its pixels, or the box a
stack's sizing child decided, which arrives as Part::Sized -- because a
slot of a row is not a fraction of anything the row can name, the same
reason a node entry is a translation. A declaration is a fraction of
whichever of those reached it, and is the only one that also places the
box.

Frame validity is a pin beside the box's, not a range: a range of window
pixels cannot say which frame an answer is a fraction of, since two
frames are different lengths at the same window size. A widget pins its
frame by reading it or by being answered with it under a fractional
rule, and the pin composes up wherever a length of this frame is what
reached the child.

Also here, because the diagnosis needed them: the shrinker reports the
shrunk tree's own divergence with each level's frame, ask, box and size
warm against cold, and there is a size-resize case -- a change and then a
resize, the order that shows an answer kept as a fraction of the wrong
length, which every other case compares at the window it was made at.

Three defects the reports found, each pinned: a rule changed over two
pads relocated the column under them instead of dividing it again (seed
59, depth 5, resize-size), a share inside padding had the padding taken
off twice, and a root resolved its own rule twice.

fmt and clippy clean with and without layout-diagnostics, 121 suite, 20
core, 11 generated, the 400-seed depth-5 shrinker over all sixteen cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-19 00:14:46 -04:00
1 parent 0ef87ebfcf
commit 1512d8418b
15 files changed
+680 -444

No files matched your search

+23 -28
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,22 +9,15 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// Its frame in `parent_move`'s coordinates: what a fraction it declares /// Where its drawing goes, in its region node's coordinates.
/// or reports is a fraction of, composed. Everything it draws sits inside
/// this by way of `extent`.
pub frame_abs: UiRegion,
/// Where its drawing goes, in the frame's own coordinates.
pub extent: UiRegion, pub extent: UiRegion,
/// That frame in its parent's frame coordinates, before composition: /// What a fraction declared or reported under this widget is a fraction
/// forwarded whole by a transparent container, narrowed by a declared /// of, as a length of the window.
/// length. Its length is the same on every ask, which is what pub frame: UiVec2,
/// a local redraw relies on to ask its parent's own question again. /// A frame its parent decided for it on each axis -- a row's slot, or
pub frame: UiRegion, /// padding's frame less its pixels -- as a length of the window. `None`
/// The length its frame was narrowed to on each axis, as a length of /// forwards the parent's frame. What it declared is kept separately in
/// its parent's frame: a declared rule, or a box its parent decided for /// `declared` and is a fraction of whichever of the two reached it.
/// it. `None` forwards the parent's frame whole. Kept as a length rather
/// than a position so that every placement puts the frame back in the
/// part it is given.
pub narrow: [Option<Len>; 2], pub narrow: [Option<Len>; 2],
/// Where its drawing was put, as a part of its parent's box, and where /// Where its drawing was put, as a part of its parent's box, and where
/// it was asked. The two differ where a container asks in one place and /// it was asked. The two differ where a container asks in one place and
@@ -33,9 +26,9 @@ pub struct ActiveData {
/// so a box that moved re-places every child by re-adding that start. /// so a box that moved re-places every child by re-adding that start.
pub place: [Place; 2], pub place: [Place; 2],
pub offer_place: [Place; 2], pub offer_place: [Place; 2],
/// The box it was asked in, in its frame's coordinates: the box its /// The box it was asked in, in the parent's region-node coordinates: the
/// drawing was made in and the one its contract is about. Its drawing /// box its drawing was made in and the one its contract is about. Its
/// is placed elsewhere by re-expression, never by asking again there. /// drawing is placed elsewhere by re-expression, never by asking again.
pub offer_part: UiRegion, pub offer_part: 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.
@@ -45,9 +38,10 @@ pub struct ActiveData {
/// rests on the first answer and its drawing on the last, so only the /// rests on the first answer and its drawing on the last, so only the
/// parent can ask either again. /// parent can ask either again.
pub re_asked: bool, pub re_asked: bool,
/// What the widget said it used of its frame, the last time it drew. /// What the widget reported, in window-unit lengths.
pub size: Size, pub size: Size,
/// The frame and extent reads that this drawing holds for. /// The window and extent reads that this drawing holds for, and the
/// frame and box it pinned.
pub holds: LayoutHolds, pub holds: LayoutHolds,
pub drawn: bool, pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
@@ -73,7 +67,8 @@ pub struct ActiveData {
/// 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 `frame_abs` uses. /// The movable region whose coordinates `extent` uses when this 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.
@@ -95,12 +90,12 @@ impl ActiveData {
self.answer.map(|(size, _)| size) self.answer.map(|(size, _)| size)
} }
/// Whether what it answered still stands for a frame of these pixel /// Whether what it answered still stands in this window, for the frame
/// lengths. The answer was given in the box its parent first asked /// and the box it was asked in. The answer was given in the box its
/// about, which is what it is checked against -- `holds` on the record /// parent first asked about, which is what it is checked against --
/// is about the box the answer then chose. /// `holds` on the record is about the box the answer then chose.
pub fn answers_at(&self, px: crate::PxVec2, part: UiRegion) -> bool { pub fn answers_at(&self, window: crate::PxVec2, part: UiRegion) -> bool {
self.answer self.answer
.is_some_and(|(_, holds)| holds.contains(px, part)) .is_some_and(|(_, holds)| holds.contains(window, self.frame, part))
} }
} }
+27 -11
View File
@@ -1,20 +1,27 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion}; use crate::{Axis, Holds, Len, 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 pixel lengths of its /// What one evaluation of a widget depends on: the window lengths its reads
/// frame and of its own box that its drawing and its answer hold for, and /// hold for, the pixel lengths of its own box, and the symbolic lengths of
/// the symbolic length of its own box where it read one. /// that box and of its frame where either one is what it was expressed in.
/// ///
/// The symbolic length is a pin rather than a range: 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 frame 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. It reaches the parent only where the box it pinned is the parent's /// is. A box pin reaches the parent only where the box it pinned is the
/// own; anywhere else the parent chose that length itself, and a widget /// parent's own; anywhere else the parent chose that length itself, and a
/// 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,
/// which is a different length wherever the frame is a different one -- at
/// 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
/// whatever the frame turns out to be.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds { pub struct LayoutHolds {
pub frame: [Holds; 2], pub frame: [Holds; 2],
pub frame_len: [Option<Len>; 2],
pub extent: [Holds; 2], pub extent: [Holds; 2],
pub extent_len: [Option<Len>; 2], pub extent_len: [Option<Len>; 2],
} }
@@ -22,6 +29,7 @@ pub struct LayoutHolds {
impl LayoutHolds { impl LayoutHolds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
frame: [Holds::ANY; 2], frame: [Holds::ANY; 2],
frame_len: [None; 2],
extent: [Holds::ANY; 2], extent: [Holds::ANY; 2],
extent_len: [None; 2], extent_len: [None; 2],
}; };
@@ -36,7 +44,13 @@ impl LayoutHolds {
|| other.extent_len[n].is_none() || other.extent_len[n].is_none()
|| self.extent_len[n] == other.extent_len[n] || 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.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 result
} }
@@ -48,15 +62,17 @@ impl LayoutHolds {
&& self.extent[n].lo <= other.extent[n].lo && self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi && self.extent[n].hi >= other.extent[n].hi
&& self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len)) && 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, px: PxVec2, extent: UiRegion) -> bool { pub fn contains(self, window: PxVec2, frame: UiVec2, extent: UiRegion) -> bool {
AXES.into_iter().all(|axis| { AXES.into_iter().all(|axis| {
let n = axis as usize; let n = axis as usize;
let len = extent.axis(axis).len(); let len = extent.axis(axis).len();
self.frame[n].contains(px.axis(axis)) self.frame[n].contains(window.axis(axis))
&& self.extent[n].contains(len.to_px(px.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) && self.extent_len[n].is_none_or(|pinned| pinned == len)
}) })
} }
+163 -169
View File
@@ -17,26 +17,20 @@ 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,
/// What a fraction this widget declares or reports is a fraction of, in /// This widget's frame, per axis: a length of the window, and what a
/// the coordinates of `move_idx`: forwarded from its parent unchanged /// fraction it or anything under it declares or reports is a fraction
/// through a span, a stack or a scroll, and narrowed only by what was /// of. A length rather than a box, so padding can take from both the
/// decided above it -- a declared length, or the root. Its length /// frame and the box without either becoming the other.
/// is the same on every ask of the widget, which is what keeps a fraction pub(super) frame: UiVec2,
/// under it from being resolved twice. /// Where this widget's drawing goes, in its region node's coordinates.
pub(super) frame: UiRegion,
/// Where this widget's drawing goes, in the frame's own coordinates.
/// Everything it writes is in these coordinates, and its children are
/// placed as parts of it.
pub(super) extent: UiRegion, pub(super) extent: UiRegion,
/// The extent's symbolic length where this draw read it, which makes the /// The extent's symbolic length where this draw read it, which makes the
/// drawing one that holds for that length alone -- the way reading a /// drawing one that holds for that length alone -- the way reading a
/// length in pixels makes it hold for that number of pixels. /// length in pixels makes it hold for that number of pixels.
pub(super) extent_len: [Option<Len>; 2], pub(super) extent_len: [Option<Len>; 2],
/// The frame in pixels, which its children's frames are a length of: /// The window in pixels. Frames and boxes become pixels against this one
/// threaded down rather than composed back up the chain, so every length /// unit, regardless of region-node boundaries.
/// in layout is one multiply from its parent's and [`Holds::through`] pub(super) window: PxVec2,
/// inverts exactly that.
pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>, pub(super) primitives: Vec<RetainedPrimitive>,
@@ -46,10 +40,13 @@ pub struct Painter<'a> {
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 its frame in pixels, per axis: every /// What this draw itself read of the window in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise. /// window until it reads one, then that one, unless it says otherwise.
pub(super) frame_own: [Holds; 2], pub(super) frame_own: [Holds; 2],
/// The same for its own box. /// Its frame's symbolic length where this draw read it, which makes the
/// drawing one that holds for that frame alone.
pub(super) frame_own_len: [Option<Len>; 2],
/// The window reads' equivalent for its own box.
pub(super) extent_own: [Holds; 2], 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.
@@ -76,12 +73,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 the /// A box in this widget's extent coordinates, composed into its region
/// coordinates its move slot is in: through the extent, then through the /// node's coordinates.
/// frame the extent is a part of. The same two steps a recomposition
/// replays, so a moved drawing lands where a cold one does.
fn resolve(&self, region: UiRegion) -> UiRegion { fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent).within(&self.frame) region.within(&self.extent)
} }
fn write_resolved<P: Primitive>( fn write_resolved<P: Primitive>(
@@ -173,27 +168,19 @@ impl<'a> Painter<'a> {
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 declared_narrow = narrowed_by(declared); let (frame, extent) =
let narrow = [ frame_and_extent(self.extent, self.frame, place, narrow, declared, align);
declared_narrow[0].or(narrow[0]),
declared_narrow[1].or(narrow[1]),
];
let (local, extent) = frame_and_extent(part_of(self.extent, place), narrow, align);
let within = match local == UiRegion::FULL {
true => self.frame,
false => local.within(&self.frame),
};
#[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, within); diag::region_node(id.id(), self.id, extent);
} }
// 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 = local.size().to_px(self.px); let px = frame.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 {
@@ -203,8 +190,7 @@ 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: local, frame,
frame_abs: within,
part: extent, part: extent,
place, place,
offer_place: place, offer_place: place,
@@ -215,9 +201,8 @@ impl<'a> Painter<'a> {
None, None,
self.rsc, self.rsc,
); );
let own = self.extent; let holds = self.in_parent(holds, extent, place, narrow, declared);
let compose = |holds| in_parent(holds, local, extent, place, narrow, own); let answer_holds = self.in_parent(answer_holds, extent, place, narrow, declared);
let holds = compose(holds);
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)),
@@ -225,8 +210,8 @@ impl<'a> Painter<'a> {
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size: in_parent_frame(size, local.size(), declared), size,
answer_holds: compose(answer_holds), answer_holds,
} }
} }
@@ -258,8 +243,8 @@ impl<'a> Painter<'a> {
Placing { Placing {
id: self.id, id: self.id,
extent: self.extent, extent: self.extent,
local: self.frame, frame: self.frame,
px: self.px, window: self.window,
depth: self.depth, depth: self.depth,
move_idx: self.move_idx, move_idx: self.move_idx,
mask: self.mask, mask: self.mask,
@@ -367,6 +352,17 @@ impl<'a> Painter<'a> {
len len
} }
/// The symbolic length of this widget's frame along one axis: what a
/// fraction it or anything under it declares is a fraction of. A
/// container reads it to hand a length of it down -- padding, which
/// takes its pixels off. Reading it pins the drawing to that frame, the
/// way [`Self::extent_len`] pins it to the box.
pub fn frame_len(&mut self, axis: Axis) -> Len {
let len = self.frame.axis(axis);
self.frame_own_len[axis as usize] = Some(len);
len
}
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
/// widget that positions its own content reads it to place that content /// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget. /// the way the box around it would have placed the widget.
@@ -402,7 +398,7 @@ impl<'a> Painter<'a> {
/// [`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 part = self.extent.axis(axis).len();
let len = part.to_px(self.px.axis(axis)); let len = part.to_px(self.window.axis(axis));
let own = &mut self.extent_own[axis as usize]; let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
@@ -418,7 +414,7 @@ impl<'a> Painter<'a> {
let part = self.extent.axis(axis).len(); let part = self.extent.axis(axis).len();
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))), holds.contains(part.to_px(self.window.axis(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
@@ -426,26 +422,19 @@ impl<'a> Painter<'a> {
self.extent_own[axis as usize] = holds; self.extent_own[axis as usize] = holds;
} }
/// One axis of this widget's frame in pixels -- what a fraction of its /// One window axis in pixels. Every length in layout is a length of the
/// area resolves against, and so what a container divides among its /// window, so this is what one becomes pixels against.
/// children. Its own box is a part of this one. pub fn window_px_len(&self, axis: Axis) -> Px {
pub fn frame_px_len(&mut self, axis: Axis) -> Px { self.window.axis(axis)
let len = self.px.axis(axis);
let own = &mut self.frame_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
} }
/// [`Self::holds`] stated about the frame rather than about this /// A validity range already stated about the window. Containers use
/// widget's own box, for a container whose drawing turns on what its /// this after branching on a window-unit length.
/// fractions are of rather than on the part of it it took. pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
pub fn frame_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.px.axis(axis)), holds.contains(self.window.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its frame", "'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(), self.label(),
self.id self.id
); );
@@ -537,81 +526,77 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// What a child depends on, said about the boxes the widget that drew it /// Moves what a child depends on into this widget's own terms: this
/// has rather than the ones the child was given. /// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// /// only what the child was asked with.
/// `frame` is the child's frame in this widget's frame coordinates and impl Painter<'_> {
/// `extent` the box it was given, in the child's own frame coordinates. Both /// Frame ranges are already ranges on the window and combine directly.
/// reach it as one length, so what it holds for maps back through that /// A frame pin becomes this widget's own frame wherever a length of it
/// length exactly -- and where the box it was given is this widget's own, /// is what reached the child; where only pixels did, no length of this
/// what it says about that box is what this widget can say about its own. /// frame can change the child's and the pin stops here.
/// `own` is this widget's own box, for a pin that cannot be said exactly. ///
pub(crate) fn in_parent( /// Extent validity maps back through the part of this widget's box,
holds: LayoutHolds, /// where the box the child was asked in is that part; a declared length
frame: UiRegion, /// places the box inside the part instead, and then only that length
extent: UiRegion, /// reaches the child. A narrowed frame is not one of these: it decides
place: [Place; 2], /// what fractions under the child mean and leaves the box the part it
narrow: [Option<Len>; 2], /// was given.
own: UiRegion, fn in_parent(
) -> LayoutHolds { &self,
let mut result = LayoutHolds::ANY; holds: LayoutHolds,
for axis in AXES { extent: UiRegion,
let n = axis as usize; place: [Place; 2],
let frame_len = frame.axis(axis).len(); narrow: [Option<Len>; 2],
result.frame[n] = holds.frame[n].through(frame_len); declared: [Option<LayoutLen>; 2],
match (place[n].part(), narrow[n]) { ) -> LayoutHolds {
// Its box is this widget's own, or a part of it in that box's let mut result = LayoutHolds::ANY;
// own lengths: so what it holds for is a range on this widget's for axis in AXES {
// own box, which is what lets that box move without a redraw. A let n = axis as usize;
// length it pinned is this widget's length wherever the part is // Every frame range is already a range on the window: the
// the whole of it, and pins the same way. // widget's own read converted through its frame exactly once.
(Part::All, None) => { result.frame[n] = holds.frame[n];
result.extent[n] = holds.extent[n]; let reaches = narrow[n].is_none()
result.extent_len[n] = holds.extent_len[n]; && !matches!(place[n].part(), Part::Sized(_))
} && declared[n].is_none_or(|len| len.rel != Rel::ZERO);
// Its box is a part of this widget's own box, in that box's own result.frame_len[n] = holds.frame_len[n].and(reaches.then(|| self.frame.axis(axis)));
// lengths, so what it holds for maps back through that part into match (place[n].part(), declared[n].is_some()) {
// a range on this widget's box. A length it pinned is this // Its box is this widget's own, or a part of it in that
// widget's length less the part's pixels where the part is the // box's own lengths: so what it holds for is a range on this
// whole of the box less pixels, which is the one shape that // widget's own box, which is what lets that box move without
// inverts exactly; any other part pins this widget's own length. // a redraw. A length it pinned is this widget's length
(Part::Of(span), None) => { // wherever the part is the whole of it, and pins the same
let part_len = span.len(); // way.
result.extent[n] = holds.extent[n].through(part_len); (Part::All, false) => {
result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel { result.extent[n] = holds.extent[n];
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px), result.extent_len[n] = holds.extent_len[n];
_ => own.axis(axis).len(), }
}); // Its box is a part of this widget's own box, in that box's
} // own lengths, so what it holds for maps back through that
// Its box is a part of this widget's frame, or a length of it // part into a range on this widget's box. A length it pinned
// decided here: a length of the frame is all that reaches it, // is this widget's length less the part's pixels where the
// so what it holds for is a range on the frame and none of it // part is the whole of the box less pixels, which is the one
// on this widget's own box. // shape that inverts exactly; any other part pins this
_ => { // widget's own length.
result.frame[n] = result.frame[n].and( (Part::Of(span), false) => {
holds.extent[n] let part_len = span.len();
.through(extent.axis(axis).len()) result.extent[n] = holds.extent[n].through(part_len);
.through(frame_len), result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel {
); Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
_ => self.extent.axis(axis).len(),
});
}
// Its box is a part of this widget's frame, or a length of
// it decided here: a length of the frame is all that reaches
// it, so what it holds for is a range on the frame and none
// of it on this widget's own box.
_ => {
result.frame[n] =
result.frame[n].and(holds.extent[n].through(extent.axis(axis).len()));
}
} }
} }
result
} }
result
}
/// A child's answer as lengths of the parent's own region. A widget reports
/// a fraction of its own region, and `of` is that region as a length of this
/// one. Pixels come through untouched, being that many pixels wherever they
/// end up. A declared axis is already the parent's: it resolved the rule in
/// its own region, and the rule is what the report says.
fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis));
}
}
size
} }
/// What a widget declares a length of its box to be. `leftover` is not one: a /// What a widget declares a length of its box to be. `leftover` is not one: a
@@ -676,49 +661,58 @@ pub(crate) fn placed_extent(
placed placed
} }
/// The part of a widget's own box a `place` names, in the coordinates that /// The frame length and the box a child is asked in, in the coordinates the
/// box is in. /// widget asking draws in.
pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion {
let mut part = extent;
for axis in AXES {
*part.axis_mut(axis) = place[axis as usize].part().of(*extent.axis(axis));
}
part
}
/// The length a rule gives a child's frame, per axis: a fraction in it is a
/// fraction of the frame the child was given, which is the one length the
/// rule can mean.
pub(crate) fn narrowed_by(declared: [Option<LayoutLen>; 2]) -> [Option<Len>; 2] {
declared.map(|declared| declared.map(|len| Len::from_parts(len.rel, len.px)))
}
/// The frame a child is asked in and the box it is asked in, both in the
/// coordinates of the widget asking.
/// ///
/// `part` is what of the caller's own box the child is given. `narrow` is a /// `own` is that widget's own box, and `place` what of it the child is
/// length decided for the child's frame -- a rule, a share, a box a sibling /// given. `narrow` is a frame the container decided for the child -- a row's
/// decided -- which makes the frame the box the child is asked in: that /// slot, or padding's frame less its pixels -- and [`Part::Sized`] one a
/// length is what decided where it goes, placed in the part by the child's /// sibling's answer decided; both are window lengths, like every other
/// alignment. Where nothing narrowed it, the frame is the caller's own and /// length here, since a slot of a row is not a fraction of anything the row
/// the part is the box. /// can name. The child's declaration is a fraction of whichever reached it,
/// /// and is the only one of the three that also places the box: a box the
/// A length rather than a position, so that a child placed again is put back /// caller decided is what `place` names.
/// in whatever part it is given rather than where it first was.
pub(crate) fn frame_and_extent( pub(crate) fn frame_and_extent(
part: UiRegion, own: UiRegion,
parent_frame: UiVec2,
place: [Place; 2],
narrow: [Option<Len>; 2], narrow: [Option<Len>; 2],
declared: [Option<LayoutLen>; 2],
align: RegionAlign, align: RegionAlign,
) -> (UiRegion, UiRegion) { ) -> (UiVec2, UiRegion) {
let mut frame = UiRegion::FULL; let part = part_of(own, place, align);
let mut frame = parent_frame;
let mut extent = part; let mut extent = part;
for (axis, narrow) in AXES.into_iter().zip(narrow) { for axis in AXES {
if let Some(len) = narrow { let n = axis as usize;
let sized = match place[n].part() {
Part::Sized(len) => Some(len),
_ => None,
};
let base = sized
.or(narrow[n])
.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 slot = part.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel()); let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*frame.axis_mut(axis) = UiSpan::new(start, start + len); *extent.axis_mut(axis) = UiSpan::new(start, start + len);
*extent.axis_mut(axis) = UiSpan::FULL;
} }
} }
(frame, extent) (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
}
+12 -2
View File
@@ -1,4 +1,4 @@
use crate::{PrimitiveHandle, UiRegion, UiSpan}; use crate::{AxisAlign, Len, PrimitiveHandle, UiRegion, UiSpan};
/// What of a widget's own box a child is given, along one axis. /// What of a widget's own box a child is given, along one axis.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@@ -16,15 +16,25 @@ pub enum Part {
/// container read its own box -- and a box chosen from its own answer /// container read its own box -- and a box chosen from its own answer
/// then feeds back into the answer. /// then feeds back into the answer.
Of(UiSpan), Of(UiSpan),
/// 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),
} }
impl Part { impl Part {
/// Where it lands in the coordinates `extent` is in. /// Where it lands in the coordinates `extent` is in.
pub(crate) fn of(self, extent: UiSpan) -> UiSpan { pub(crate) fn of(self, extent: UiSpan, align: AxisAlign) -> UiSpan {
match self { match self {
Self::All => extent, Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end), Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Of(span) => span.within(&extent), Self::Of(span) => span.within(&extent),
Self::Sized(len) => {
let start = extent.start + (extent.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
} }
} }
} }
+172 -204
View File
@@ -1,10 +1,10 @@
#[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, narrowed_by, part_of, placed_extent}; 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, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
Moves, Painter, Part, PixelRegion, Place, PxVec2, Size, StrongWidget, UiRegion, UiRsc, Weight, Moves, Painter, Part, PixelRegion, Place, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc,
WidgetId, Widgets, UiSpan, UiVec2, Weight, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -20,28 +20,23 @@ pub(super) struct DrawInfo {
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
pub region_node: bool, pub region_node: bool,
pub mask: MaskIdx, pub mask: MaskIdx,
/// The frame in the parent widget's frame coordinates, before /// What a fraction declared or reported under this widget is a fraction
/// composition. Its length is the same on every ask of the widget. /// of, as a length of the window.
pub frame: UiRegion, pub frame: UiVec2,
/// That frame composed into `parent_move`'s coordinates, which is what /// The box the widget is asked in, in its parent region node's
/// the widget's own drawing is written within. /// coordinates.
pub frame_abs: UiRegion,
/// The box the widget is asked in, in the frame's own coordinates: the
/// part of the parent's own box that `place` names, before the widget's
/// answer is placed inside it.
pub part: UiRegion, pub part: 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 [`Place`]. The two are one ask's place until the
/// parent places the answer somewhere else. /// parent places the answer somewhere else.
pub place: [Place; 2], pub place: [Place; 2],
pub offer_place: [Place; 2], pub offer_place: [Place; 2],
/// The length the frame was narrowed to on each axis, as a length of /// A frame the parent decided for it on each axis, as a length of the
/// the parent's frame, where anything narrowed it. /// window, which the widget's own declaration is a fraction of.
pub narrow: [Option<Len>; 2], 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: one multiply from the parent's own, which is /// The frame in pixels, resolved once against the window.
/// where every pixel length in layout comes from.
pub px: PxVec2, pub px: PxVec2,
} }
@@ -58,10 +53,8 @@ impl DrawInfo {
pub(super) struct Placing { pub(super) struct Placing {
pub id: WidgetId, pub id: WidgetId,
pub extent: UiRegion, pub extent: UiRegion,
/// The widget's frame in the coordinates its children compose within: pub frame: UiVec2,
/// `FULL` where it is a region node, since its box is that node. pub window: PxVec2,
pub local: UiRegion,
pub px: PxVec2,
pub depth: usize, pub depth: usize,
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
pub mask: MaskIdx, pub mask: MaskIdx,
@@ -126,23 +119,20 @@ impl UiRenderState {
self.resized = true; self.resized = true;
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| {
let px = active.frame.size().to_px(size);
// 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. // asked about is the whole of its frame.
active.answers_at(px, active.offer_part) && active.holds.contains(px, active.offer_part) active.answers_at(size, active.offer_part)
&& active.holds.contains(size, active.frame, active.offer_part)
}); });
if !stands { if !stands {
widgets.needs_redraw.insert(root); widgets.needs_redraw.insert(root);
} }
} }
/// The root is asked about in the output: the window is where a fraction /// The root is asked about in the output. Its own rules narrow both its
/// becomes pixels rather than a box of its own, so the root's box is the /// frame and box; nothing above it chose a different one.
/// first length threaded down. Its own rules narrow that box, and where fn root_info(&self, frame: UiVec2, extent: UiRegion) -> DrawInfo {
/// they do the narrowed box is also the offer -- nothing above it chose let px = frame.to_px(self.output_size);
/// anything else.
fn root_info(&self, region: UiRegion) -> DrawInfo {
let px = region.size().to_px(self.output_size);
DrawInfo { DrawInfo {
layer: 0, layer: 0,
parent: None, parent: None,
@@ -150,9 +140,8 @@ impl UiRenderState {
parent_move: MoveIdx::NONE, parent_move: MoveIdx::NONE,
region_node: false, region_node: false,
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
frame: region, frame,
frame_abs: region, part: extent,
part: UiRegion::FULL,
place: [Place::Within(Part::All); 2], place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2],
narrow: [None; 2], narrow: [None; 2],
@@ -203,17 +192,24 @@ 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 region = Self::root_region(id.id(), rsc.widgets()); let (frame, extent) = Self::root_layout(id.id(), rsc.widgets());
let info = self.root_info(region); let info = self.root_info(frame, extent);
self.draw_inner(id.id(), info, None, rsc); self.draw_inner(id.id(), info, None, rsc);
} }
} }
/// The root's frame: the window, narrowed by the root's own rules. Its /// The root's frame and box: the window, taken in by the root's own
/// extent is that frame, since nothing above it chose anything else. /// rules. Nothing above it narrowed anything or chose where it goes, so
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { /// its declaration is the whole of what decides either.
let narrow = narrowed_by(declared_lens(widgets, id)); fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
frame_and_extent(UiRegion::FULL, narrow, widgets.alignment(id)).0 frame_and_extent(
UiRegion::FULL,
UiVec2::FULL_SIZE,
[Place::Within(Part::All); 2],
[None; 2],
declared_lens(widgets, id),
widgets.alignment(id),
)
} }
pub(super) fn draw_inner( pub(super) fn draw_inner(
@@ -223,11 +219,11 @@ 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 (frame, part) = (info.frame_abs, info.part); let part = info.part;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::DrawRequests); diag::bump(Counter::DrawRequests);
diag::draw_request(id, info.parent, frame, info.px, info.region_node); diag::draw_request(id, info.parent, part, 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 = declared_lens(rsc.widgets(), id);
@@ -245,8 +241,7 @@ impl UiRenderState {
.flatten() .flatten()
.and_then(|answer| { .and_then(|answer| {
let extent = placed_extent(part, answer.0, declared, info.fill(), align); let extent = placed_extent(part, answer.0, declared, info.fill(), align);
self.try_reuse(id, frame, part, extent, info, rsc) self.try_reuse(id, part, extent, info, rsc).map(|()| answer)
.map(|()| answer)
}); });
let answer = reused.unwrap_or_else(|| { let answer = reused.unwrap_or_else(|| {
if old.is_none() { if old.is_none() {
@@ -258,7 +253,7 @@ impl UiRenderState {
// open. // open.
let extent = placed_extent(part, answer.0, declared, info.fill(), align); let extent = placed_extent(part, answer.0, declared, info.fill(), align);
if extent != part { if extent != part {
self.reposition(id, frame, extent, info, rsc); self.relocate(id, extent, info, rsc);
} }
answer answer
}); });
@@ -268,7 +263,6 @@ impl UiRenderState {
// Whoever asked owns how the boxes were reached: the frame it stated, // Whoever asked owns how the boxes were reached: the frame 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_abs = frame;
active.frame = info.frame; active.frame = info.frame;
active.narrow = info.narrow; active.narrow = info.narrow;
active.re_asked = info.re_asked; active.re_asked = info.re_asked;
@@ -301,29 +295,30 @@ impl UiRenderState {
old: Option<ActiveData>, old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) { ) -> (Size, LayoutHolds) {
let frame = info.frame_abs; let frame = info.frame;
let (move_idx, local, retired_move) = match info.region_node { let (move_idx, extent, retired_move) = match info.region_node {
// Its box becomes its movable region, so it draws in that // A node entry is only a translation. Its local box keeps the
// region's coordinates and its box is one entry to rewrite. // same window-unit length as the box in its parent's node.
true => ( true => (
self.move_slot(id, info.parent_move, frame), self.move_slot(id, info.parent_move, translation(extent)),
UiRegion::FULL, local_region(extent),
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, frame, self.slots.remove(&id)), false => (info.parent_move, extent, self.slots.remove(&id)),
}; };
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;
let window = self.output_size;
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
frame: local, frame,
extent, extent,
extent_len: [None; 2], extent_len: [None; 2],
px, window,
mask: info.mask, mask: info.mask,
layer: info.layer, layer: info.layer,
own_layer: info.layer, own_layer: info.layer,
@@ -334,6 +329,7 @@ impl UiRenderState {
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
frame_own: [Holds::ANY; 2], frame_own: [Holds::ANY; 2],
frame_own_len: [None; 2],
under: Vec::new(), under: Vec::new(),
extent_own: [Holds::ANY; 2], extent_own: [Holds::ANY; 2],
answer_under: LayoutHolds::ANY, answer_under: LayoutHolds::ANY,
@@ -358,7 +354,7 @@ impl UiRenderState {
rsc: _, rsc: _,
frame: _, frame: _,
extent: _, extent: _,
px: _, window: _,
mask, mask,
textures, textures,
primitives, primitives,
@@ -369,6 +365,7 @@ impl UiRenderState {
children, children,
size_deps, size_deps,
frame_own, frame_own,
frame_own_len,
under, under,
move_idx, move_idx,
layer, layer,
@@ -384,11 +381,23 @@ 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. // 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
// resolved into the frame when the child was asked, and resolving it
// 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() {
None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.frame.axis(axis).rel,
px: info.frame.axis(axis).px,
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.frame.axis(axis)),
};
let size = Size { let size = Size {
x: rules.x.apply(size.x), x: ruled(Axis::X, size.x),
y: rules.y.apply(size.y), y: ruled(Axis::Y, size.y),
}; };
// A widget that clipped its contents to its box drew nothing outside // A widget that clipped its contents to its box drew nothing outside
// it, so reporting more than the box asks to be placed at a length it // it, so reporting more than the box asks to be placed at a length it
@@ -396,7 +405,10 @@ impl UiRenderState {
// Overflowing is otherwise ordinary: a text too tall for the box it // Overflowing is otherwise ordinary: a text too tall for the box it
// was offered reports the height it needs. // was offered reports the height it needs.
debug_assert!( debug_assert!(
mask == info.mask || AXES.into_iter().all(|axis| within_box(size, px, axis)), mask == info.mask
|| AXES
.into_iter()
.all(|axis| within_box(size, extent, 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),
); );
@@ -408,8 +420,23 @@ impl UiRenderState {
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
// frame's own length, so the answer is that frame's and not just
// that many pixels of this window -- the same pin a widget that read
// its frame took for its drawing.
let frame_len = AXES.map(|axis| {
let fraction = rules
.axis(axis)
.exact()
.is_some_and(|len| len.rel != Rel::ZERO);
match fraction {
true => Some(info.frame.axis(axis)),
false => frame_own_len[axis as usize],
}
});
let own_holds = LayoutHolds { let own_holds = LayoutHolds {
frame: frame_own, frame: frame_own,
frame_len,
extent: extent_own, extent: extent_own,
extent_len, extent_len,
}; };
@@ -418,7 +445,7 @@ impl UiRenderState {
.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(px, extent), holds.contains(self.output_size, info.frame, extent),
"'{}' ({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),
); );
@@ -436,8 +463,7 @@ impl UiRenderState {
parent_move: move_idx, parent_move: move_idx,
region_node: false, region_node: false,
mask, mask,
frame: UiRegion::FULL, frame: UiVec2::FULL_SIZE,
frame_abs: UiRegion::FULL,
part: UiRegion::FULL, part: UiRegion::FULL,
place: [Place::Within(Part::All); 2], place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2],
@@ -453,7 +479,6 @@ impl UiRenderState {
let active = ActiveData { let active = ActiveData {
id, id,
frame_abs: frame,
extent, extent,
frame: info.frame, frame: info.frame,
narrow: info.narrow, narrow: info.narrow,
@@ -525,26 +550,10 @@ impl UiRenderState {
return None; return None;
} }
let answer = active.answer?; let answer = active.answer?;
answer.1.contains(info.px, part).then_some(answer) answer
} .1
.contains(self.output_size, info.frame, part)
/// The pixel lengths of a widget's frame, which is what a local redraw .then_some(answer)
/// needs to ask the question its parent asked.
///
/// It is threaded down from the window a length of a box at a time, and
/// this takes the same steps back up: a widget's frame is a length of its
/// parent's frame, and that chain has no coordinate frame in it, so a
/// region node cannot break it -- and it lands on the number a cold
/// layout computes rather than near it.
fn asked_px(&self, id: WidgetId) -> PxVec2 {
let active = &self.active[&id];
// Nothing above the root: the window is where a fraction becomes
// pixels, which is also the whole of the frame the root is given.
let parent_px = match active.parent.and_then(|p| self.active.get(&p)) {
Some(parent) => self.asked_px(parent.id),
None => self.output_size,
};
active.frame.size().to_px(parent_px)
} }
/// Keeps the retained drawing if its contract holds for `part`, the box /// Keeps the retained drawing if its contract holds for `part`, the box
@@ -552,7 +561,6 @@ impl UiRenderState {
fn try_reuse( fn try_reuse(
&mut self, &mut self,
id: WidgetId, id: WidgetId,
frame: UiRegion,
part: UiRegion, part: UiRegion,
extent: UiRegion, extent: UiRegion,
info: DrawInfo, info: DrawInfo,
@@ -603,10 +611,10 @@ impl UiRenderState {
} }
return None; return None;
} }
// In pixels, because the frame is a fraction of its parent's and // In pixels, because the box is a fraction of the window and that
// that may be what changed -- an unchanged fraction of a box 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(info.px, part) { if !active.holds.contains(self.output_size, info.frame, part) {
#[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 frame that redraws more
@@ -618,10 +626,14 @@ impl UiRenderState {
if holds.extent_len[n].is_some_and(|pinned| pinned != part.axis(axis).len()) { if holds.extent_len[n].is_some_and(|pinned| pinned != part.axis(axis).len()) {
diag::bump(Counter::OutsidePinnedLen); diag::bump(Counter::OutsidePinnedLen);
} }
if !holds.frame[n].contains(info.px.axis(axis)) { if !holds.frame[n].contains(self.output_size.axis(axis))
|| holds.frame_len[n].is_some_and(|pinned| pinned != info.frame.axis(axis))
{
diag::bump(Counter::OutsideFrame); diag::bump(Counter::OutsideFrame);
} }
if !holds.extent[n].contains(part.axis(axis).len().to_px(info.px.axis(axis))) { if !holds.extent[n]
.contains(part.axis(axis).len().to_px(self.output_size.axis(axis)))
{
diag::bump(Counter::OutsideExtent); diag::bump(Counter::OutsideExtent);
} }
} }
@@ -630,21 +642,14 @@ impl UiRenderState {
} }
return None; return None;
} }
self.relocate(id, frame, extent, info, rsc); self.relocate(id, extent, 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 moved frame recomposes the subtree or rewrites its node, and a box /// a widget with a node of its own writes that node's translation, and
/// of another length re-expresses everything inside it. /// one without re-expresses its own drawing and everything inside it.
fn relocate( fn relocate(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
&mut self,
id: WidgetId,
frame: UiRegion,
extent: 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),
@@ -652,22 +657,20 @@ impl UiRenderState {
rsc.widgets().label(id) rsc.widgets().label(id)
); );
let has_region_node = active.move_idx != active.parent_move; let has_region_node = active.move_idx != active.parent_move;
let extent_moved = active.extent != extent; let local = match has_region_node {
let moved = active.frame_abs != frame; true => local_region(extent),
false => extent,
};
let moved = active.extent != local;
let slot = active.move_idx; let slot = active.move_idx;
if moved { if has_region_node {
if has_region_node { self.moves.set(slot, translation(extent));
self.moves.set(slot, frame);
} else {
self.recompose_subtree(id, frame, info.parent_move, rsc);
}
} }
if extent_moved { if moved {
self.reposition(id, frame, extent, 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_abs = frame;
active.frame = info.frame; active.frame = info.frame;
active.place = info.place; active.place = info.place;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -709,7 +712,7 @@ impl UiRenderState {
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) { ) {
let active = &self.active[&child]; let active = &self.active[&child];
let (frame, part) = Self::re_ask(active, at.extent, place); let (frame, part) = Self::ask_again(active, at, place);
let extent = placed_extent( let extent = placed_extent(
part, part,
active.measured().unwrap_or(active.size), active.measured().unwrap_or(active.size),
@@ -725,63 +728,50 @@ impl UiRenderState {
region_node: active.move_idx != active.parent_move, region_node: active.move_idx != active.parent_move,
mask: at.mask, mask: at.mask,
frame, frame,
frame_abs: frame.within(&at.local),
part, part,
place, place,
offer_place: active.offer_place, offer_place: active.offer_place,
narrow: active.narrow, narrow: active.narrow,
re_asked: active.re_asked, re_asked: active.re_asked,
px: frame.size().to_px(at.px), px: frame.to_px(at.window),
}; };
self.relocate(child, info.frame_abs, extent, info, rsc); self.relocate(child, extent, info, rsc);
} }
/// The frame and the box a widget is given at `place` of its parent's /// The frame and the box a widget already drawn is given at `place` of
/// box, from what it already has. A frame's length is the same on every /// the box its parent is being taken as. What narrowed its frame and what
/// ask, so a narrowed frame is put back where it sits in the part rather /// it declared are its own record's, so both are resolved against that
/// than resolved a second time. /// parent's frame again exactly as the first ask resolved them.
fn re_ask( fn ask_again(active: &ActiveData, at: &Placing, place: [Place; 2]) -> (UiVec2, UiRegion) {
active: &ActiveData, frame_and_extent(
parent_box: UiRegion, at.extent,
place: [Place; 2], at.frame,
) -> (UiRegion, UiRegion) { place,
frame_and_extent(part_of(parent_box, place), active.narrow, active.own_align) 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( fn reposition(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
&mut self,
id: WidgetId,
frame: UiRegion,
extent: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.frame_abs = frame;
active.extent = extent; active.extent = extent;
let local = if info.region_node {
UiRegion::FULL
} else {
frame
};
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) = *self.layers[handle.layer].region_mut(handle) = primitive.region.within(&extent);
primitive.region.within(&extent).within(&local);
} }
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 = rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&extent);
mask_region.within(&extent).within(&local);
} }
let at = Placing { let at = Placing {
id, id,
extent, extent,
local, frame: info.frame,
px: info.px, window: self.output_size,
depth: info.depth, depth: info.depth,
move_idx: active.move_idx, move_idx: active.move_idx,
mask: active.mask, mask: active.mask,
@@ -811,38 +801,6 @@ impl UiRenderState {
} }
} }
/// Replays the original local compositions, including their rounding order.
/// A region node terminates the walk because its contents name its slot.
fn recompose_subtree(
&mut self,
id: WidgetId,
frame: UiRegion,
parent_move: MoveIdx,
rsc: &mut dyn UiRsc,
) {
let active = self.active.get_mut(&id).unwrap();
active.frame_abs = frame;
if active.move_idx != parent_move {
self.moves.set(active.move_idx, frame);
return;
}
let extent = active.extent;
for primitive in &active.primitives {
let handle = &primitive.handle;
*self.layers[handle.layer].region_mut(handle) =
primitive.region.within(&extent).within(&frame);
}
if let Some(local) = active.mask_region {
rsc.ui_mut().masks.get_mut(active.mask).region = local.within(&extent).within(&frame);
}
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
let local = self.active[&child].frame;
self.recompose_subtree(child, local.within(&frame), parent_move, rsc);
}
}
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool { fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
let Some(widget) = rsc.widgets().get_dyn(id) else { let Some(widget) = rsc.widgets().get_dyn(id) else {
return true; return true;
@@ -913,9 +871,8 @@ impl UiRenderState {
id, id,
ActiveData { ActiveData {
id, id,
frame_abs: UiRegion::FULL,
extent: UiRegion::FULL, extent: UiRegion::FULL,
frame: UiRegion::FULL, frame: UiVec2::FULL_SIZE,
narrow: [None; 2], narrow: [None; 2],
place: [Place::Within(Part::All); 2], place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2],
@@ -1102,9 +1059,8 @@ impl UiRenderState {
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> { pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?; let active = self.active.get(&id.id())?;
active.drawn.then(|| { active.drawn.then(|| {
let placed = active.extent.within(&active.frame_abs);
self.moves self.moves
.resolve(active.parent_move, placed) .resolve(active.move_idx, active.extent)
.to_px(self.output_size) .to_px(self.output_size)
}) })
} }
@@ -1146,10 +1102,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 region = Self::root_region(id, rsc.widgets()); let (frame, extent) = Self::root_layout(id, rsc.widgets());
let info = DrawInfo { let info = DrawInfo {
mask: active.parent_mask, mask: active.parent_mask,
..self.root_info(region) ..self.root_info(frame, extent)
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
@@ -1157,7 +1113,6 @@ impl UiRenderState {
self.draw_inner(id, info, old, rsc); self.draw_inner(id, info, old, rsc);
return true; return true;
}; };
let px = self.asked_px(id);
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.place); let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.place);
// The question its parent asked, asked again: the same place of the // The question its parent asked, asked again: the same place of the
// box the parent was asked in, which is the box the parent's own // box the parent was asked in, which is the box the parent's own
@@ -1165,13 +1120,7 @@ impl UiRenderState {
// 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 asked = self.placing_of(parent, self.active[&parent].offer_part); let asked = self.placing_of(parent, self.active[&parent].offer_part);
let (frame, part) = Self::re_ask(active, asked.extent, active.offer_place); let (frame, part) = Self::ask_again(active, &asked, active.offer_place);
debug_assert_eq!(
frame.size(),
active.frame.size(),
"'{}' ({id:?}) asked again in a frame of another length",
rsc.widgets().label(id)
);
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: active.parent, parent: active.parent,
@@ -1180,13 +1129,12 @@ impl UiRenderState {
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, frame,
frame_abs: frame.within(&asked.local),
part, part,
place: active.offer_place, place: active.offer_place,
offer_place: active.offer_place, offer_place: active.offer_place,
narrow: active.narrow, narrow: active.narrow,
re_asked: false, re_asked: false,
px, px: frame.to_px(self.output_size),
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
@@ -1202,7 +1150,9 @@ impl UiRenderState {
{ {
active.answer = was_answer; active.answer = was_answer;
} }
if active.holds.covers(was_holds) && was_holds.contains(px, active.extent) { if active.holds.covers(was_holds)
&& was_holds.contains(self.output_size, active.frame, active.extent)
{
active.holds = was_holds; active.holds = was_holds;
} }
if active.answer != was_answer || active.holds != was_holds { if active.answer != was_answer || active.holds != was_holds {
@@ -1232,11 +1182,8 @@ impl UiRenderState {
Placing { Placing {
id, id,
extent, extent,
local: match active.move_idx != active.parent_move { frame: active.frame,
true => UiRegion::FULL, window: self.output_size,
false => active.frame_abs,
},
px: self.asked_px(id),
depth: active.depth, depth: active.depth,
move_idx: active.move_idx, move_idx: active.move_idx,
mask: active.mask, mask: active.mask,
@@ -1245,12 +1192,33 @@ impl UiRenderState {
} }
/// Whether what a widget reports along `axis` is inside the box it drew in. /// Whether what a widget reports along `axis` is inside the box it drew in.
/// A share is a length only to whoever divides one, so it is not a claim /// Both are lengths of the window, so the comparison is in its pixels. A
/// about this box and cannot exceed it. /// share is a length only to whoever divides one, so it is not a claim about
fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool { /// this box and cannot exceed it.
fn within_box(size: Size, extent: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = size.axis(axis); let len = size.axis(axis);
let box_len = px.axis(axis); let window = window.axis(axis);
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len len.leftover != Weight::ZERO
|| Len::from_parts(len.rel, len.px).to_px(window) <= extent.axis(axis).len().to_px(window)
}
/// A box in a fresh region node keeps its window-unit length and starts at
/// that node's origin.
fn local_region(region: UiRegion) -> UiRegion {
let size = region.size();
UiRegion::new(
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
/// the box start composes as that translation in both the CPU and shader.
fn translation(region: UiRegion) -> UiRegion {
UiRegion {
x: UiSpan::new(region.x.start, region.x.start + Len::FULL),
y: UiSpan::new(region.y.start, region.y.start + Len::FULL),
}
} }
impl Default for UiRenderState { impl Default for UiRenderState {
+3 -1
View File
@@ -122,7 +122,9 @@ impl Widget for Branch {
let measured = painter let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X); .len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let px = measured
.apply_leftover()
.to_px(painter.window_px_len(Axis::X));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
let place = [Place::Within(Part::All), below]; let place = [Place::Within(Part::All), below];
+15 -7
View File
@@ -14,12 +14,11 @@ 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.
// //
// The padding goes around what it pads: the frame passes through, so // Padding is an inset of both: it comes off the frame, so `rel(1)`
// the inner's fractions mean what they would without it, and only // under it fills this widget rather than overflowing it by the
// the box it draws in is moved in by the pixels. Said as a part of // padding, and it comes off the box, so what is drawn sits inside.
// this widget's own box in that box's own lengths, so nothing here // The two stay distinct -- the box can be narrower still, where a row
// reads how long the box is -- and a box chosen from this widget's // asked this widget in the room left, and a text wraps at that.
// own answer therefore does not feed back into that answer.
let inset = |lead: Px, trail: Px| { let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new( Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead), Len::from_parts(Rel::ZERO, lead),
@@ -30,7 +29,16 @@ impl Widget for Pad {
inset(self.padding.left, self.padding.right), inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom), inset(self.padding.top, self.padding.bottom),
]; ];
let inner = painter.widget_at(&self.inner, [None; 2], place).size(); // 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,
+2 -1
View File
@@ -16,7 +16,8 @@ impl Widget for Scroll {
let answer_len = painter let answer_len = painter
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2]) .widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2])
.len(self.axis); .len(self.axis);
let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len); let fixed =
Len::from_parts(answer_len.rel, answer_len.px).to_px(painter.window_px_len(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);
+2 -2
View File
@@ -68,12 +68,12 @@ impl Widget for Span {
// exist at all turns on this. // exist at all turns on this.
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
shares = room.to_px(painter.frame_px_len(axis)) > Px::ZERO; shares = room.to_px(painter.window_px_len(axis)) > Px::ZERO;
let holds = match shares { let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX), true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO), false => Holds::from(Px::MIN..=Px::ZERO),
}; };
painter.frame_holds(axis, holds.through(room)); painter.window_holds(axis, holds.through(room));
} }
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
+6 -3
View File
@@ -33,16 +33,19 @@ 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 narrow = [Axis::X, Axis::Y].map(|axis| { let place = [Axis::X, Axis::Y].map(|axis| {
let len = size.axis(axis); let len = size.axis(axis);
(len.leftover == Weight::ZERO).then(|| Len::from_parts(len.rel, len.px)) match len.leftover == Weight::ZERO {
true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))),
false => Place::Within(Part::All),
}
}); });
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) { if sizing == Some(i) {
continue; continue;
} }
painter.child_layer_at(i); painter.child_layer_at(i);
painter.widget_at(child, narrow, [Place::Within(Part::All); 2]); painter.widget_at(child, [None; 2], place);
} }
size size
} }
+3 -1
View File
@@ -26,7 +26,9 @@ impl Widget for BranchesOnMeasurement {
let measured = painter let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top]) .widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X); .len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let px = measured
.apply_leftover()
.to_px(painter.window_px_len(Axis::X));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
let place = [Place::Within(Part::All), below]; let place = [Place::Within(Part::All), below];
+90 -9
View File
@@ -83,10 +83,8 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
assert!(crowded > whole_row, "{crowded} against {whole_row}"); assert!(crowded > whole_row, "{crowded} against {whole_row}");
} }
/// The same reading through a pad: padding goes around what it pads and /// Padding is an inset: it narrows the frame a fraction resolves against and
/// does not narrow what a fraction under it is a fraction of, so half of the /// adds itself back to the padded widget's reported length.
/// window plus the padding is what the pad takes and where the next child
/// starts.
#[test] #[test]
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() { fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
let mut h = Harness::new((400, 100)); let mut h = Harness::new((400, 100));
@@ -97,9 +95,83 @@ fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
// placed inside it by its own alignment, which is not what is under test. // placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0))); h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inner, (10, 10), (210, 90)); assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (220, 100)); assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (220, 0), (320, 100)); assert_corners!(h, tail, (210, 0), (310, 100));
}
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.";
/// The worked example of what padding insets: in a 900 px row after a 24 px
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
/// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there.
#[test]
fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200));
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 padded = fill.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let fill_width = h.region(&fill).unwrap().size().x;
assert_eq!(fill_width, Px::from_int(868));
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
let asked = active.offer_part.x.len().to_px(window);
assert_eq!(active.frame.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844));
}
/// The other way round: a share inside padding. A slot is a length of the
/// row, which is already the padded width, so what the span decided reaches
/// the child as it stands -- taking the padding off a second time would make
/// `rel(1.0)` in the slot shorter than the slot.
#[test]
fn a_share_inside_padding_fills_the_slot_it_was_given() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let first = Span {
children: vec![fill.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.width(leftover(1))
.add(&mut h.rsc);
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row.pad(16));
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
}
/// 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.
#[test]
fn padding_narrows_both_frame_and_box_inside_a_share() {
let mut h = Harness::new((900, 200));
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 other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
let mut h = Harness::new((900, 200));
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
assert_eq!(active.frame.x.to_px(window), Px::from_int(418));
assert_eq!(active.offer_part.x.len().to_px(window), Px::from_int(418));
} }
#[test] #[test]
@@ -428,8 +500,7 @@ 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 drawn = active.extent.within(&active.frame_abs); let region = h.render.moves.resolve(active.move_idx, active.extent);
let region = h.render.moves.resolve(active.parent_move, drawn);
let dim = h.size().axis(axis); let dim = h.size().axis(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());
@@ -740,3 +811,13 @@ fn a_fixed_child_is_centered_in_its_wrappers_share() {
assert_corners!(h, wrapper, (200, 0), (900, 400)); assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250)); assert_corners!(h, leaf, (500, 150), (600, 250));
} }
/// The root's frame is the window and its rule is a fraction of that, which
/// is one resolution and not two: nothing above it narrowed anything.
#[test]
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
let mut h = Harness::new((900, 200));
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
}
+96
View File
@@ -733,6 +733,51 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \ const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
box leaves room for, so a paragraph's height is an answer and not a setting."; box leaves room for, so a paragraph's height is an answer and not a setting.";
fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec<WidgetId>, WidgetId) {
let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None);
if fixed {
h.rsc.widgets_mut().set_size_rules(
sizing.id(),
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
}
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let pad = Pad {
padding: Padding::ZERO,
inner: text.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.set_root(stack);
(
vec![sizing.id(), text.id(), pad.id(), stack.id()],
sizing.id(),
)
}
#[test]
fn fixing_a_stacks_sizing_child_repositions_its_overlay() {
let mut warm = Harness::new((900, 1200));
let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false);
warm.frame();
warm.rsc.widgets_mut().set_size_rules(
sizing,
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4, /// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
/// `shuffle-swap-for-three`). The stack takes its size from the span above, /// `shuffle-swap-for-three`). The stack takes its size from the span above,
/// the span takes its width from the longest line of the texts in it, and /// the span takes its width from the longest line of the texts in it, and
@@ -927,3 +972,54 @@ fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
assert_same_regions(&warm, &ids, &cold, &cold_ids); assert_same_regions(&warm, &ids, &cold, &cold_ids);
} }
/// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column
/// divides the box it is given between two shares, so its drawing holds for
/// that box's length alone, and the pads above it pass that dependency up:
/// each one's box is a part of the box it was asked in. Padding narrowing
/// the frame it hands down does not change that, and while it was taken to,
/// changing the rule over the pads relocated the column's drawing into the
/// new box instead of dividing it again.
fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec<WidgetId> {
let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
let bottom = rect(Color::RED).add(&mut h.rsc);
let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
let inner = Pad {
padding: Padding::ZERO,
inner: column.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let outer = Pad {
padding: Padding::ZERO,
inner: inner.add_strong(&mut h.rsc),
}
.height(height)
.add(&mut h.rsc);
let beside = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((outer, beside).span(Dir::RIGHT));
vec![
top.id(),
bottom.id(),
column.id(),
inner.id(),
outer.id(),
beside.id(),
]
}
#[test]
fn changing_a_rule_over_two_pads_divides_the_column_again() {
let mut warm = Harness::new((900, 1200));
let ids = plant_two_shares_under_two_pads(&mut warm, 88.0);
warm.frame();
warm.rsc
.widgets_mut()
.set_size_rules(ids[4], None, Some(LayoutLen::px(105)));
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
+60 -4
View File
@@ -103,6 +103,11 @@ pub enum Case {
/// A resize and then a size change, so a retained answer is asked to /// A resize and then a size change, so a retained answer is asked to
/// survive two different kinds of invalidation in a row. /// survive two different kinds of invalidation in a row.
ResizeSize, ResizeSize,
/// A size change and then a resize, which is the other order and not the
/// same test: a length answered as a fraction of one box and kept as a
/// fraction of another agrees at the size it was changed at and parts
/// from it at every other one.
SizeResize,
/// A few declared sizes. /// A few declared sizes.
Size, Size,
/// Every declared size at once, so every reader of a size has a changed /// Every declared size at once, so every reader of a size has a changed
@@ -119,12 +124,13 @@ pub enum Case {
Shuffle(Shuffle), Shuffle(Shuffle),
} }
pub const ALL: [Case; 15] = [ pub const ALL: [Case; 16] = [
Case::Repaint, Case::Repaint,
Case::RepaintSome, Case::RepaintSome,
Case::Resize, Case::Resize,
Case::ResizeRepaint, Case::ResizeRepaint,
Case::ResizeSize, Case::ResizeSize,
Case::SizeResize,
Case::Size, Case::Size,
Case::EverySize, Case::EverySize,
Case::Align, Case::Align,
@@ -146,6 +152,7 @@ impl Case {
Self::Resize => "resize", Self::Resize => "resize",
Self::ResizeRepaint => "resize-repaint", Self::ResizeRepaint => "resize-repaint",
Self::ResizeSize => "resize-size", Self::ResizeSize => "resize-size",
Self::SizeResize => "size-resize",
Self::Size => "size", Self::Size => "size",
Self::EverySize => "every-size", Self::EverySize => "every-size",
Self::Align => "align", Self::Align => "align",
@@ -170,6 +177,15 @@ impl Case {
_ => (STILL, STILL), _ => (STILL, STILL),
} }
} }
/// The window the warm tree is taken to after the change, where the case
/// is about what the change left behind rather than about the change.
fn then_resize(self) -> Option<(f32, f32)> {
match self {
Self::SizeResize => Some(INNER),
_ => None,
}
}
} }
fn mark(warm: &mut Harness, tree: &Tree, step: usize) { fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
@@ -284,7 +300,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
warm.frame(); warm.frame();
return out; return out;
} }
Case::Size | Case::ResizeSize => Edits { Case::Size | Case::ResizeSize | Case::SizeResize => Edits {
sizes: some_sizes(warm, tree, rng), sizes: some_sizes(warm, tree, rng),
..Default::default() ..Default::default()
}, },
@@ -384,6 +400,17 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label label
} }
/// One widget's layout as it stands: the frame its fractions resolved
/// against, the box it was asked in, the box its drawing went in, and what
/// it reported. In window units, which is what both trees are in.
fn record(id: WidgetId, h: &Harness) -> String {
let active = &h.render.active[&id];
format!(
"frame {} ask {} box {} size {}",
active.frame, active.offer_part, active.extent, active.size,
)
}
/// Runs `case` on the tree `plan` describes, warm and cold, and says where /// Runs `case` on the tree `plan` describes, warm and cold, and says where
/// the two disagree. `seed` chooses only the values a case picks at random, /// the two disagree. `seed` chooses only the values a case picks at random,
/// so one plan under one case is one comparison however it was reached. /// so one plan under one case is one comparison however it was reached.
@@ -400,12 +427,29 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
warm.frame(); warm.frame();
} }
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed)); let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
// Whatever the change left, seen at another window: an answer kept as a
// fraction of the wrong length is the same number of pixels where it was
// made and a different one everywhere else.
let end = match case.then_resize() {
Some(after) => {
warm.resize(after);
warm.frame();
after
}
None => end,
};
let mut cold = Harness::new(end); let mut cold = Harness::new(end);
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan); let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
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));
@@ -416,6 +460,7 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
// 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();
let mut records = Vec::new();
let mut at = Some(w); let mut at = Some(w);
while let Some(id) = at { while let Some(id) = at {
let active = &warm.render.active[&id]; let active = &warm.render.active[&id];
@@ -424,11 +469,22 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
false => "*", false => "*",
}; };
chain.push(format!("{}{node}", describe(id, &warm))); chain.push(format!("{}{node}", describe(id, &warm)));
// What each level was asked in on both sides, since the level
// where the two stop agreeing is the one to look at rather than
// the leaf that reported the difference.
let cold_id = places.get(&id).and_then(|&i| cold_tree.ids.get(i));
records.push(format!(
" {}\n warm {}\n cold {}",
describe(id, &warm),
record(id, &warm),
cold_id.map_or("-".into(), |&id| record(id, &cold)),
));
at = active.parent; at = active.parent;
} }
return Some(format!( return Some(format!(
"widget {i}\n warm {got:?}\n cold {want:?}\n {}", "widget {i}\n warm {got:?}\n cold {want:?}\n {}\n{}",
chain.join(" < ") chain.join(" < "),
records.join("\n"),
)); ));
} }
match drawn { match drawn {
+6 -2
View File
@@ -70,10 +70,14 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default()); let grown = plan(seed, depth, &Edits::default());
for &case in &cases { for &case in &cases {
let Some(how) = diverges(&grown, case, seed) else { if diverges(&grown, case, seed).is_none() {
continue; continue;
}; }
let small = shrink(grown.clone(), case, seed); let small = shrink(grown.clone(), case, seed);
// Described from the shrunk tree: the grown tree's chain names
// widgets that are no longer there, and the ancestry of the
// failure is what a test is written from.
let how = diverges(&small, case, seed).unwrap_or_default();
println!( println!(
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}", "seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
case.name(), case.name(),