Compare commits

..
Author SHA1 Message Date
iris-ai f6242aa33c Take a span child's length from its hint, and ask it once in its slot
A share child was drawn in the measuring room and again in its slot, and
one record holding two questions made every local change under it defer
to the span. Where a rule or a hint gives the length along the span, the
first ask answers nothing the rule does not, so the child is asked once,
in its slot; the widgets that always report the whole of their box now
say so. A hint with a fraction resolves against the frame and pins it.

The dump rig prints every cold layout so a change to it shows in a diff.
2026-09-19 02:09:41 -04:00
iris-ai a888717ee9 Say window where these comments still say frame
Lengths became lengths of the window when the frame did, and `Part::From`'s
own documentation still described its spans as frame lengths -- which is
what the scroll above read them as.
2026-09-19 01:23:34 -04:00
iris-ai e8a5792dcb Place a scroll's fitting content in the viewport, not in the window
A scroll that has not been scrolled and whose content fits asked for its
content box as `Part::From(UiSpan::FULL)`. A `Part::From` span is in window
lengths, so `rel(1.0)` in one is the whole window rather than the whole box,
and the content landed in a window-tall box anchored at the viewport's
start -- 50 px low for a 300 px viewport in a 400 px window.

Saying the whole of the box as `Part::All` is the one expression that cannot
mean anything else, and it is also the place the child was already asked in,
so the placement becomes a no-op.
2026-09-19 01:23:34 -04:00
iris-aiandClaude Opus 5 a30971e4c5 Call the record's boxes what they are
The offer names are from the protocol before this one, where a widget was
drawn twice and the record had to say which drawing was the question. It
is asked once now, so offer_part is the part it was asked in, offer_place
the place it was asked at, and place where its drawing was put: part,
asked and placed. LayoutHolds::frame is a range on the window since the
frame became a length of one, and the frame's own entry is the frame_len
pin beside it, so it is window; Painter::frame_own goes with it.
answers_at had one caller and said less than the line that replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:40:23 -04:00
iris-aiandClaude Opus 5 adbedaf264 Say what the fuzzer's branching widget branched on
Branch reads a measurement in pixels and draws a different subtree either
side of a threshold, and it left that read as a pin on the window, so
every one of them redrew on every resize: at depth 8 that was seed 1's
resize going from 40 widget draws to 131 and seed 13's from nothing to
828. It now states the range it actually branched on, the way Span states
the one that decides whether its shares have room. A fixture that redraws
everything on a resize cannot tell a change that reuses well from one
that does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:24:10 -04:00
iris-aiandClaude Opus 5 23523eea29 Take a window read where a length is resolved against it
A widget that resolves a window length in pixels depends on that window
wherever the length is a fraction of it, and nothing was recording that:
Painter::to_px replaces window_px_len and pins the window it read, while
a length that is only pixels is that many pixels in any window and pins
nothing. Span still states the range it actually branched on, which
replaces the pin with something wider.

Scroll is where it showed: its content's answer is a window length now,
so a viewport whose own box does not change with the window -- 40 px of
a branch's box -- kept an end-snapped offset from the window before.
Seed 942 at depth 6 under resize, pinned as
unsettled::resizing_under_a_short_scroll_snaps_its_window_tall_content_again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:21:11 -04:00
iris-aiandClaude Opus 5 1512d8418b 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>
2026-09-19 00:14:46 -04:00
iris-aiandClaude Fable 5.1 0ef87ebfcf Defer a twice-asked widget's local redraw to its parent
A span asks a share child twice in one draw: in the room, whose answer
its slots rest on, and in the decided slot, whose cross-axis answer it
reads. The record keeps only the second question, so a local redraw that
found that answer unchanged never told the row that the first had --
seed 946 at depth 6, where emptying a fixed-height column turns it from
a share into a fixed width as wide as the row. A widget its parent asked
more than once in one draw now defers to that parent, like one whose
declared length changed. Pinned as
unsettled::emptying_a_column_the_row_asked_twice_asks_the_row_again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:29:58 -04:00
iris-aiandClaude Fable 5.1 3091fb86df Ask each child once and place its answer by re-expression
A widget draws in the box it is asked in and its answer is placed inside
that box by re-expressing the drawing; nothing is drawn again in a box an
answer chose. The offer machinery, whose job was to tell a measuring draw
from a placing one, goes with the placing draw. A span measures each child
from its cursor and moves fixed children to their slots with place_at; a
share child is asked once more in its decided slot with its frame narrowed
to it. A stack asks non-sizing children in the box its sizing child
decided. A scroll asks its content once and moves it to the scrolled
offset. A local redraw asks the retained question again and puts the
answer back where the parent placed it.

A symbolic length a child pinned composes through Part::Of exactly where
the part is the whole box less pixels, and pins the parent's own length
otherwise; dropping it let a pad reuse a drawing across a narrowed frame
of the same pixel length (shrinker seeds 60, 248 and 384 at depth 5).

Suite 114/114 including the two decided-box pins, fast oracle 11/11,
shrinker 400 seeds at depth 5 over all fifteen cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:20:57 -04:00
21 changed files with 1217 additions and 794 deletions

No files matched your search

-4
View File
@@ -32,7 +32,6 @@ pub(crate) enum Counter {
SizeReads,
HintHits,
HintMisses,
RetainedSizeHits,
ReuseAttempts,
ReuseExact,
ReuseMoved,
@@ -42,7 +41,6 @@ pub(crate) enum Counter {
ReuseOutside,
ReuseWrongLayer,
ReuseWrongNode,
PlaceRedraws,
QueuePops,
DepthReads,
LocalRedraws,
@@ -70,7 +68,6 @@ impl Counter {
"draw-result size reads",
"hint hits",
"hint misses",
"retained size hits",
"reuse attempts",
"reuse exact",
"reuse moved",
@@ -80,7 +77,6 @@ impl Counter {
"reuse: outside what it holds for",
"reuse: another layer",
"reuse: region-node choice changed",
"placed by redrawing",
"redraw queue pops",
"depth reads",
"local redraws",
+35 -40
View File
@@ -1,6 +1,6 @@
use crate::{
LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, Size,
TextureHandle, UiRegion, WidgetId,
LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
};
/// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,35 +9,39 @@ use crate::{
#[derive(Debug)]
pub struct ActiveData {
pub id: WidgetId,
/// Its frame in `parent_move`'s coordinates: what a fraction it declares
/// 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.
/// Where its drawing goes, in its region node's coordinates.
pub extent: UiRegion,
/// That frame in its parent's frame coordinates, before composition:
/// forwarded whole by a transparent container, narrowed by a declared
/// length. Its length is the same on every ask, which is what
/// a local redraw relies on to ask its parent's own question again.
pub frame: UiRegion,
/// What of its parent's extent the drawing was given, and what it was
/// given at the parent's first ask of it -- the question a cold layout
/// asks. A part is a length from the extent's start, so an extent that
/// moved re-places every child by re-adding that start.
pub place: [Place; 2],
pub offer_place: [Place; 2],
/// The box that ask gave it, in its frame's coordinates. Kept rather
/// than worked out again from where its parent's own box is now: a
/// parent drawn again in the box its own answer chose gives its children
/// boxes it never measured anything in, and the measurement this widget
/// answered is the one its parent's layout was built on.
pub offer_part: UiRegion,
/// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window.
pub frame: UiVec2,
/// A frame its parent decided for it on each axis -- a row's slot, or
/// padding's frame less its pixels -- as a length of the window. `None`
/// forwards the parent's frame. What it declared is kept separately in
/// `declared` and is a fraction of whichever of the two reached it.
pub narrow: [Option<Len>; 2],
/// Where its drawing was put, and where it was asked, each as a part of
/// its parent's box. The two differ where a container asks in one place
/// 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
/// 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.
pub part: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// What the widget said it used of its frame, the last time it drew.
/// Asked more than once in its parent's last draw -- measured in one box
/// and then asked in the one the parent decided. The parent's layout
/// rests on the first answer and its drawing on the last, so only the
/// parent can ask either again.
pub re_asked: bool,
/// What the widget reported, in window-unit lengths.
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 drawn: bool,
pub parent: Option<WidgetId>,
@@ -63,7 +67,8 @@ pub struct ActiveData {
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
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,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
@@ -77,20 +82,10 @@ pub struct ActiveData {
}
impl ActiveData {
/// What it answered when its parent measured it, where it has been
/// measured at all. Not `size`, which is what its last drawing reported:
/// a drawing made in the box that answer chose is answering a different
/// question.
/// What it answered when its parent asked, where it has been asked at
/// all. Not `size`, which is what its last drawing reported: a drawing
/// re-expressed in the box that answer chose is not a second answer.
pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size)
}
/// Whether what it answered still stands for a frame of these pixel
/// lengths. The answer was given in the box its parent first asked
/// about, which is what it is checked against -- `holds` on the record
/// is about the box the answer then chose.
pub fn answers_at(&self, px: crate::PxVec2, part: UiRegion) -> bool {
self.answer
.is_some_and(|(_, holds)| holds.contains(px, part))
}
}
+32 -16
View File
@@ -1,27 +1,35 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion};
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on: the pixel lengths of its
/// frame and of its own box that its drawing and its answer hold for, and
/// the symbolic length of its own box where it read one.
/// What one evaluation of a widget depends on: the window lengths its reads
/// hold for, the pixel lengths of its own box, and the symbolic lengths of
/// 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,
/// 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
/// own; anywhere else the parent chose that length itself, and a widget
/// pinned this way is checked when it is re-placed.
/// 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
/// 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)]
pub struct LayoutHolds {
pub frame: [Holds; 2],
pub window: [Holds; 2],
pub frame_len: [Option<Len>; 2],
pub extent: [Holds; 2],
pub extent_len: [Option<Len>; 2],
}
impl LayoutHolds {
pub const ANY: Self = Self {
frame: [Holds::ANY; 2],
window: [Holds::ANY; 2],
frame_len: [None; 2],
extent: [Holds::ANY; 2],
extent_len: [None; 2],
};
@@ -29,34 +37,42 @@ impl LayoutHolds {
pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY;
for n in 0..2 {
result.frame[n] = self.frame[n].and(other.frame[n]);
result.window[n] = self.window[n].and(other.window[n]);
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 {
(0..2).all(|n| {
self.frame[n].lo <= other.frame[n].lo
&& self.frame[n].hi >= other.frame[n].hi
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, px: PxVec2, extent: UiRegion) -> bool {
pub fn contains(self, window: PxVec2, frame: UiVec2, extent: UiRegion) -> bool {
AXES.into_iter().all(|axis| {
let n = axis as usize;
let len = extent.axis(axis).len();
self.frame[n].contains(px.axis(axis))
&& self.extent[n].contains(len.to_px(px.axis(axis)))
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)
})
}
+224 -252
View File
@@ -1,14 +1,14 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, RenderedText,
RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, Rel,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
ui::render_state::DrawInfo,
ui::render_state::{DrawInfo, Placing},
};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
@@ -17,30 +17,20 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc,
/// What a fraction this widget declares or reports is a fraction of, in
/// the coordinates of `move_idx`: forwarded from its parent unchanged
/// through a span, a stack or a scroll, and narrowed only by what was
/// decided above it -- a declared length, or the root. Its length
/// is the same on every ask of the widget, which is what keeps a fraction
/// under it from being resolved twice.
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.
/// This widget's frame, per axis: a length of the window, and what a
/// 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
/// frame and the box without either becoming the other.
pub(super) frame: UiVec2,
/// Where this widget's drawing goes, in its region node's coordinates.
pub(super) extent: UiRegion,
/// 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
/// length in pixels makes it hold for that number of pixels.
pub(super) extent_len: [Option<Len>; 2],
/// Symbolic box lengths read only to compute the answer. A container can
/// replace the provisional drawings used for that answer with drawings
/// in decided boxes, so this contract is independent of the final one.
pub(super) answer_extent_len: [Option<Len>; 2],
/// The frame in pixels, which its children's frames are a length of:
/// threaded down rather than composed back up the chain, so every length
/// in layout is one multiply from its parent's and [`Holds::through`]
/// inverts exactly that.
pub(super) px: PxVec2,
/// The window in pixels. Frames and boxes become pixels against this one
/// unit, regardless of region-node boundaries.
pub(super) window: PxVec2,
pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>,
@@ -48,25 +38,18 @@ pub struct Painter<'a> {
/// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first place each was asked in
/// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>,
/// Whether this draw is at the place its parent first asked about, which
/// makes the questions it asks the ones a cold layout asks and their
/// answers the ones to keep.
pub(super) at_offer: bool,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of its frame in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise.
pub(super) frame_own: [Holds; 2],
/// The same for its own box.
/// What this draw itself read of the window in pixels, per axis: every
/// window until it reads one, then that one, unless it says otherwise.
pub(super) window_own: [Holds; 2],
/// 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],
/// Pixel-box dependencies used only to compute the answer. These do not
/// constrain a retained drawing placed inside that answer.
pub(super) answer_extent_own: [Holds; 2],
/// The final drawing kept for each child. Asking one child again replaces
/// its provisional drawing and therefore replaces this contract too.
/// What each child's drawing depends on. Asking a child again replaces
/// its drawing, so it replaces this too rather than narrowing it.
pub(super) under: Vec<(WidgetId, LayoutHolds)>,
/// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's.
@@ -90,12 +73,10 @@ impl<'a> Painter<'a> {
self.write_resolved(kind, primitive, region, self.resolve(region));
}
/// A box in this widget's extent coordinates, composed into the
/// coordinates its move slot is in: through the extent, then through the
/// frame the extent is a part of. The same two steps a recomposition
/// replays, so a moved drawing lands where a cold one does.
/// A box in this widget's extent coordinates, composed into its region
/// node's coordinates.
fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent).within(&self.frame)
region.within(&self.extent)
}
fn write_resolved<P: Primitive>(
@@ -159,63 +140,47 @@ impl<'a> Painter<'a> {
/// one child wants, and what every transparent container passes for the
/// frame.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, UiRegion::FULL, [Place::Within(Part::All); 2])
self.widget_at(id, [None; 2], [Place::Within(Part::All); 2])
}
/// Draws a child, saying what its fractions are of and where its drawing
/// goes.
/// Asks a child, saying what its fractions are of and where it is asked.
///
/// `frame` is that reference, in this widget's own frame coordinates:
/// [`UiRegion::FULL`] forwards this widget's frame, 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. Narrowing
/// it is for what is decided from above, and a declared length narrows
/// it here.
/// `narrow` is a length this widget decided for the child's frame, per
/// axis, as a length of this widget's own frame: a resolved share, or a
/// box a sibling's answer decided. `None` forwards this widget's frame,
/// 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 drawing goes, per axis, as a part of this
/// widget's extent: see [`Place`]. A narrowed frame is its own extent,
/// since the narrowing is what said where the drawing goes.
/// `place` is where the child is asked, per axis, as a part of this
/// widget's box: see [`Place`]. The child draws once, in that box, and
/// its answer is placed inside it by re-expressing the drawing. Nothing
/// is drawn again in a box an answer chose; a container that puts the
/// answer somewhere else says so with [`Self::place_at`].
pub fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
frame: UiRegion,
narrow: [Option<Len>; 2],
place: [Place; 2],
) -> DrawResult<'s, 'a, W> {
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id());
let (local, extent) = frame_and_extent(
frame,
part_of(self.extent, place),
narrowed_by(declared, frame),
align,
);
let within = match local == UiRegion::FULL {
true => self.frame,
false => local.within(&self.frame),
};
let (frame, extent) =
frame_and_extent(self.extent, self.frame, place, narrow, declared, align);
#[cfg(feature = "layout-diagnostics")]
if region_node {
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.
if !self.children.contains(&id.id()) {
let re_asked = self.children.contains(&id.id());
if !re_asked {
self.children.push(id.id());
}
let first_ask = self.offer(id.id());
let offer_place = if first_ask {
place
} else {
self.state
.active
.get(&id.id())
.map_or(place, |a| a.offer_place)
};
let px = local.size().to_px(self.px);
// The answer and what it holds for, both about the place asked in.
// The child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box.
let px = frame.to_px(self.window);
let (size, answer_holds, holds) = self.state.draw_inner(
id.id(),
DrawInfo {
@@ -225,18 +190,19 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx,
region_node,
mask: self.mask,
frame: local,
frame_abs: within,
frame,
part: extent,
place,
offer_place,
placed: place,
asked: place,
narrow,
re_asked,
px,
},
None,
self.rsc,
);
let compose = |holds| in_parent(holds, local, extent, place, declared);
let holds = compose(holds);
let holds = self.in_parent(holds, extent, place, narrow, declared);
let answer_holds = self.in_parent(answer_holds, extent, place, narrow, declared);
match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
Some((_, kept)) => *kept = holds,
None => self.under.push((id.id(), holds)),
@@ -244,8 +210,8 @@ impl<'a> Painter<'a> {
DrawResult {
child: id,
painter: self,
size: in_parent_frame(size, local.size(), declared),
answer_holds: compose(answer_holds),
size,
answer_holds,
}
}
@@ -258,6 +224,33 @@ impl<'a> Painter<'a> {
self.state.undraw_rec(id.id(), self.rsc);
}
/// Puts a child asked about in this draw somewhere else in this
/// widget's box: its answer, placed in this part instead. The drawing
/// is re-expressed there rather than made again -- what a row does once
/// it knows every slot, having measured each child from its cursor.
pub fn place_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, place: [Place; 2]) {
debug_assert!(
self.children.contains(&id.id()),
"'{}' placed a child it did not ask about in this draw",
self.label()
);
let at = self.placing();
self.state.place_in(id.id(), &at, place, self.rsc);
}
/// This widget as the thing its children are placed within.
fn placing(&self) -> Placing {
Placing {
id: self.id,
extent: self.extent,
frame: self.frame,
window: self.window,
depth: self.depth,
move_idx: self.move_idx,
mask: self.mask,
}
}
/// 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
/// that comes of them is kept on the child, and `redraw` compares it
@@ -266,17 +259,24 @@ impl<'a> Painter<'a> {
declared_lens(self.rsc.widgets(), id.id())
}
/// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size.
/// 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
/// against this widget's frame, which is the frame a child asked with
/// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let widgets = self.rsc.widgets();
// 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.
let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
let hint = widgets
.size_rules(id.id())
.axis(axis)
.exact()
.or_else(|| {
widgets
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis))
});
})
.map(|hint| hint.within_len(self.frame.axis(axis)));
#[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint);
match hint {
@@ -284,6 +284,11 @@ impl<'a> Painter<'a> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits);
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 hint.rel != Rel::ZERO {
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis));
}
Some(hint)
}
None => {
@@ -294,19 +299,6 @@ impl<'a> Painter<'a> {
}
}
/// Whether this is the first box a child is asked about in during a draw
/// that is itself the one its parent measured -- the question a cold
/// layout asks, whose answer is the one to keep. A drawing made again in
/// a box chosen from an answer asks about that box instead, and what it
/// hears back is not a measurement of anything.
fn offer(&mut self, child: WidgetId) -> bool {
if !self.at_offer || self.offered.contains(&child) {
return false;
}
self.offered.push(child);
true
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
@@ -369,31 +361,20 @@ impl<'a> Painter<'a> {
pub fn extent_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len();
self.extent_len[axis as usize] = Some(len);
self.answer_extent_len[axis as usize] = Some(len);
len
}
/// The symbolic length used to compute this widget's answer, where the
/// final drawing itself is rebuilt without depending on that length.
pub fn answer_extent_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len();
self.answer_extent_len[axis as usize] = Some(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
}
/// Says that the final drawing uses a symbolic length already read for
/// the answer.
pub fn drawing_uses_extent_len(&mut self, axis: Axis, len: Len) {
debug_assert_eq!(self.extent.axis(axis).len(), len);
self.extent_len[axis as usize] = Some(len);
}
/// A part of this widget's box, expressed in its frame coordinates so it
/// can be used as a child frame decided here.
pub fn extent_part(&self, axis: Axis, part: Part) -> UiSpan {
part.of(*self.extent.axis(axis))
}
/// 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
/// the way the box around it would have placed the widget.
@@ -425,19 +406,11 @@ impl<'a> Painter<'a> {
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
}
/// This widget's own box in pixels, used only to compute its answer.
pub fn answer_px_size(&mut self) -> PxVec2 {
PxVec2::new(
self.answer_px_len(Axis::X),
self.answer_px_len(Axis::Y),
)
}
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let 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];
if *own == Holds::ANY {
*own = Holds::at(len);
@@ -445,17 +418,6 @@ impl<'a> Painter<'a> {
len
}
/// One pixel length used only to compute this widget's answer. The final
/// drawing may be retained when that answer is placed in another box.
pub fn answer_px_len(&mut self, axis: Axis) -> Px {
let len = self.extent.axis(axis).len().to_px(self.px.axis(axis));
let own = &mut self.answer_extent_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// The lengths of this widget's own box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its length
@@ -464,7 +426,7 @@ impl<'a> Painter<'a> {
let part = self.extent.axis(axis).len();
let holds = holds.into();
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",
self.label(),
self.id
@@ -472,30 +434,34 @@ impl<'a> Painter<'a> {
self.extent_own[axis as usize] = holds;
}
/// One axis of this widget's frame in pixels -- what a fraction of its
/// area resolves against, and so what a container divides among its
/// children. Its own box is a part of this one.
pub fn frame_px_len(&mut self, axis: Axis) -> Px {
let len = self.px.axis(axis);
let own = &mut self.frame_own[axis as usize];
/// A window length in pixels, which is what every length in layout is
/// measured in. Reading one pins the drawing to this window wherever the
/// length is a fraction of it; one that is only pixels is that many
/// pixels in any window and pins nothing.
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let window = self.window.axis(axis);
if len.rel != Rel::ZERO {
let own = &mut self.window_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
*own = Holds::at(window);
}
len
}
len.to_px(window)
}
/// [`Self::holds`] stated about the frame rather than about this
/// widget's own box, for a container whose drawing turns on what its
/// fractions are of rather than on the part of it it took.
pub fn frame_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
/// The windows this drawing holds for, stated rather than taken: a
/// container that branched on a length in pixels says which side of the
/// boundary it was on, which is wider than the one window reading that
/// length pins, and replaces it.
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its frame",
holds.contains(self.window.axis(axis)),
"'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(),
self.id
);
self.frame_own[axis as usize] = holds;
self.window_own[axis as usize] = holds;
}
pub fn text_data(&mut self) -> &mut TextData {
@@ -583,70 +549,77 @@ impl PrimitiveLike for &TextureHandle {
}
}
/// What a child depends on, said about the boxes the widget that drew it
/// has rather than the ones the child was given.
/// Moves what a child depends on into this widget's own terms: this
/// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// only what the child was asked with.
impl Painter<'_> {
/// 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
/// is what reached the child; where only pixels did, no length of this
/// frame can change the child's and the pin stops here.
///
/// `frame` is the child's frame in this widget's frame coordinates and
/// `extent` the box it was given, in the child's own frame coordinates. Both
/// reach it as one length, so what it holds for maps back through that
/// length exactly -- and where the box it was given is this widget's own,
/// what it says about that box is what this widget can say about its own.
pub(crate) fn in_parent(
/// Extent 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
/// 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
/// what fractions under the child mean and leaves the box the part it
/// was given.
fn in_parent(
&self,
holds: LayoutHolds,
frame: UiRegion,
extent: UiRegion,
place: [Place; 2],
narrow: [Option<Len>; 2],
declared: [Option<LayoutLen>; 2],
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
let frame_len = frame.axis(axis).len();
result.frame[n] = holds.frame[n].through(frame_len);
match (place[n].part(), declared[n]) {
// Its box is this widget's own, or a part of it in that box's
// own lengths: so what it holds for is a range on this widget's
// own box, which is what lets that box move without a redraw. A
// length it pinned is this widget's length wherever the part is
// the whole of it, and pins the same way.
(Part::All, None) if *frame.axis(axis) == UiSpan::FULL => {
// Every read became pixels against the window, so a range on
// it is already in this widget's terms.
result.window[n] = holds.window[n];
let reaches = narrow[n].is_none()
&& !matches!(place[n].part(), Part::Sized(_))
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO);
result.frame_len[n] = holds.frame_len[n].and(reaches.then(|| self.frame.axis(axis)));
match (place[n].part(), declared[n].is_some()) {
// Its box is this widget's own, or a part of it in that
// box's own lengths: so what it holds for is a range on this
// widget's own box, which is what lets that box move without
// a redraw. A length it pinned is this widget's length
// wherever the part is the whole of it, and pins the same
// way.
(Part::All, false) => {
result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n];
}
// 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 part into
// a range on this widget's box.
(Part::Of(span), None) => {
result.extent[n] = holds.extent[n].through(span.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
// part into a range on this widget's box. A length it pinned
// is this widget's length less the part's pixels where the
// part is the whole of the box less pixels, which is the one
// shape that inverts exactly; any other part pins this
// widget's own length.
(Part::Of(span), false) => {
let part_len = span.len();
result.extent[n] = holds.extent[n].through(part_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: 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.
// Its box is a length this widget decided, from its own
// frame or from a sibling's answer: no length of this
// widget's box reaches it, so what it holds for is a range
// on the window and none of it on that box.
_ => {
result.frame[n] = result.frame[n].and(
holds.extent[n]
.through(extent.axis(axis).len())
.through(frame_len),
);
result.window[n] =
result.window[n].and(holds.extent[n].through(extent.axis(axis).len()));
}
}
}
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
@@ -711,59 +684,58 @@ pub(crate) fn placed_extent(
placed
}
/// The part of a widget's own box a `place` names, in the coordinates that
/// box is 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], frame: UiRegion) -> [Option<Len>; 2] {
AXES.map(|axis| {
declared[axis as usize]
.map(|len| Len::from_parts(len.rel, len.px).within_len(frame.axis(axis).len()))
})
}
/// The frame a child is asked in and the box its drawing goes in, both in
/// the coordinates of the widget asking.
/// The frame length and the box a child is asked in, in the coordinates the
/// widget asking draws in.
///
/// `frame` is what the caller said the child's fractions are of, and `part`
/// what of the caller's own box the drawing takes. `narrow` is the length a
/// declared rule gives the frame, which makes the frame the box the drawing
/// goes in -- a rule is what decided where it goes, and there is nothing
/// left to place inside it. A caller that narrowed the frame itself said the
/// same thing.
///
/// The length is the caller's to supply so that a widget asked again gets
/// the frame it already has rather than a second resolution of its rule.
/// `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
/// slot, or padding's frame less its pixels -- and [`Part::Sized`] one a
/// sibling's answer decided; both are window lengths, like every other
/// length here, since a slot of a row is not a fraction of anything the row
/// 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
/// caller decided is what `place` names.
pub(crate) fn frame_and_extent(
mut frame: UiRegion,
part: UiRegion,
own: UiRegion,
parent_frame: UiVec2,
place: [Place; 2],
narrow: [Option<Len>; 2],
declared: [Option<LayoutLen>; 2],
align: RegionAlign,
) -> (UiRegion, UiRegion) {
) -> (UiVec2, UiRegion) {
let part = part_of(own, place, align);
let mut frame = parent_frame;
let mut extent = part;
for (axis, narrow) in AXES.into_iter().zip(narrow) {
let span = frame.axis_mut(axis);
let narrowed = match narrow {
Some(len) => {
for axis in AXES {
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 start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*span = UiSpan::new(start, start + len);
true
}
None => *span != UiSpan::FULL,
};
if narrowed {
*extent.axis_mut(axis) = UiSpan::FULL;
*extent.axis_mut(axis) = UiSpan::new(start, start + len);
}
}
(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
}
+19 -7
View File
@@ -1,30 +1,42 @@
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.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Part {
/// The whole of it.
All,
/// Frame lengths from where the box starts, which is what a container
/// dividing room among its children speaks: a child's report is a length
/// of the frame, so the cursor that sums those reports is one too. A
/// moved box re-places every child by re-adding its start, exactly.
/// Window lengths from where the box starts, 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. A moved box
/// re-places every child by re-adding its start, exactly. A fraction
/// here is a fraction of the window and not of the box -- the whole of a
/// box is [`Self::All`], not a `rel(1.0)` span.
From(UiSpan),
/// A part of the box in its own coordinates, which is what a container
/// that insets one speaks: taking eleven pixels off the end needs no
/// length, where saying the same thing in frame lengths would make the
/// 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.
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 {
/// 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 {
Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Of(span) => span.within(&extent),
Self::Sized(len) => {
let start = extent.start + (extent.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
}
}
}
+318 -330
View File
@@ -1,10 +1,10 @@
#[cfg(feature = "layout-diagnostics")]
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::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Moves,
Painter, Part, PixelRegion, Place, PxVec2, Size, StrongWidget, UiRegion, UiRsc, Weight,
WidgetId, Widgets,
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
Moves, Painter, Part, PixelRegion, Place, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc,
UiSpan, UiVec2, Weight, WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -20,22 +20,23 @@ pub(super) struct DrawInfo {
pub parent_move: MoveIdx,
pub region_node: bool,
pub mask: MaskIdx,
/// The frame in the parent widget's frame coordinates, before
/// composition. Its length is the same on every ask of the widget.
pub frame: UiRegion,
/// That frame composed into `parent_move`'s coordinates, which is what
/// the widget's own drawing is written within.
pub frame_abs: UiRegion,
/// The box the drawing is given, 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.
/// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window.
pub frame: UiVec2,
/// The box the widget is asked in, in its parent region node's
/// coordinates.
pub part: UiRegion,
/// What of the parent's extent the drawing was given, and what it was
/// given at the parent's first ask of it. See [`Place`].
pub place: [Place; 2],
pub offer_place: [Place; 2],
/// The frame in pixels: one multiply from the parent's own, which is
/// where every pixel length in layout comes from.
/// 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 puts the answer somewhere else.
pub placed: [Place; 2],
pub asked: [Place; 2],
/// 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.
pub re_asked: bool,
/// The frame in pixels, resolved once against the window.
pub px: PxVec2,
}
@@ -43,36 +44,20 @@ 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.place.map(Place::fills)
}
/// Whether this ask is the one the widget's answer is kept from: the
/// same widget in the place its parent measured it by, however this draw
/// came about.
///
/// **Open.** A place is a length from where the asking widget's own box
/// starts, so two drawings of that widget -- one in the box its parent
/// measured it in, one in the box its own answer chose -- ask their
/// children in the same places and different boxes, and this cannot tell
/// them apart. Shrinker seeds 2 (`repaint`) and 108 (`reorder`) at depth
/// 5 are where that shows.
fn offer(&self) -> bool {
self.place == self.offer_place
self.placed.map(Place::fills)
}
}
/// 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.
struct Placing {
id: WidgetId,
extent: UiRegion,
/// The widget's frame in the coordinates its children compose within:
/// `FULL` where it is a region node, since its box is that node.
local: UiRegion,
px: PxVec2,
depth: usize,
move_idx: MoveIdx,
mask: MaskIdx,
pub(super) struct Placing {
pub id: WidgetId,
pub extent: UiRegion,
pub frame: UiVec2,
pub window: PxVec2,
pub depth: usize,
pub move_idx: MoveIdx,
pub mask: MaskIdx,
}
pub struct UiRenderState {
@@ -134,24 +119,24 @@ impl UiRenderState {
self.resized = true;
let Some(root) = self.old_root else { return };
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
// asked about is the whole of its frame.
let offer = part_of(UiRegion::FULL, active.offer_place);
active.answers_at(px, offer) && active.holds.contains(px, active.extent)
// asked about is the whole of its frame. Both its answer and its
// drawing have to stand in the new window, since nothing above
// it will ask either again.
let answer = active
.answer
.is_some_and(|(_, holds)| holds.contains(size, active.frame, active.part));
answer && active.holds.contains(size, active.frame, active.part)
});
if !stands {
widgets.needs_redraw.insert(root);
}
}
/// The root is asked about in the output: the window is where a fraction
/// becomes pixels rather than a box of its own, so the root's box is the
/// first length threaded down. Its own rules narrow that box, and where
/// they do the narrowed box is also the offer -- nothing above it chose
/// anything else.
fn root_info(&self, region: UiRegion) -> DrawInfo {
let px = region.size().to_px(self.output_size);
/// The root is asked about in the output. Its own rules narrow both its
/// frame and box; nothing above it chose a different one.
fn root_info(&self, frame: UiVec2, extent: UiRegion) -> DrawInfo {
let px = frame.to_px(self.output_size);
DrawInfo {
layer: 0,
parent: None,
@@ -159,11 +144,12 @@ impl UiRenderState {
parent_move: MoveIdx::NONE,
region_node: false,
mask: MaskIdx::NONE,
frame: region,
frame_abs: region,
part: UiRegion::FULL,
place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2],
frame,
part: extent,
placed: [Place::Within(Part::All); 2],
asked: [Place::Within(Part::All); 2],
narrow: [None; 2],
re_asked: false,
px,
}
}
@@ -210,23 +196,24 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
if let Some(id) = root {
let region = Self::root_region(id.id(), rsc.widgets());
let info = self.root_info(region);
let (frame, extent) = Self::root_layout(id.id(), rsc.widgets());
let info = self.root_info(frame, extent);
self.draw_inner(id.id(), info, None, rsc);
}
}
/// The root's frame: the window, narrowed by the root's own rules. Its
/// extent is that frame, since nothing above it chose anything else.
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
let narrow = narrowed_by(declared_lens(widgets, id), UiRegion::FULL);
/// The root's frame and box: the window, taken in by the root's own
/// rules. Nothing above it narrowed anything or chose where it goes, so
/// its declaration is the whole of what decides either.
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
frame_and_extent(
UiRegion::FULL,
UiRegion::FULL,
narrow,
UiVec2::FULL_SIZE,
[Place::Within(Part::All); 2],
[None; 2],
declared_lens(widgets, id),
widgets.alignment(id),
)
.0
}
pub(super) fn draw_inner(
@@ -236,55 +223,57 @@ impl UiRenderState {
mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds, LayoutHolds) {
let (frame, part) = (info.frame_abs, info.part);
let part = info.part;
#[cfg(feature = "layout-diagnostics")]
{
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 declared = declared_lens(rsc.widgets(), id);
// Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
let stale = rsc.widgets().needs_redraw.contains(&id);
let retained = match stale {
true => None,
false => self
.retained_answer(id, part, info)
.and_then(|answer| self.try_reuse(id, frame, part, info, rsc).map(|_| answer)),
};
let answer = retained.unwrap_or_else(|| {
// The widget draws once, in the box it is asked in, and its answer
// is placed inside that box by re-expressing the drawing. The box the
// answer chose is never a question: nothing is drawn again in it, so
// an answer is kept only with the drawing that gave it, and both
// have to hold for the box asked about.
let reused = (!stale)
.then(|| self.retained_answer(id, part, info))
.flatten()
.and_then(|answer| {
let extent = placed_extent(part, answer.0, declared, info.fill(), align);
self.try_reuse(id, part, extent, info, rsc).map(|()| answer)
});
let answer = reused.unwrap_or_else(|| {
if old.is_none() {
old = self.remove(id, false, rsc);
}
self.draw_at(id, part, info, old.take(), rsc)
let answer = self.draw_at(id, part, info, old.take(), rsc);
// Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left
// open.
let extent = placed_extent(part, answer.0, declared, info.fill(), align);
if extent != part {
self.relocate(id, extent, info, rsc);
}
answer
});
// Where the drawing goes, in the frame's own coordinates: the part
// its parent gave it, with the answer placed inside that part on any
// axis the parent left open. The frame itself does not change, so
// nothing under it resolves a fraction a second time.
//
let extent = placed_extent(
part,
answer.0,
declared_lens(rsc.widgets(), id),
info.fill(),
align,
);
self.place(id, extent, info, rsc);
let drawing_holds = self.active[&id].holds;
let active = self.active.get_mut(&id).unwrap();
// Whoever asked owns how the boxes were reached: the frame it stated,
// and what of its own box it gave the drawing. A local redraw asks
// the same question again from these.
active.frame_abs = frame;
// and what of its own box it asked in. A local redraw asks the same
// question again from these.
active.frame = info.frame;
active.narrow = info.narrow;
active.re_asked = info.re_asked;
active.answer = Some(answer);
active.offer_place = info.offer_place;
active.offer_part = part;
active.place = info.place;
active.asked = info.asked;
active.part = part;
active.placed = info.placed;
active.own_align = align;
// A subtree can be reused whole under a different parent -- same box,
// same layer, same region node -- and nothing in the drawing says it
@@ -301,29 +290,6 @@ impl UiRenderState {
(answer.0, answer.1, drawing_holds)
}
/// Recompose retained geometry when the evaluation still holds at this extent.
fn place(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
if self
.try_reuse(id, info.frame_abs, extent, info, rsc)
.is_some()
{
return;
}
assert_eq!(
extent,
info.part,
"'{}' ({id:?}) does not hold for the box its answer chose: part {:?}, wanted {extent:?}, retained {:?} with {:?}",
rsc.widgets().label(id),
info.part,
self.active.get(&id).map(|active| active.extent),
self.active.get(&id).map(|active| active.holds),
);
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, extent, info, old, rsc);
}
/// Calls a widget's `draw` and keeps what it drew in `extent` of `frame`.
fn draw_at(
&mut self,
@@ -333,36 +299,30 @@ impl UiRenderState {
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) {
let frame = info.frame_abs;
let (move_idx, local, retired_move) = match info.region_node {
// Its box becomes its movable region, so it draws in that
// region's coordinates and its box is one entry to rewrite.
let frame = info.frame;
let (move_idx, extent, retired_move) = match info.region_node {
// A node entry is only a translation. Its local box keeps the
// same window-unit length as the box in its parent's node.
true => (
self.move_slot(id, info.parent_move, frame),
UiRegion::FULL,
self.move_slot(id, info.parent_move, translation(extent)),
local_region(extent),
None,
),
// Keep the old entry alive until every descendant has migrated.
// Reusing its index sooner could make an old parent look current.
false => (info.parent_move, frame, self.slots.remove(&id)),
};
let (old_children, old_answer, old_offer_part) = match old {
Some(old) => (old.children, old.answer, Some(old.offer_part)),
None => (Vec::new(), None, None),
false => (info.parent_move, extent, self.slots.remove(&id)),
};
let old_children = old.map_or_else(Vec::new, |old| old.children);
rsc.widgets_mut().needs_redraw.remove(&id);
// Only evaluation at the original offer establishes the children's
// offers. A placing evaluation must not overwrite that question.
let px = info.px;
let at_offer = info.offer();
let window = self.output_size;
let mut painter = Painter {
state: self,
frame: local,
frame,
extent,
extent_len: [None; 2],
answer_extent_len: [None; 2],
px,
window,
mask: info.mask,
layer: info.layer,
own_layer: info.layer,
@@ -371,13 +331,11 @@ impl UiRenderState {
primitives: Vec::new(),
mask_region: None,
children: Vec::new(),
offered: Vec::new(),
at_offer,
size_deps: Vec::new(),
frame_own: [Holds::ANY; 2],
window_own: [Holds::ANY; 2],
frame_own_len: [None; 2],
under: Vec::new(),
extent_own: [Holds::ANY; 2],
answer_extent_own: [Holds::ANY; 2],
answer_under: LayoutHolds::ANY,
depth: info.depth,
move_idx,
@@ -400,21 +358,18 @@ impl UiRenderState {
rsc: _,
frame: _,
extent: _,
px: _,
window: _,
mask,
textures,
primitives,
mask_region,
extent_own,
answer_extent_own,
extent_len,
answer_extent_len,
answer_under,
children,
offered: _,
at_offer: _,
size_deps,
frame_own,
window_own,
frame_own_len,
under,
move_idx,
layer,
@@ -430,11 +385,23 @@ impl UiRenderState {
);
// 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
// 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 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 {
x: rules.x.apply(size.x),
y: rules.y.apply(size.y),
x: ruled(Axis::X, size.x),
y: ruled(Axis::Y, size.y),
};
// 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
@@ -442,7 +409,10 @@ impl UiRenderState {
// Overflowing is otherwise ordinary: a text too tall for the box it
// was offered reports the height it needs.
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}",
rsc.widgets().label(id),
);
@@ -454,25 +424,32 @@ impl UiRenderState {
if let Some(idx) = retired_move {
self.moves.remove(idx);
}
let drawing_own = LayoutHolds {
frame: frame_own,
// 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 {
window: window_own,
frame_len,
extent: extent_own,
extent_len,
};
let answer_own = LayoutHolds {
extent: [
drawing_own.extent[0].and(answer_extent_own[0]),
drawing_own.extent[1].and(answer_extent_own[1]),
],
extent_len: answer_extent_len,
..drawing_own
};
let answer_holds = answer_own.and(answer_under);
let answer_holds = own_holds.and(answer_under);
let holds = under
.into_iter()
.fold(drawing_own, |holds, (_, child)| holds.and(child));
.fold(answer_holds, |holds, (_, child)| holds.and(child));
debug_assert!(
holds.contains(px, extent),
holds.contains(self.output_size, info.frame, extent),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
rsc.widgets().label(id),
);
@@ -490,11 +467,12 @@ impl UiRenderState {
parent_move: move_idx,
region_node: false,
mask,
frame: UiRegion::FULL,
frame_abs: UiRegion::FULL,
frame: UiVec2::FULL_SIZE,
part: UiRegion::FULL,
place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2],
placed: [Place::Within(Part::All); 2],
asked: [Place::Within(Part::All); 2],
narrow: [None; 2],
re_asked: false,
px,
},
rsc,
@@ -505,14 +483,15 @@ impl UiRenderState {
let active = ActiveData {
id,
frame_abs: frame,
extent,
frame: info.frame,
place: info.place,
offer_place: info.offer_place,
offer_part: old_offer_part.unwrap_or(extent),
// Whoever asked writes the answer, if this was the asking.
answer: old_answer,
narrow: info.narrow,
placed: info.placed,
asked: info.asked,
part: extent,
// Whoever asked writes the answer.
answer: None,
re_asked: info.re_asked,
size,
holds,
drawn: true,
@@ -575,38 +554,22 @@ impl UiRenderState {
return None;
}
let answer = active.answer?;
answer.1.contains(info.px, part).then_some(answer)
answer
.1
.contains(self.output_size, info.frame, part)
.then_some(answer)
}
/// The pixel lengths of a widget's frame, which is what a local redraw
/// 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)
}
/// Reuses the actual drawing in a new box if its retained contract holds
/// there. Answers retained from a different ask are handled separately.
/// Keeps the retained drawing if its contract holds for `part`, the box
/// asked about, and puts it at `extent`, where the answer places it.
fn try_reuse(
&mut self,
id: WidgetId,
frame: UiRegion,
part: UiRegion,
extent: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) -> Option<(Size, LayoutHolds)> {
) -> Option<()> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) {
@@ -652,10 +615,10 @@ impl UiRenderState {
}
return None;
}
// In pixels, because the frame is a fraction of its parent's and
// that may be what changed -- an unchanged fraction of a box half the
// 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
// size is half the widget.
if !active.holds.contains(info.px, extent) {
if !active.holds.contains(self.output_size, info.frame, part) {
#[cfg(feature = "layout-diagnostics")]
{
// Which of the three said no, so a frame that redraws more
@@ -664,13 +627,16 @@ impl UiRenderState {
let holds = active.holds;
for axis in AXES {
let n = axis as usize;
if holds.extent_len[n].is_some_and(|pinned| pinned != extent.axis(axis).len()) {
if holds.extent_len[n].is_some_and(|pinned| pinned != part.axis(axis).len()) {
diag::bump(Counter::OutsidePinnedLen);
}
if !holds.frame[n].contains(info.px.axis(axis)) {
if !holds.window[n].contains(self.output_size.axis(axis))
|| holds.frame_len[n].is_some_and(|pinned| pinned != info.frame.axis(axis))
{
diag::bump(Counter::OutsideFrame);
}
if !holds.extent[n].contains(extent.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);
}
@@ -680,24 +646,37 @@ impl UiRenderState {
}
return None;
}
let extent_moved = active.extent != extent;
let moved = active.frame_abs != frame;
let (answer, slot) = ((active.size, active.holds), active.move_idx);
if moved {
self.relocate(id, extent, info, rsc);
Some(())
}
/// 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
/// 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) {
let active = &self.active[&id];
debug_assert!(
!rsc.widgets().needs_redraw.contains(&id),
"'{}' ({id:?}) placed while marked to draw",
rsc.widgets().label(id)
);
let has_region_node = active.move_idx != active.parent_move;
let local = match has_region_node {
true => local_region(extent),
false => extent,
};
let moved = active.extent != local;
let slot = active.move_idx;
if has_region_node {
self.moves.set(slot, frame);
} else {
self.recompose_subtree(id, frame, info.parent_move, rsc);
self.moves.set(slot, translation(extent));
}
}
if extent_moved {
self.reposition(id, frame, extent, info, rsc);
if moved {
self.reposition(id, local, info, rsc);
}
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap();
active.frame_abs = frame;
active.frame = info.frame;
active.place = info.place;
active.placed = info.placed;
#[cfg(feature = "layout-diagnostics")]
{
match (moved, has_region_node) {
@@ -718,20 +697,31 @@ impl UiRenderState {
},
);
}
Some(answer)
}
/// Places one child of `at.id` in the box that widget's own box gives
/// it: its part of the extent, with its answer placed inside that part
/// where the ask left the axis open.
/// Places one child of `at.id` where that widget's own box now has it.
fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) {
let place = self.active[&child].placed;
self.place_in(child, at, place, rsc);
}
/// Puts a child of `at.id` in `place` of that widget's box: its answer
/// placed inside that part where the place leaves the axis open, the
/// drawing re-expressed there.
pub(super) fn place_in(
&mut self,
child: WidgetId,
at: &Placing,
place: [Place; 2],
rsc: &mut dyn UiRsc,
) {
let active = &self.active[&child];
let (frame, part) = Self::re_ask(active, at.extent, active.place);
let (frame, part) = Self::ask_again(active, at, place);
let extent = placed_extent(
part,
active.measured().unwrap_or(active.size),
active.declared,
active.place.map(Place::fills),
place.map(Place::fills),
active.own_align,
);
let info = DrawInfo {
@@ -742,34 +732,27 @@ impl UiRenderState {
region_node: active.move_idx != active.parent_move,
mask: at.mask,
frame,
frame_abs: frame.within(&at.local),
part,
place: active.place,
offer_place: active.offer_place,
px: frame.size().to_px(at.px),
placed: place,
asked: active.asked,
narrow: active.narrow,
re_asked: active.re_asked,
px: frame.to_px(at.window),
};
self.place(child, extent, info, rsc);
self.relocate(child, extent, info, rsc);
}
/// The frame and the box a widget being asked again is given, from what
/// it already has and where its parent's box is now. A frame's length is
/// the same on every ask, so a declared length is put back where it sits
/// in the part rather than resolved from its rule a second time.
fn re_ask(
active: &ActiveData,
parent_extent: UiRegion,
place: [Place; 2],
) -> (UiRegion, UiRegion) {
let narrow = AXES.map(|axis| {
let n = axis as usize;
active.declared[n]
.is_some()
.then(|| active.frame.axis(axis).len())
});
/// The frame 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
/// it declared are its own record's, so both are resolved against that
/// parent's frame again exactly as the first ask resolved them.
fn ask_again(active: &ActiveData, at: &Placing, place: [Place; 2]) -> (UiVec2, UiRegion) {
frame_and_extent(
active.frame,
part_of(parent_extent, place),
narrow,
at.extent,
at.frame,
place,
active.narrow,
active.declared,
active.own_align,
)
}
@@ -778,36 +761,21 @@ impl UiRenderState {
/// 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
/// not change is not touched at all.
fn reposition(
&mut self,
id: WidgetId,
frame: UiRegion,
extent: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) {
fn reposition(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
let active = self.active.get_mut(&id).unwrap();
active.frame_abs = frame;
active.extent = extent;
let local = if info.region_node {
UiRegion::FULL
} else {
frame
};
for primitive in &active.primitives {
let handle = &primitive.handle;
*self.layers[handle.layer].region_mut(handle) =
primitive.region.within(&extent).within(&local);
*self.layers[handle.layer].region_mut(handle) = primitive.region.within(&extent);
}
if let Some(mask_region) = active.mask_region {
rsc.ui_mut().masks.get_mut(active.mask).region =
mask_region.within(&extent).within(&local);
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&extent);
}
let at = Placing {
id,
extent,
local,
px: info.px,
frame: info.frame,
window: self.output_size,
depth: info.depth,
move_idx: active.move_idx,
mask: active.mask,
@@ -837,38 +805,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 {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
@@ -939,13 +875,14 @@ impl UiRenderState {
id,
ActiveData {
id,
frame_abs: UiRegion::FULL,
extent: UiRegion::FULL,
frame: UiRegion::FULL,
place: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2],
offer_part: UiRegion::FULL,
frame: UiVec2::FULL_SIZE,
narrow: [None; 2],
placed: [Place::Within(Part::All); 2],
asked: [Place::Within(Part::All); 2],
part: UiRegion::FULL,
answer: None,
re_asked: false,
size,
holds: LayoutHolds::ANY,
drawn: false,
@@ -1126,9 +1063,8 @@ impl UiRenderState {
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?;
active.drawn.then(|| {
let placed = active.extent.within(&active.frame_abs);
self.moves
.resolve(active.parent_move, placed)
.resolve(active.move_idx, active.extent)
.to_px(self.output_size)
})
}
@@ -1145,11 +1081,16 @@ impl UiRenderState {
// Its parent resolved its declared lengths into its box and decided
// whether to draw it at all, so a change to either is the parent's
// to draw -- with the mark left on, so the parent draws it rather
// than keeping it.
// than keeping it. So is a widget the parent asked twice: its
// layout rests on an answer this widget cannot give again alone.
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent
&& (declared_changed || alignment_changed || !active.drawn || active.answer.is_none())
&& (declared_changed
|| alignment_changed
|| active.re_asked
|| !active.drawn
|| active.answer.is_none())
{
// Both stay marked: the parent because it has this to draw, and
// this because the parent must draw it rather than keep what it
@@ -1165,10 +1106,10 @@ impl UiRenderState {
// box is its own to work out again against the output. Every other
// widget was given one.
let Some(parent) = active.parent else {
let region = Self::root_region(id, rsc.widgets());
let (frame, extent) = Self::root_layout(id, rsc.widgets());
let info = DrawInfo {
mask: active.parent_mask,
..self.root_info(region)
..self.root_info(frame, extent)
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
@@ -1176,13 +1117,14 @@ impl UiRenderState {
self.draw_inner(id, info, old, rsc);
return true;
};
let px = self.asked_px(id);
let (was_answer, was_holds) = (active.answer, active.holds);
let needs_replacement = active.place != active.offer_place;
// Re-ask the question its answer came from, not a later placement of
// that answer. A parent may move the retained drawing into a box the
// answer chose without making that box a new question.
let parent_extent = self.active[&parent].extent;
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed);
// 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
// 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
// asked, and nothing is asked in it here either.
let parent_at = self.placing_of(parent, self.active[&parent].part);
let (frame, part) = Self::ask_again(active, &parent_at, active.asked);
let info = DrawInfo {
layer: active.layer,
parent: active.parent,
@@ -1190,12 +1132,13 @@ impl UiRenderState {
parent_move: active.parent_move,
region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask,
frame: active.frame,
frame_abs: active.frame_abs,
part: Self::re_ask(active, parent_extent, active.offer_place).1,
place: active.offer_place,
offer_place: active.offer_place,
px,
frame,
part,
placed: active.asked,
asked: active.asked,
narrow: active.narrow,
re_asked: false,
px: frame.to_px(self.output_size),
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
@@ -1211,10 +1154,12 @@ impl UiRenderState {
{
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;
}
if needs_replacement || active.answer != was_answer || active.holds != was_holds {
if active.answer != was_answer || active.holds != was_holds {
// The parent retains both the answer and the drawing's validity;
// even an unchanged size can narrow the range safe for a resize.
#[cfg(feature = "layout-diagnostics")]
@@ -1223,18 +1168,61 @@ impl UiRenderState {
diag::bump(Counter::ReaderEdges);
}
self.mark(parent, rsc.widgets_mut());
} else {
// The answer stands, so where the parent put it stands: the
// fresh drawing goes back there -- the same place, of the box
// the parent's answer chose rather than the one it was asked in.
let at = self.placing_of(parent, self.active[&parent].extent);
self.place_in(id, &at, was_place, rsc);
}
true
}
/// 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
/// asking one of them again, the box its answer chose for placing one.
fn placing_of(&self, id: WidgetId, extent: UiRegion) -> Placing {
let active = &self.active[&id];
Placing {
id,
extent,
frame: active.frame,
window: self.output_size,
depth: active.depth,
move_idx: active.move_idx,
mask: active.mask,
}
}
}
/// 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
/// about this box and cannot exceed it.
fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
/// 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
/// this box and cannot exceed it.
fn within_box(size: Size, extent: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = size.axis(axis);
let box_len = px.axis(axis);
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
let window = window.axis(axis);
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 {
+20 -6
View File
@@ -120,18 +120,32 @@ impl Widget for Branch {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(Part::All), top])
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X);
// The range it actually branched on, said the way a container says
// one: pinning the window instead would redraw this widget on every
// resize, which is a fixture that never exercises reuse.
let threshold = Px::from_f32(self.threshold);
let holds = match px > threshold {
true => Holds::from(threshold + Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=threshold),
};
painter.window_holds(Axis::X, holds.through(len));
let below = Place::Within(Part::Of(UiSpan::new(cut, Len::FULL)));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, UiRegion::FULL, place),
false => painter.widget_at(&self.narrow, UiRegion::FULL, place),
match px > threshold {
true => painter.widget_at(&self.wide, [None; 2], place),
false => painter.widget_at(&self.narrow, [None; 2], place),
};
Size::LEFTOVER
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
}
pub struct Spanned {
+4
View File
@@ -15,4 +15,8 @@ impl Widget for Masked {
// draw, and the framework would place the drawing it clipped away.
Size::LEFTOVER
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
}
+1 -1
View File
@@ -15,6 +15,6 @@ impl Widget for Offset {
moved(painter.extent_len(Axis::X), self.amt.x),
moved(painter.extent_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, UiRegion::FULL, place).size()
painter.widget_at(&self.inner, [None; 2], place).size()
}
}
+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
// edge pinned it to a corner it had not asked for.
//
// The padding goes around what it pads: the frame passes through, so
// the inner's fractions mean what they would without it, and only
// the box it draws in is moved in by the pixels. Said as a part of
// this widget's own box in that box's own lengths, so nothing here
// reads how long the box is -- and a box chosen from this widget's
// own answer therefore does not feed back into that answer.
// Padding is an inset of both: it comes off the frame, so `rel(1)`
// under it fills this widget rather than overflowing it by the
// 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
// asked this widget in the room left, and a text wraps at that.
let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead),
@@ -30,7 +29,16 @@ impl Widget for Pad {
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
let inner = painter.widget_at(&self.inner, UiRegion::FULL, 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 {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
+16 -13
View File
@@ -12,12 +12,11 @@ pub struct Scroll {
impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size {
let container_len = painter.px_len(self.axis);
// Measured in the whole viewport, then drawn at the scrolled offset.
let whole = UiRegion::FULL;
// Asked in the whole viewport, then put at the scrolled offset.
let answer_len = painter
.widget_at(&self.inner, whole, [Place::Fill(Part::All); 2])
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2])
.len(self.axis);
let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len);
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis);
self.container_len = container_len;
self.content_len = fixed.max(container_len);
@@ -45,7 +44,6 @@ impl Widget for Scroll {
// have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel());
let mut content = UiSpan::FULL;
// Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. Writing the same box as
// its own length in pixels is the same box in another form, and the
@@ -53,19 +51,20 @@ impl Widget for Scroll {
// one centred in `px 900`, since halving a difference is not halving
// each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
if moved || self.content_len != self.container_len {
let content = match moved || self.content_len != self.container_len {
true => {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
content = UiSpan::new(start, start.offset(self.content_len));
Part::From(UiSpan::new(start, start.offset(self.content_len)))
}
false => Part::All,
};
// The viewport is the inner's frame, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is drawn is the
// content box, scrolled.
painter.widget_at(
// content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there.
painter.place_at(
&self.inner,
whole,
self.axis
.pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)),
self.axis.pair(Place::Fill(content), Place::Fill(Part::All)),
);
// 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
@@ -73,6 +72,10 @@ impl Widget for Scroll {
// is.
Size::LEFTOVER
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
}
impl Scroll {
+50 -43
View File
@@ -13,8 +13,8 @@ impl Widget for Span {
// The row: this span's own box, as a length of the frame its children
// are laid out against. Its start is nothing's business -- a slot is
// a length from it -- so what this reads is the length alone.
let far = painter.answer_extent_len(axis);
let measure_along = |from: Len, to: Len| match self.dir.sign {
let far = painter.extent_len(axis);
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
@@ -22,18 +22,34 @@ impl Widget for Span {
// 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. The
// frame passes 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 them; what it is drawn in is the room left from the cursor,
// because a text has to wrap at the width actually there.
// A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The frame passes
// 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
// 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.
// 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 lens = Vec::with_capacity(self.children.len());
let mut measured = Vec::with_capacity(self.children.len());
for child in &self.children {
let room = Place::Fill(Part::From(measure_along(cursor, far)));
let len = painter
.widget_at(child, UiRegion::FULL, axis.pair(room, across))
.len(axis);
let size = match painter.size_hint(child, axis) {
Some(len) => {
measured.push(None);
len
}
None => {
let room = Place::Within(Part::From(along(cursor, far)));
let size = painter
.widget_at(child, [None; 2], axis.pair(room, across))
.size();
measured.push(Some(size));
size.axis(axis)
}
};
let len = size;
cursor.px += len.px + self.gap;
cursor.rel += len.rel;
lens.push(len);
@@ -50,10 +66,9 @@ impl Widget for Span {
|sum, len| sum + *len,
);
let fixed_total = Len::from_parts(total.rel, total.px);
// 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.
let room = far - fixed_total;
let room = far - Len::from_parts(total.rel, total.px);
// 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`
// itself, and answered back through the same expression, so the
@@ -65,26 +80,14 @@ impl Widget for Span {
// exist at all turns on this.
let mut shares = false;
if total.leftover > Weight::ZERO {
shares = room.to_px(painter.frame_px_len(axis)) > Px::ZERO;
shares = painter.to_px(room, axis) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.frame_holds(axis, holds.through(room));
painter.window_holds(axis, holds.through(room));
}
if shares {
painter.drawing_uses_extent_len(axis, far);
}
let drawing_far = match shares {
true => far,
false => fixed_total,
};
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(drawing_far - to, drawing_far - from),
};
// Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. A rule
@@ -101,7 +104,8 @@ impl Widget for Span {
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) {
for ((child, len), measured) in self.children.iter().zip(&lens).zip(&measured) {
let len = *len;
// 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
// pixels or a fraction keeps those and overflows.
@@ -119,23 +123,26 @@ impl Widget for Span {
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
// Along the row the span says where the child goes, and that slot
// is the drawing's box outright rather than something to place an
// answer inside again.
let span = along(from, start);
let (frame, slot) = match len.leftover > Weight::ZERO && shares {
true => (
UiRegion::from_axis(
axis,
painter.extent_part(axis, Part::From(span)),
UiSpan::FULL,
),
Place::Fill(Part::All),
),
false => (UiRegion::FULL, Place::Fill(Part::From(span))),
// is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere
// else: its slot narrows its frame, and the child is asked in
// 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
// room is put there as it is, and one not made yet is made here.
let slot = along(from, start);
let place = axis.pair(Place::Fill(Part::From(slot)), across);
let mut narrow = [None; 2];
if len.leftover > Weight::ZERO && shares {
narrow[axis as usize] = Some(slot.len());
}
let used = match (measured, narrow[axis as usize]) {
(Some(size), None) => {
painter.place_at(child, place);
size.axis(!axis)
}
_ => painter.widget_at(child, narrow, place).len(!axis),
};
let placed = painter.widget_at(child, frame, axis.pair(slot, across));
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too;
+23 -10
View File
@@ -23,28 +23,41 @@ impl Widget for Stack {
Some((i, child)) => {
painter.child_layer_at(i);
painter
.widget_at(child, UiRegion::FULL, [Place::Fill(Part::All); 2])
.widget_at(child, [None; 2], [Place::Fill(Part::All); 2])
.size()
}
None => Size::LEFTOVER,
};
// Every other child gets the box the sizing child decided: the
// stack is that length, so that is the box they are asked in, and a
// 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
// bigger than itself is its own business.
let place = [Axis::X, Axis::Y].map(|axis| {
let len = size.axis(axis);
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() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i);
let place = [Axis::X, Axis::Y].map(|axis| {
let len = size.axis(axis);
let part = match len.leftover > Weight::ZERO {
true => Part::All,
false => Part::From(UiSpan::new(Len::ZERO, Len::from_parts(len.rel, len.px))),
};
Place::Within(part)
});
painter.widget_at(child, UiRegion::FULL, place);
painter.widget_at(child, [None; 2], place);
}
size
}
/// Without a sizing child a stack is whatever box it is given, which it
/// can say without drawing anything.
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
match self.size {
StackSize::Default => Some(LayoutLen::LEFTOVER),
StackSize::Child(_) => None,
}
}
}
#[derive(Default, Debug)]
+4 -4
View File
@@ -24,15 +24,15 @@ impl Widget for BranchesOnMeasurement {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(Part::All), top])
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(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 place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, UiRegion::FULL, place),
false => painter.widget_at(&self.narrow, UiRegion::FULL, place),
true => painter.widget_at(&self.wide, [None; 2], place),
false => painter.widget_at(&self.narrow, [None; 2], place),
};
Size::LEFTOVER
}
+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}");
}
/// The same reading through a pad: padding goes around what it pads and
/// does not narrow what a fraction under it is a fraction of, so half of the
/// window plus the padding is what the pad takes and where the next child
/// starts.
/// Padding is an inset: it narrows the frame a fraction resolves against and
/// adds itself back to the padded widget's reported length.
#[test]
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
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.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inner, (10, 10), (210, 90));
assert_corners!(h, padded, (0, 0), (220, 100));
assert_corners!(h, tail, (220, 0), (320, 100));
assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (210, 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.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.part.x.len().to_px(window), Px::from_int(418));
}
#[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`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let drawn = active.extent.within(&active.frame_abs);
let region = h.render.moves.resolve(active.parent_move, drawn);
let region = h.render.moves.resolve(active.move_idx, active.extent);
let dim = h.size().axis(axis);
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());
@@ -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, 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));
}
+21 -46
View File
@@ -12,7 +12,6 @@ struct Counted {
draws: Rc<Cell<usize>>,
size: Size,
reads_box: bool,
reads_answer_box: bool,
}
impl Widget for Counted {
@@ -20,8 +19,6 @@ impl Widget for Counted {
self.draws.set(self.draws.get() + 1);
if self.reads_box {
painter.px_size();
} else if self.reads_answer_box {
painter.answer_px_size();
}
self.size
}
@@ -41,18 +38,11 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
draws: draws.clone(),
size,
reads_box,
reads_answer_box: false,
}
.add(&mut h.rsc);
(id, Counts(draws))
}
fn answer_counted(h: &mut Harness, size: Size) -> (WeakWidget<Counted>, Counts) {
let (id, draws) = counted(h, size, false);
h.rsc[id].reads_answer_box = true;
(id, draws)
}
struct Layered {
children: [StrongWidget<Rect>; 2],
_revision: usize,
@@ -166,9 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
// Its final slot is a parent decision, so it is evaluated there after
// the provisional ask established its length.
assert_eq!(asked_draws.get(), 2);
// Asked once, from the cursor; its slot is its answer and the drawing is
// moved there.
assert_eq!(asked_draws.get(), 1);
}
#[test]
@@ -226,7 +216,7 @@ impl Widget for FromHint {
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
painter.widget_at(
&self.inner,
UiRegion::FULL,
[None; 2],
[Place::Within(Part::All), Place::Within(Part::From(top))],
);
Size::LEFTOVER
@@ -250,7 +240,7 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads its box's size to compute its answer.
/// Reads its box's size, which nothing but its own draw can put right.
struct ReadsBox {
draws: Rc<Cell<usize>>,
}
@@ -258,36 +248,25 @@ struct ReadsBox {
impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::from_px(painter.answer_px_size().div_int(4))
Size::from_px(painter.px_size().div_int(4))
}
}
/// Reads its box across one axis only, so its drawing holds for a taller
/// box on its own and only a wider one is worth a draw.
///
/// Both report a quarter of what they read. Their empty drawings are
/// independent of that read, so a changed question costs one draw.
/// Both of these report a quarter of what they read. The quarter-sized box
/// the answer places them in is not a question: the drawing is moved there,
/// so each length they are asked at costs one draw.
struct ReadsWidth {
draws: Rc<Cell<usize>>,
}
struct ReadsDrawingWidth {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsDrawingWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.px_len(Axis::X);
Size::LEFTOVER
}
}
impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::from_px(PxVec2::new(
painter.answer_px_len(Axis::X).div_int(4),
painter.px_len(Axis::X).div_int(4),
Px::from_int(20),
))
}
@@ -343,7 +322,6 @@ fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
draws: ruled.clone(),
size: Size::LEFTOVER,
reads_box: false,
reads_answer_box: false,
};
let second = match declared {
true => second.width(rel(0.25)).add(&mut h.rsc),
@@ -521,7 +499,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), false);
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12));
@@ -899,7 +877,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[None; 2],
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
@@ -988,7 +966,7 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
self.region,
[Some(self.region.x.len()), None],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
@@ -1087,7 +1065,7 @@ fn a_declared_size_change_stops_at_an_independent_parent() {
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsDrawingWidth {
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
@@ -1098,7 +1076,7 @@ fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
h.frame();
assert!(draws.get() > settled);
assert_corners!(h, leaf, (0, 0), (800, 200));
assert_corners!(h, leaf, (300, 90), (500, 110));
}
#[test]
@@ -1158,7 +1136,7 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[None; 2],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
@@ -1233,7 +1211,7 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
struct Measured;
impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size {
let width = painter.answer_px_len(Axis::X);
let width = painter.px_len(Axis::X);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
}
@@ -1246,7 +1224,7 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[None; 2],
[
Place::Fill(Part::From(UiSpan::new(
Len::px(self.start),
@@ -1288,7 +1266,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter
.widget_at(
&self.child,
UiRegion::FULL,
[None; 2],
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
@@ -1308,7 +1286,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter
.widget_at(
&self.child,
UiRegion::FULL,
[None; 2],
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(self.extent.y)),
@@ -1333,10 +1311,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
} else {
Size::from((80, 27))
};
let (leaf, _) = match fractional {
true => counted(h, size, false),
false => answer_counted(h, size),
};
let (leaf, _) = counted(h, size, !fractional);
let child = Container {
child: leaf.add_strong(&mut h.rsc),
region,
+16
View File
@@ -145,3 +145,19 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
h.set_root(clipper);
h.frame();
}
/// Content that fits sits in the viewport, not in a box of the window's
/// length anchored at the viewport's start. `Part::From` takes window
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
/// filling the window would land right.
#[test]
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).height(100).add(&mut h.rsc);
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
h.set_root((head, scroll).span(Dir::DOWN));
assert_corners!(h, scroll, (0, 100), (400, 400));
assert_corners!(h, inner, (0, 225), (400, 275));
}
+221
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 \
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,
/// `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
@@ -846,3 +891,179 @@ fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other));
}
/// Nine widgets, shrunk from seed 946 at depth 6. The column is a share of
/// the row while its rect has room to draw and a fixed width once it has
/// not, so the row asks it twice: in the room, where it answers a share,
/// and in its slot, where it answers its text's width. Emptying the column
/// changes only the first answer. A local redraw that asked only the second
/// question kept the row as it was; the column has to defer to the row.
fn plant_column_that_is_a_share_only_while_its_rect_fits(
h: &mut Harness,
emptied: bool,
) -> (Vec<WidgetId>, WeakWidget<Span>, Vec<StrongWidget>) {
let first = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
let second = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let mut spare: Vec<StrongWidget> =
vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)];
let mut children: Vec<StrongWidget> = vec![first.add_strong(&mut h.rsc)];
if !emptied {
children.append(&mut spare);
}
let column = Span {
children,
dir: Dir::DOWN,
gap: Px::ZERO,
}
.height(159)
.add(&mut h.rsc);
let left = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
let right = rect(Color::BLUE.alpha(0)).add(&mut h.rsc);
let row = Span {
children: vec![
left.add_strong(&mut h.rsc),
column.add_strong(&mut h.rsc),
right.add_strong(&mut h.rsc),
],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let end = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
let root = Span {
children: vec![end.add_strong(&mut h.rsc), row.add_strong(&mut h.rsc)],
dir: Dir::LEFT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.set_root(root);
(
vec![
first.id(),
filler.id(),
second.id(),
column.id(),
left.id(),
right.id(),
row.id(),
end.id(),
root.id(),
],
column,
spare,
)
}
#[test]
fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
let mut warm = Harness::new((900, 1200));
let (ids, column, _spare) =
plant_column_that_is_a_share_only_while_its_rect_fits(&mut warm, false);
warm.frame();
// Kept alive: dropping the last share of a widget frees its id.
let _removed: Vec<StrongWidget> = warm.rsc[column].children.drain(1..).collect();
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _, _spare) =
plant_column_that_is_a_share_only_while_its_rect_fits(&mut cold, true);
cold.frame();
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);
}
/// Six widgets, shrunk from seed 942 at depth 6 (`resize`). A `Branch` asks
/// its probe in the top 40 px of its box and forwards the frame, so the
/// scroll's own box is 40 px tall whatever the window is -- but its content
/// is as tall as the frame, which is the window, and a scroll kept to its
/// end has to be told when that changes. Resolving a length against the
/// window is what reads it, so that is where the dependency is taken.
fn plant_a_window_tall_column_in_a_short_scroll(h: &mut Harness) -> Vec<WidgetId> {
let leaf = rect(Color::RED).add(&mut h.rsc);
let column = Span {
children: vec![leaf.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.height(rel(1.0))
.add(&mut h.rsc);
let scroll = Scroll::new(column.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
let wide = rect(Color::BLUE).add(&mut h.rsc);
let narrow = rect(Color::GREEN).add(&mut h.rsc);
let root = Branch {
probe: scroll.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold: 55.0,
}
.add(&mut h.rsc);
h.set_root(root);
vec![leaf.id(), column.id(), scroll.id(), root.id()]
}
#[test]
fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
let mut warm = Harness::new((1920, 1200));
let ids = plant_a_window_tall_column_in_a_short_scroll(&mut warm);
warm.frame();
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant_a_window_tall_column_in_a_short_scroll(&mut cold);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
+42
View File
@@ -0,0 +1,42 @@
//! Prints where a cold layout puts every widget of many grown trees, so two
//! commits can be compared on cold layout alone. The warm/cold oracle cannot
//! see a change that moves cold layout, since both of its sides move; this
//! can, by diffing its output across the change:
//!
//! IRIS_DUMP_SEEDS=400 IRIS_DUMP_DEPTH=5 cargo test --release \
//! --test layout_dump -- --ignored --nocapture > /tmp/before.txt
//!
//! then the same after, and `diff` the two. A line is one widget: the seed,
//! its index in creation order, and its box in window pixels, or `-` where
//! it is not drawn.
use iris::harness::Harness;
use iris::random::{Edits, grow};
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
#[test]
#[ignore = "a dump to diff across commits, not a check"]
fn every_cold_layout_is_printed() {
let seeds = env("IRIS_DUMP_SEEDS", 400_u64);
let depth = env("IRIS_DUMP_DEPTH", 5_usize);
let mut out = String::new();
for seed in 1..=seeds {
let mut harness = Harness::new((1920.0, 1200.0));
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
harness.state.root = Some(root);
harness.frame();
for (index, id) in tree.ids.iter().enumerate() {
match harness.region(id) {
Some(region) => out.push_str(&format!("{seed} {index} {region:?}\n")),
None => out.push_str(&format!("{seed} {index} -\n")),
}
}
}
print!("{out}");
}
+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
/// survive two different kinds of invalidation in a row.
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.
Size,
/// Every declared size at once, so every reader of a size has a changed
@@ -119,12 +124,13 @@ pub enum Case {
Shuffle(Shuffle),
}
pub const ALL: [Case; 15] = [
pub const ALL: [Case; 16] = [
Case::Repaint,
Case::RepaintSome,
Case::Resize,
Case::ResizeRepaint,
Case::ResizeSize,
Case::SizeResize,
Case::Size,
Case::EverySize,
Case::Align,
@@ -146,6 +152,7 @@ impl Case {
Self::Resize => "resize",
Self::ResizeRepaint => "resize-repaint",
Self::ResizeSize => "resize-size",
Self::SizeResize => "size-resize",
Self::Size => "size",
Self::EverySize => "every-size",
Self::Align => "align",
@@ -170,6 +177,15 @@ impl Case {
_ => (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) {
@@ -284,7 +300,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
warm.frame();
return out;
}
Case::Size | Case::ResizeSize => Edits {
Case::Size | Case::ResizeSize | Case::SizeResize => Edits {
sizes: some_sizes(warm, tree, rng),
..Default::default()
},
@@ -384,6 +400,17 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
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.part, active.extent, active.size,
)
}
/// 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,
/// 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();
}
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 (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
cold.state.root = Some(root);
cold.frame();
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let mut drawn = 0;
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
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
// ancestry comes with it, marking the widgets that own a region.
let mut chain = Vec::new();
let mut records = Vec::new();
let mut at = Some(w);
while let Some(id) = at {
let active = &warm.render.active[&id];
@@ -424,11 +469,22 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
false => "*",
};
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;
}
return Some(format!(
"widget {i}\n warm {got:?}\n cold {want:?}\n {}",
chain.join(" < ")
"widget {i}\n warm {got:?}\n cold {want:?}\n {}\n{}",
chain.join(" < "),
records.join("\n"),
));
}
match drawn {
+6 -2
View File
@@ -70,10 +70,14 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default());
for &case in &cases {
let Some(how) = diverges(&grown, case, seed) else {
if diverges(&grown, case, seed).is_none() {
continue;
};
}
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!(
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
case.name(),