Compare commits

..
Author SHA1 Message Date
iris-aiandClaude Opus 5 cbccfb600a Say a length that is zero, and share the seed a scan takes
A full sweep of #19, and the first review of f48e04e.

`Display for LayoutLen` leaves out every part that is zero, so
`LayoutLen::ZERO` printed as the empty string -- and `Debug` forwards to
`Display` since the last commit, so the four `assert_eq!`s in
`cases/deferred.rs` print nothing where a request of zero is, and
`scenario::describe` prints a `.width(0)` rule as `-`, which is what it
prints for a widget that has no rule at all. That file exists so a tree a
fuzzer found can be written out by hand; a value it cannot say is a hole in
the one thing it is for.

`Fixed::ceil_from_f32` took `next_up` of a `from_f32` that had already
clamped, and `next_up` wraps, so a measurement past the top of the grid came
back as the bottom of it. `from_f32` clamps deliberately because a float has
further to come from; the ceiling is the other way in from a float and now
holds to the same rule. The check goes beside the one `from_f32` already
had.

`Moves::depth` walked the move chain a second way, with its own copy of
`CHAIN_LIMIT` and without the assertion `walk` makes; it is `walk` now, so
the CPU counts the chain in one place and the shader's constant reaches
both.

`Harness::set_len` said it set a length "the way `.width()` sets one" and
wrote the whole rule instead, dropping any bound beside it. A case that set
a bound and then a length would have passed with no bound at all.

Three rigs each spelled "one seed, or a range of them" by hand -- the class
the eleventh sweep closed for reading a parameter and not for this. There is
one `rig::seeds` now. `cases/deferred` was last in `suite.rs`'s otherwise
alphabetical list.

`diag::outside` writes out `AxisHolds::contains`'s four clauses to say which
one refused a reuse; a debug assertion now catches a fifth clause added
there and not here, which would leave a refusal counted and unexplained.

`Sow::bound`'s comment recorded an open hole reached by seeds 4 and 196 at
depth 5 -- but `generated.rs` says seeds stopped naming those trees when the
leaves grew images, and 600 depth-5 trees over all sixteen cases agree warm
against cold with every bound a fraction. The comment says what is true now
and why the generator still grows pixels.

Format, workspace clippy under -D warnings with and without
layout-diagnostics, 208 ordinary and 212 diagnostic tests (207 and 211
before, plus the one this adds), and the cold dump byte-identical to
f48e04e across all 34,986 boxes. The three seed scans were not run: nothing
here can move a box, which the dump confirms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 22:04:07 -04:00
iris-aiandClaude Opus 5 f48e04ed36 Cover joining two expressions, and print a request one way
Four findings over 05e6ced, which no earlier round reviewed.

`SizeRequest::join` grafts the other side's nodes into this side's arena,
which happens only when both sides are expressions -- nothing in the suite
did that, so the whole thing passed with a `panic!` in that arm. It is
where a missed renumbering would be silent, since an operand copied
without remapping still names a node that exists. A fixture at two window
widths with absolute geometry covers it now, and the deferred corpus puts
an expression on both sides of one arm; both were checked to reach it by
instrumenting again. The path was already right.

`SizeRequest` grew a `Display` because a derived `Debug` of an arena is
not a tree anyone can write out again, and `describe` moved onto it -- but
`Debug` stayed derived, so the `assert_eq!`s in `cases/deferred.rs`, the
only place a request is compared, still printed the arena on failure.
`Debug` forwards to `Display`.

`Nodes::linear` asked nothing of the arena beside it: it is `Operand`'s
question, the way `RequestedLen::linear` is `RequestedLen`'s.

`describe`'s `|r| format!("{r}")` shadowed the `r: &SizeRule` four lines
above it.

Format, workspace clippy under -D warnings with and without
layout-diagnostics, 207 ordinary and 211 diagnostic tests, the cold dump
byte-identical to 05e6ced across all 34,986 boxes, and 400 depth-5 trees
in each of the three deferred corpora in 200.95s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:33:54 -04:00
iris-ai 05e6ced31d Hold a request in the arena its nodes are allocated in
A size request was a second expression shape beside the one the layout
pass already has. `SizeRequest` held `Sum`/`Min`/`Max` over `Arc` pairs;
`RequestArena` held the same three operators as `Node { op, a, b }` in a
`Vec`, with the same fold over `independent_order` written a second time,
and `import` walked the first rebuilding it as the second.

There is one node type now. An expression is the pass's nodes in an arena
of its own that lasts as long as the rule holding it, and `import` grafts
those nodes into the pass's arena, resolving fractions as they land. The
fold is `Nodes::combine`, which both the builder and the importer call.

So the `Arc` goes, and no refcount replaces it: nothing shares a request
and nothing outside widget code holds one. A plain length stays inline,
so `size_of::<SizeRule>()` is 40 either way and only an actual expression
allocates. A node's operand is a number within its own arena, and
`RequestedLen` -- the only form that leaves one -- is that plus the epoch
saying which pass numbered it, so the epoch is now checked once where a
handle comes back in rather than at every level of the walk it starts.

`SizeRule::at_least`/`at_most` were the only clones of a request in the
framework, and both read a rule out, moved one end of its bound, and
wrote it back into the slot it came from. `Widgets::edit_bound` does it
where it sits, so nothing copies an expression to cap it.

`SizeRequest` grew a `Display`, since the shrinker prints one and a
derived `Debug` of an arena is not something a tree can be rebuilt from:
`min(30 px;1 leftover;, 2 leftover;)<0.5 rel;`.

Measured, medians of three release runs under `perf stat -e
instructions:u`, each set within 0.005% of its median: bounds_cost
MODE=cap FRAMES=2000 is 5.665B against 5.743B (-1.34%), and
revision_cost resize ROWS=40 FRAMES=500 is 4.855B against 4.893B
(-0.79%).

Format, workspace clippy under -D warnings with and without
layout-diagnostics, 206 ordinary and 210 diagnostic tests, the cold dump
byte-identical to 2ac0843 across all 34,986 boxes, 400 depth-5 trees in
64.24s, 1,000 depth-6 in 160.35s, 2,000 depth-4 in 298.82s, and 400
depth-5 trees in each of the three deferred-request corpora in 205.42s.
2026-09-20 21:07:19 -04:00
iris-ai ea1f836bf9 Say the environment once, and stop a hint read going uncounted
The eleventh sweep, over the built-in bounds work in 2ac0843.

`Painter::size_hint` refused to answer for a bounded widget by returning
above the diagnostics, so that read was neither a hint hit nor a miss and
`hint_read` recorded nothing. It is a miss now, with the reason on it.

The two `for axis in Axis::BOTH` loops that `draw_widget` grew, both
writing `own_holds`, are one loop, and the comment about combining the
ask's holds no longer sits between a comment and the code it describes.

`Declared::from_axes` lost its only caller with `Widgets::declared_lens`;
`Bounds::from_axes` and `SizeRule::declared` never had one.

The scenario shrinker printed a rule with derived `Debug`, which is 130
characters an axis in a line that carries every ancestor, in the one
function whose job is output a tree can be rebuilt from. It prints its
parts again.

`bounds_cost` invented three environment-reading spellings where four
copies of one `env` helper already existed; there is now one, in
`tests/rig`, and the four copies are gone. It also verified 128 regions
inside its measured loop, which the other rigs deliberately do before
theirs; that measured 0.65% of the total, and none of it is layout.

The 250-window row with a 300 cap was built by two tests, and the one
that still explained itself tested less; they are one. The half of
`a_cap_attribute_narrows_the_widgets_box` that the wrapper's removal left
without its deciding assertion is the allocator's path instead, which
nothing at the root covered.

Format, workspace clippy under -D warnings with and without
layout-diagnostics, 206 ordinary and 210 diagnostic tests, 400 depth-5
trees warm against cold in 64.19s, and the cold dump byte-identical to
2ac0843 across all 34,986 boxes.
2026-09-20 20:13:57 -04:00
iris-ai 2ac0843cb2 Constrain offered boxes with independent widget size bounds 2026-09-20 19:47:32 -04:00
iris-ai 4cb6f6882a Hold a bound's length where it is decided, and say each thing once
A quality sweep over the deferred request system, which no earlier round
has reviewed.

`Bound::outside` said which end a length fell outside and left the caller
to look that end up through `Bound::at`, which `expect`s an end the value
it is given does not promise: only the pairing of the two calls kept
`at(Shorter)` off a bound with no floor. It already had the length in
hand, so it returns that, and `Outside` and `at` go with the state that
could panic.

`measured_request` pinned the rel base for any bound at all, so a measured
share under a cap in pixels was invalidated by a change to a base its
answer cannot depend on. That question is `Bound::has_fraction` now, which
is also the one `Placing::ask` and `SizeRule::has_fraction` were each
writing out over a bare array.

The rest is one name where there were several spellings: `Span::gaps`,
`Padding::along`, `Plan::drop_bounds` behind one `IRIS_UNBOUNDED` in both
rigs that had grown their own, and `Stack::size_request` resolving its
sizing child the way its draw already does. `Span`'s placement loop asked
three times whether the row was allocated, twice to decide one child's
length; one match answers all three, so the allocated and plain rules are
read side by side.

The buffers `draw_at` now reuses for their capacity are empty only because
every path to it drains them in `remove`; a `debug_assert` says so, since
a drawing over primitives left in one would record them twice.

Comments: `with_requests` named discovery as the hazard where it is a
child drawn mid-row, `Painter::allocate` documented the window it holds
for rather than what it does, `minimum_request` had none, and the note
saying a span carries its children's weight whole -- which is still what
the unallocated path does, and still the surprising part -- had been
replaced by one about the other path.
2026-09-20 18:24:37 -04:00
iris-ai 0e838e9dd1 Retain request dependencies only when discovery supplies the answer 2026-09-20 17:31:53 -04:00
iris-ai 8780b40bb7 Resolve deferred size comparisons before allocating span slots 2026-09-20 16:50:18 -04:00
iris-aiandClaude Opus 5 de1eb7e406 Hold what a widget answers with a rule, and its box with a widget
Bryan's call, given the measurements in `76aaf06`: `SizeRule::{Min, Max,
Clamp}` holds the length a widget answers and never touches the box it draws
in, and `MaxSize` is the box version.

The split is the difference between a rule and a widget here. A box is
whoever asked's to decide, and the retained machinery hands a widget one by
paths that never ask it anything -- a parent re-placing a child, a subtree
repositioned after its parent's box moved. A rule that read the box was
therefore decided again by whichever path arrived last, which is what the
oracle was refusing. A widget has no such trouble: it is drawn again whenever
its own box changes, so `MaxSize` asks `longer_than` where the answer can be
kept, and `region_len` pins the box lengths its drawing holds for.

What that costs is nothing the app wanted: `a_capped_scroll_takes_its_
viewport_from_the_cap` puts 400 px of content under `.max_height(100)` and
gets a 100 px viewport with 300 to scroll, which is what `MaxSize` gave on the
app's pin, and `.max_width`/`.max_height` are that widget rather than a rule.
A cap narrows the offer and not a declared length, so a child that declares
500 px still draws 500 and the cap holds what `MaxSize` itself answers; a
child that asked for a share takes the box the cap allows and the share passes
up, since whoever divides one is `MaxSize`'s parent.

`.min_width`/`.min_height` stay a rule: answering at least so much is a claim
about the length, and a row honours it without anyone narrowing anything.

Bounds in the generated trees are pixels for now, with the reason written
where the next tree is grown: a fraction in a bound is resolved against the
rel base the widget was asked with, and `place_at` hands a parent a retained
answer without checking that it still holds for the rel base this place
gives. Seeds 4 and 196 at depth 5 are where that showed. The hole is older
than bounds -- an `Exact` rule that is a fraction can reach it too -- and
closing it is a check at the re-place site rather than anything about bounds.
A fraction through `MaxSize` is fine and tested, since the widget compares
against its own box.

Format, clippy with and without layout-diagnostics, and the suite (142 + 19 +
13 + 4) are clean. All three seed scans pass: 400 at depth 5 (62s), 1,000 at
depth 6 (162s), 2,000 at depth 4 (299s). The cold dump is 34,986 boxes and
moves wholesale against `2dba90b`, which is the generator growing rules it
did not grow before rather than a layout change; it is the new baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:13:46 -04:00
iris-aiandClaude Opus 5 76aaf06c0b Add SizeRule::{Min, Max, Clamp}, which the oracle refuses
`MaxSize` on the app's pin narrows the box it asks its child in and cuts the
answer to the cap; nothing on this branch does either, so the capability is
missing rather than merely unported. This is that capability as a rule beside
the widget, the way `Exact` already is: `Min(Len)`, `Max(Len)` and
`Clamp { min, max }`, resolved against the rel base a declared length is a
fraction of, and never carrying `leftover` -- a cap containing a share admits
several self-sizing fixed points (`docs/LAYOUT.md`, failed hypotheses).

Where it stands: every hand-written test passes, including the capability the
app actually used -- `a_capped_scroll_takes_its_viewport_from_the_cap` puts
400 px of content under a 100 px cap and gets a 100 px viewport with 300 to
scroll, which is what `MaxSize` gave. The 400-seed depth-5 scan does not
pass, and the reason is a design question rather than a slip, so this sits on
its own branch instead of in #19.

What the scan finds: a bound is the first rule whose effect depends on the
box its parent gives it, and the retained machinery hands a widget a box by
paths that never ask it again -- `place_in` from a re-placing parent, and
`reposition` after a parent's box moved. A decision made when the box was one
length therefore survives into a box of another, so warm and cold disagree
about a tree they agree on structurally. Four readings were measured over 400
seeds at depth 5:

- deciding at every ask and keeping it: seeds 291, 1, 120, 178, 64 differ.
- the same, re-decided at `place_in` too: seeds 1, 362, 188, 254, 156 differ,
  because that path's box is the one the answer chose rather than the one the
  widget was asked in.
- skipping a place its parent decided outright, which is the rule the share
  follows: worse -- the same widget then gets two decisions by two paths.
- the bound as an answer rule only, leaving the box alone: seeds 4 and 196,
  and those are the closest to passing by a wide margin.

The share is the one existing rule of this kind and it is stable because
`place_at` re-asks a child whose rel base it narrows, and because its
decision is baked into the retained place as a `Sized` length. Neither
protection generalises: a bound that binds is a length of the rel base, and
`Sized` cannot say "this slot, narrowed" for a `Within` place.

Also here, because a bound needed them: `Len::longer_than` and
`Bound::outside` share one comparison with the span; a rule that is a
fraction now pins its rel base whether the fraction is a length or a bound,
which was a real gap for `Exact` too; `widget_trait!` passes attributes
through, so the methods it defines can carry doc comments (none could);
`From<N> for Len`, so a bound reads `max_width(300)`; and `random.rs` grows
all three variants, with `describe` printing them so a failure can be written
out by hand.

Format, clippy with and without layout-diagnostics, and the suite (142 + 19 +
13 + 4) are clean. The fast ten-seed oracle passes; the long scans do not.
Neutering the bounds in the generator while leaving its draws in place puts
the same shapes back to green, so the divergence is the bounds and not the
new trees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 14:34:34 -04:00
iris-aiandClaude Opus 5 0d0326769c Ask the root the way every other widget is asked
The root had a layout path of its own: `root_layout` read its declared
lengths against the window, while every other widget's box came of
`Painter::widget_at`, where a rule of the widget's own -- a share with pixels
or a fraction beside it -- is compared against the offer and can take the box
past it. So a share on the root was the window whatever it asked for, which
`docs/LAYOUT_LOG.md` recorded as a gap rather than fixing, and any later rule
that reads the offer would have had to be written twice.

There is one box nobody drew, and that is the whole of what the root is
asked in. `Placing::WINDOW` says it -- the full output, fractions of the full
output, no move entry and no mask -- and `Placing::ask` is then the one place
a box is decided, called by the painter, by a local redraw, and by the root's
first draw. The root's own path is what is left of it: a widget with no
parent keeps different bookkeeping, not a different layout.

Measured in a 400 px window, a probe under each of three parents, which now
agree on every row where two of them agreed before:

    rule                      as root   wrapped   in a span
    leftover(1)                   400       400         400
    px(50) + leftover(1)          400       400         400
    px(500) + leftover(1)         500       500         500   (was 400 as root)
    rel(0.5) + leftover(1)        400       400         400
    rel(2.0) + leftover(1)        800       800         800   (was 400 as root)
    px(500)                       500       500         500
    rel(0.5)                      200       200         200

The comparison is kept on the widget asked about rather than on the asker,
which is what makes the root need nothing of its own: a window range means
the same thing at either end of an ask, `in_parent` passes one up unchanged,
and the asker ends up holding it through the child's drawing exactly as it
did when `longer_than` narrowed the asker directly. The root has no asker, so
its own record is the only place that range can live -- and `resize` already
checks that record, so a share crossing its length is caught with no new
code. `a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing`
now runs at the root too: 500 at a 400 window, 900 at 900, 500 again at 400.

Two things this changes beyond the share. `DrawInfo::asked` is now the place
the parent offered rather than the place the ask came to, so a local redraw
re-decides the rule instead of re-reading the decision -- the two were the
same until a rule could move the box. And the root's `is_region_node` is
read, where the old path passed `false`: a region-node root now gets its
entry, whose translation is the identity, pinned by
`a_region_node_root_is_a_region_node`.

Format, clippy with and without layout-diagnostics, and the suite (136 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`2dba90b` across all 34,571 boxes, and the three seed scans pass: 400 at
depth 5 (61s), 1,000 at depth 6 (155s), 2,000 at depth 4 (291s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 13:51:19 -04:00
38 changed files with 3138 additions and 503 deletions

No files matched your search

+8 -1
View File
@@ -115,7 +115,10 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// that leaves out the thing it was measured from. /// that leaves out the thing it was measured from.
pub const fn ceil_from_f32(v: f32) -> Self { pub const fn ceil_from_f32(v: f32) -> Self {
let nearest = Self::from_f32(v); let nearest = Self::from_f32(v);
match nearest.to_f32() < v { // The top of the grid has no step above it, and stepping past it
// wraps to the bottom. A value out there is a caller's mistake, and
// the clamp `from_f32` already made is the answer to it.
match nearest.to_f32() < v && nearest.0 != Self::MAX.0 {
true => nearest.next_up(), true => nearest.next_up(),
false => nearest, false => nearest,
} }
@@ -511,6 +514,10 @@ mod tests {
fn a_number_from_outside_is_clamped_to_the_grid() { fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::from_f32(1e12), Px::MAX); assert_eq!(Px::from_f32(1e12), Px::MAX);
assert_eq!(Px::from_f32(-1e12), Px::MIN); assert_eq!(Px::from_f32(-1e12), Px::MIN);
// The ceiling is the other way in from a float, and there is no step
// above the top of the grid for it to take.
assert_eq!(Px::ceil_from_f32(1e12), Px::MAX);
assert_eq!(Px::ceil_from_f32(-1e12), Px::MIN);
} }
#[test] #[test]
+13 -4
View File
@@ -363,26 +363,35 @@ pub(crate) fn outside(
rel_base: UiVec2, rel_base: UiVec2,
window: PxVec2, window: PxVec2,
) { ) {
let mut reasons = 0;
let mut why = |counter| {
bump(counter);
reasons += 1;
};
for axis in Axis::BOTH { for axis in Axis::BOTH {
let holds = holds[axis]; let holds = holds[axis];
let len = region[axis].len(); let len = region[axis].len();
let window = window[axis]; let window = window[axis];
if holds.region_len.is_some_and(|pinned| pinned != len) { if holds.region_len.is_some_and(|pinned| pinned != len) {
bump(Counter::OutsidePinnedLen); why(Counter::OutsidePinnedLen);
} }
if !holds.window.contains(window) { if !holds.window.contains(window) {
bump(Counter::OutsideWindow); why(Counter::OutsideWindow);
} }
if holds if holds
.rel_base .rel_base
.is_some_and(|pinned| pinned != rel_base[axis]) .is_some_and(|pinned| pinned != rel_base[axis])
{ {
bump(Counter::OutsideRelBase); why(Counter::OutsideRelBase);
} }
if !holds.region.contains(len.to_px(window)) { if !holds.region.contains(len.to_px(window)) {
bump(Counter::OutsideRegion); why(Counter::OutsideRegion);
} }
} }
// These four are `AxisHolds::contains`'s four clauses written out again,
// because the report wants which one refused rather than that one did. A
// clause added there and not here would leave a refusal unexplained.
debug_assert!(reasons > 0, "a reuse was refused for no reason counted");
bump(Counter::ReuseOutside); bump(Counter::ReuseOutside);
reuse(id, ReuseOutcome::Outside); reuse(id, ReuseOutcome::Outside);
} }
+35
View File
@@ -23,6 +23,14 @@ pub struct LayoutLen {
pub leftover: Weight, pub leftover: Weight,
} }
/// A bare number is pixels, which is the one length that needs no box to be
/// read in.
impl<N: UiNum> From<N> for Len {
fn from(value: N) -> Self {
Len::px(value.to_f32())
}
}
impl<N: UiNum> From<N> for LayoutLen { impl<N: UiNum> From<N> for LayoutLen {
fn from(value: N) -> Self { fn from(value: N) -> Self {
LayoutLen::px(value.to_f32()) LayoutLen::px(value.to_f32())
@@ -239,6 +247,12 @@ impl std::fmt::Display for Size {
impl std::fmt::Display for LayoutLen { impl std::fmt::Display for LayoutLen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// A part that is zero is left out, so a length that is zero all
// through would print as nothing -- which reads as no length at all
// wherever one is printed beside something that has none.
if *self == Self::ZERO {
return write!(f, "0 px;");
}
if self.px != Px::ZERO { if self.px != Px::ZERO {
write!(f, "{} px;", self.px)?; write!(f, "{} px;", self.px)?;
} }
@@ -253,3 +267,24 @@ impl std::fmt::Display for LayoutLen {
} }
impl_axis_index!(Size => LayoutLen); impl_axis_index!(Size => LayoutLen);
#[cfg(test)]
mod tests {
use super::*;
/// What a request prints as is how a failing case is read and written out
/// again, and a length that printed as nothing could not be told from a
/// widget that has no rule at all.
#[test]
fn every_length_prints_as_something() {
for len in [
LayoutLen::ZERO,
LayoutLen::px(8),
LayoutLen::rel(0.5),
LayoutLen::LEFTOVER,
] {
assert!(!len.to_string().is_empty(), "{len:?} printed as nothing");
}
assert_eq!(LayoutLen::ZERO.to_string(), "0 px;");
}
}
+17 -2
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive, Bounds, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign,
Size, TextureHandle, UiRegion, UiVec2, WidgetId, RetainedPrimitive, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -51,6 +51,8 @@ pub struct ActiveData {
/// An owned mask holds one reference independently of its primitives. /// An owned mask holds one reference independently of its primitives.
pub mask_region: Option<UiRegion>, pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
pub request_deps: Vec<WidgetId>,
pub(crate) scratch: DrawScratch,
/// The movable region its primitives are positioned through: its own when /// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's. /// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
@@ -58,6 +60,10 @@ pub struct ActiveData {
/// A change to one moves a box this widget cannot fix by drawing again, /// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so. /// and comparing them is what says so.
pub declared: Declared, pub declared: Declared,
/// Its bounds, resolved the same way. The answer is held to these where
/// the box was not, so a change to one changes what it answers even
/// where its declared lengths stand.
pub bounds: Bounds,
/// Its alignment when it was last drawn, which a change to the property /// Its alignment when it was last drawn, which a change to the property
/// is found against. /// is found against.
pub own_align: RegionAlign, pub own_align: RegionAlign,
@@ -97,3 +103,12 @@ pub struct Answer {
pub size: Size, pub size: Size,
pub holds: LayoutHolds, pub holds: LayoutHolds,
} }
#[derive(Debug, Default)]
pub(crate) struct DrawScratch {
pub children: Vec<WidgetId>,
pub size_deps: Vec<WidgetId>,
pub under: Vec<(WidgetId, LayoutHolds)>,
pub requests: Vec<crate::RequestedLen>,
pub lengths: Vec<crate::Px>,
}
+58 -1
View File
@@ -1,4 +1,4 @@
use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow}; use crate::{Bound, Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for: /// The lengths of a box, in pixels, that one drawing of a widget holds for:
@@ -19,6 +19,63 @@ pub struct Holds {
pub hi: Px, pub hi: Px,
} }
impl Len {
/// Whether this is longer than `than` in a window this wide, and the
/// windows that answer holds for.
///
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than
/// 300 px at a box of 600 and shorter at 400 -- and it is asked of the
/// difference and answered back through that same difference, so the
/// boundary is the comparison's own rather than a second way of finding
/// it.
pub fn longer_than(&self, than: Len, window: Px) -> (bool, Holds) {
let over = *self - than;
let longer = over.to_px(window) > Px::ZERO;
let side = match longer {
true => Px::STEP..=Px::MAX,
false => Px::MIN..=Px::ZERO,
};
(longer, Holds::from(side).through(over))
}
}
impl Bound {
/// The end of this bound `len` falls outside, which is the length it
/// gets instead of its own, and the windows that answer holds for.
/// Nothing where it is inside, which is the answer wherever there is no
/// bound at all.
///
/// `len` and this bound are lengths of the same thing, whichever that
/// is: a box in window lengths wants the bound resolved, and a length a
/// widget declares of its rel base wants it as the rule wrote it. Both
/// comparisons are in pixels, so each is a question about this window,
/// and the box is decided again on the other side of a crossing.
pub fn outside(&self, len: Len, window: Px) -> (Option<Len>, Holds) {
let mut held = None;
let mut holds = Holds::ANY;
if let Some(min) = self.min {
let (shorter, kept) = min.longer_than(len, window);
holds = holds.and(kept);
if shorter {
held = Some(min);
}
}
if let Some(max) = self.max {
let (longer, kept) = held.unwrap_or(len).longer_than(max, window);
holds = holds.and(kept);
if longer {
debug_assert!(
held.is_none(),
"a floor of {:?} over a cap of {max:?} bounds nothing",
self.min,
);
held = Some(max);
}
}
(held, holds)
}
}
impl Holds { impl Holds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
lo: Px::MIN, lo: Px::MIN,
+2 -6
View File
@@ -20,7 +20,7 @@ pub use active::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*; pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive}; pub use place::{PlaceDesc, PlaceDescAxis, PlaceFit, RetainedPrimitive};
pub use render_state::*; pub use render_state::*;
#[derive(Default)] #[derive(Default)]
@@ -103,11 +103,7 @@ impl Moves {
/// the shader's walk costs per primitive. /// the shader's walk costs per primitive.
pub fn depth(&self, idx: MoveIdx) -> usize { pub fn depth(&self, idx: MoveIdx) -> usize {
let mut depth = 0; let mut depth = 0;
let mut at = idx; self.walk(idx, |_| depth += 1);
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
at = self.arena[at.idx()].parent;
depth += 1;
}
depth depth
} }
+354 -114
View File
@@ -1,8 +1,9 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel, Axis, Bound, Bounds, Declared, DrawScratch, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle, PlaceFit, Px, PxVec2, RegionAlign, Rel, RenderedText, RequestArena, RequestedLen,
RetainedPrimitive, Size, SizeRequests, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
@@ -41,6 +42,8 @@ pub struct Painter<'a> {
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
pub(super) request_deps: Vec<WidgetId>,
pub(super) scratch: DrawScratch,
/// What this draw itself reads, as against what its children's drawings /// What this draw itself reads, as against what its children's drawings
/// hold for: every window and every length of its own region until it /// hold for: every window and every length of its own region until it
/// reads one, then that one unless it says otherwise, and the rel base or /// reads one, then that one unless it says otherwise, and the rel base or
@@ -62,6 +65,131 @@ pub struct Painter<'a> {
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
/// Reuses this widget's allocation buffers across draws. A child drawn
/// part way through gets a painter of its own, with buffers of its own,
/// so nothing it does while this one is mid-row can reach these.
pub fn with_requests<T>(
&mut self,
f: impl FnOnce(&mut Self, &mut Vec<RequestedLen>, &mut Vec<Px>) -> T,
) -> T {
let mut requests = std::mem::take(&mut self.scratch.requests);
let mut lengths = std::mem::take(&mut self.scratch.lengths);
requests.clear();
lengths.clear();
let result = f(self, &mut requests, &mut lengths);
self.scratch.requests = requests;
self.scratch.lengths = lengths;
result
}
/// Discovers a composable request without painting a provisional box.
pub fn size_request<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
) -> Option<RequestedLen> {
if let Some(len) = self.size_hint(child, axis) {
self.request_deps.push(child.id());
return Some(len.into());
}
let start = self.request_deps.len();
let mut requests = SizeRequests {
arena: &mut self.state.requests,
measured: None,
widgets: self.rsc.widgets(),
dependencies: &mut self.request_deps,
rel_base: self.rel_base[axis],
};
// Intrinsic fixed content must keep its offered box for wrapping;
// only a declaration or a share chooses the box it is drawn in.
let request = requests.widget(child, axis).filter(|request| {
request.has_leftover()
|| self.rsc.widgets().size_rules(child.id())[axis]
.request
.is_some()
});
if request.is_some() {
self.rel_base(axis);
} else {
// A discarded request contributes no dependency: the measured
// draw below records the size and box it actually used instead.
self.request_deps.truncate(start);
}
request
}
/// Completes discovery after a child was measured. Only this call may use
/// drawn answers: before the ask they could belong to an obsolete box.
pub fn measured_request<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
len: LayoutLen,
) -> RequestedLen {
let start = self.request_deps.len();
let bound = self.rsc.widgets().size_rules(child.id())[axis].bound;
let mut requests = SizeRequests {
arena: &mut self.state.requests,
measured: Some(&self.state.active),
widgets: self.rsc.widgets(),
dependencies: &mut self.request_deps,
rel_base: self.rel_base[axis],
};
if let Some(request) = requests.widget(child, axis)
&& request.linear().is_none()
&& request.has_leftover()
{
self.rel_base(axis);
return request;
}
let shares = len.leftover > Weight::ZERO;
let request = match shares {
true => requests.bounded(len.into(), bound),
false => len.into(),
};
self.request_deps.truncate(start);
// Only a bound that is a fraction was read against the rel base; one
// in pixels binds at the same length under any of them.
if shares && bound.has_fraction() {
self.rel_base(axis);
}
request
}
/// Divides `room` between `requests`, one length per request in
/// `output`. A deferred comparison is decided here, against this window:
/// which side of a crossing the solution falls is a question in pixels,
/// so the drawing holds only for the window that answered it.
pub fn allocate(
&mut self,
requests: &[RequestedLen],
room: Len,
axis: Axis,
output: &mut Vec<Px>,
) {
let window = self.window[axis];
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
output.clear();
output.extend(
self.state
.requests
.allocate(requests, room.to_px(window), window),
)
}
/// The least a request can come to, which is what it takes of the row
/// before anything is divided. A comparison is read at no share at all.
pub fn minimum_request(&mut self, request: &RequestedLen, axis: Axis) -> Len {
match request.linear() {
Some(len) => len.without_leftover(),
None => {
let window = self.window[axis];
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
Len::from_parts(Rel::ZERO, self.state.requests.minimum(*request, window))
}
}
}
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>(); let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region); self.write(kind, primitive, region);
@@ -187,22 +315,24 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>, place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let mut place = self.resolve_rel_base(place.into()); let offer = self.resolve_rel_base(place.into());
let Ask {
rel_base,
region,
place,
declared,
bounds,
holds: ask_holds,
inputs,
} = self.placing().ask(
self.rsc.widgets(),
&mut self.state.requests,
self.window,
id.id(),
offer,
);
self.own = self.own.and(inputs);
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let align = self.rsc.widgets().alignment(id.id());
// A share fills what the pixels and fraction beside it leave of the
// box and overflows where they are longer, which is the rule a span
// follows with one child. Only the overflow is a box of the child's
// own: a share that fits is the box it was given, which is what this
// place already says.
for axis in Axis::BOTH {
if let Some(len) = self.share_past_the_offer(id.id(), place, align, axis) {
place[axis] = len.as_desc().fills();
}
}
let declared = self.declared_lens(id);
let (rel_base, region) =
place.rel_base_and_region(self.region, self.rel_base, declared, align);
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
if region_node { if region_node {
diag::bump(Counter::RegionNodeDraws); diag::bump(Counter::RegionNodeDraws);
@@ -225,7 +355,10 @@ impl<'a> Painter<'a> {
rel_base, rel_base,
region, region,
placed: place, placed: place,
asked: place, asked: offer,
declared,
bounds,
ask_holds,
re_asked, re_asked,
}, },
None, None,
@@ -292,7 +425,7 @@ impl<'a> Painter<'a> {
/// This widget as the thing its children are placed within. /// This widget as the thing its children are placed within.
fn placing(&self) -> Placing { fn placing(&self) -> Placing {
Placing { Placing {
id: self.id, id: Some(self.id),
region: self.region, region: self.region,
rel_base: self.rel_base, rel_base: self.rel_base,
depth: self.depth, depth: self.depth,
@@ -301,59 +434,19 @@ impl<'a> Painter<'a> {
} }
} }
/// What a rule or a hint declares a widget's lengths to be, which whoever
/// draws it resolves into its rel base. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it
/// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> Declared {
self.rsc.widgets().declared_lens(id.id())
}
/// The box a child's own share asks for where that is longer than the box
/// `place` gives it, and nothing where the share fits.
///
/// A share is a length only to whoever divides one, and nothing divides a
/// box handed to one child: what is left of it after the pixels and the
/// fraction beside the share is what the share takes, so the length comes
/// to the whole box until those are longer than it and to them once they
/// are. Only that second case is a box this widget did not give, and the
/// crossing between them is a question in pixels, so this widget's drawing
/// holds for the windows on one side of it. Narrowed rather than stated,
/// because this widget may have read its own box as well, and a range it
/// pinned for that still holds.
fn share_past_the_offer(
&mut self,
id: WidgetId,
place: PlaceDesc,
align: RegionAlign,
axis: Axis,
) -> Option<Len> {
// A place that is the child's placement outright is a box its parent
// decided, and a parent that divides one has already given the share
// whatever it was owed. Only an offer -- a box with the answer still
// to be placed inside it -- is a box a share reads.
if place[axis].fills {
return None;
}
// A share with nothing beside it is the box whatever the box is, so
// there is no comparison to make and no range to keep for one.
let stated = self.rsc.widgets().exact_len(id, axis)?;
if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
return None;
}
let fixed = stated
.without_leftover()
.within_len(place.base(axis, self.rel_base));
let offer = place.of(self.region, align)[axis].len();
self.longer_than(fixed, offer, axis).then_some(fixed)
}
/// What a child says its length is without being drawn, if it can say, /// What a child says its length is without being drawn, if it can say,
/// as the length its draw would report: a fraction in it is resolved /// as the length its draw would report: a fraction in it is resolved
/// against this widget's rel base, which is the rel base a child asked with /// against this widget's rel base, which is the rel base a child asked with
/// nothing narrowed gets. Asking counts as reading its size. /// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let hint = self.rsc.widgets().exact_len(id.id(), axis); // A bound is composed into a request rather than applied to a hint,
// so a bounded widget cannot say its length without being asked: what
// it comes to is a comparison only the ask or the allocator makes.
// A miss rather than no read at all, so the counters see it.
let bounded = self.rsc.widgets().size_rules(id.id())[axis].bound != Bound::ANY;
let hint = (!bounded)
.then(|| self.rsc.widgets().exact_len(id.id(), axis))
.flatten();
let rel_base = self.rel_base[axis]; let rel_base = self.rel_base[axis];
let resolved = hint.map(|hint| hint.within_len(rel_base)); let resolved = hint.map(|hint| hint.within_len(rel_base));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -471,7 +564,7 @@ impl<'a> Painter<'a> {
/// depend on it. /// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool { pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc.widgets().size_rules(self.id)[axis] self.rsc.widgets().size_rules(self.id)[axis]
.exact() .request
.is_some() .is_some()
} }
@@ -524,30 +617,16 @@ impl<'a> Painter<'a> {
len.to_px(window) len.to_px(window)
} }
/// Whether `len` is longer than `than`, kept as the windows that comparison /// [`Len::longer_than`], asked on this widget's behalf: the windows the
/// comes out the same way on: a drawing that took one of two lengths holds /// comparison comes out the same way on are windows its drawing holds
/// where the same one is the longer, and nowhere else. /// for, and nowhere else does it. What a container has left for the
/// shares it divides is the one thing that asks.
/// ///
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than 300
/// px at a box of 600 and shorter at 400 -- and it is asked of the
/// difference and answered back through that same difference, so the
/// boundary is the drawing's own rather than a second way of finding it.
/// Narrowed rather than stated, because whatever else this widget read /// Narrowed rather than stated, because whatever else this widget read
/// about the window is a reason its drawing holds where it does too. /// about the window is a reason its drawing holds where it does too.
///
/// This is the one operation a length that is the longer of two needs: the
/// room a container has left for the shares it divides, and a share that
/// overflows the box it was given because the pixels beside it are longer
/// than the box.
pub fn longer_than(&mut self, len: Len, than: Len, axis: Axis) -> bool { pub fn longer_than(&mut self, len: Len, than: Len, axis: Axis) -> bool {
let over = len - than;
let window = self.window[axis]; let window = self.window[axis];
let longer = over.to_px(window) > Px::ZERO; let (longer, holds) = len.longer_than(than, window);
let side = match longer {
true => Px::STEP..=Px::MAX,
false => Px::MIN..=Px::ZERO,
};
let holds = Holds::from(side).through(over);
debug_assert!( debug_assert!(
holds.contains(window), holds.contains(window),
"'{}' ({:?}) compared two lengths and kept a range without this window", "'{}' ({:?}) compared two lengths and kept a range without this window",
@@ -729,7 +808,15 @@ impl Widgets {
/// share included, since a share is a length only to whoever divides one, /// share included, since a share is a length only to whoever divides one,
/// and that is the parent rather than this widget. /// and that is the parent rather than this widget.
fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> { fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
self.size_rules(id)[axis].exact().or_else(|| { // A request is a length the rule gives, and the hint below must not
// narrow the box in its place: what the request comes to is not known
// until the parent allocates, and it is the parent's answer, not this
// widget's.
let rule = &self.size_rules(id)[axis];
if rule.deferred().is_some() {
return None;
}
rule.exact().or_else(|| {
// A hint still narrows the box where no rule does, which is how a // A hint still narrows the box where no rule does, which is how a
// widget with a natural pixel size -- an image, a gap -- gets that // widget with a natural pixel size -- an image, a gap -- gets that
// size rather than the whole offer. That is the offer's business // size rather than the whole offer. That is the offer's business
@@ -738,11 +825,177 @@ impl Widgets {
self.get_dyn(id)?.size_hint(axis) self.get_dyn(id)?.size_hint(axis)
}) })
} }
}
/// What a widget's box is where a rule or its own hint gives one outright, /// One ask of a widget: the box it draws in, what its fractions are of, and
/// rather than a share for whoever draws it to divide. /// what deciding those read.
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared { pub(super) struct Ask {
Declared::from_axes(|axis| self.exact_len(id, axis)?.declared()) pub rel_base: UiVec2,
pub region: UiRegion,
/// The place the ask came to, which a rule of the widget's own can take
/// past the box its parent offered.
pub place: PlaceDesc,
/// What the widget's box is on each axis where something says so
/// outright: its rule, its hint, or a bound the offer fell outside.
/// An intrinsic answer can still occupy less than a capped box.
pub declared: Declared,
/// Its bounds, resolved against the rel base its rules were resolved
/// against, for the answer to be held to where the box was not.
pub bounds: Bounds,
/// What the ask itself holds for, kept on the widget asked about: a rule
/// compared against the offer in pixels holds only for the windows on its
/// side of the crossing, and that range reaches whoever asked through the
/// drawing it is part of. Kept on the widget asked about rather than on
/// the asker because the root has no asker.
pub holds: LayoutHolds,
/// Inputs read against the parent before declarations choose a new base.
/// These belong to the asker; the widget's own holds describe its output box.
pub inputs: LayoutHolds,
}
impl Placing {
/// Asks about a widget at `place` of this box, with the widget's own
/// rules applied to what the place offers it. `place` is resolved: what
/// a rel base of the asker's is a fraction of, the asker worked out.
///
/// Every ask is this one, the root's included -- there the box is the
/// window and nothing above narrowed it, which is what [`Self::WINDOW`]
/// says.
pub(super) fn ask(
&self,
widgets: &Widgets,
requests: &mut RequestArena,
window: PxVec2,
id: WidgetId,
mut place: PlaceDesc,
) -> Ask {
let align = widgets.alignment(id);
let rules = widgets.size_rules(id);
let mut holds = LayoutHolds::ANY;
let mut inputs = LayoutHolds::ANY;
let mut declared = Declared::NONE;
let mut bounds = Bounds::ANY;
for axis in Axis::BOTH {
let base = place.base(axis, self.rel_base);
let stated = widgets.exact_len(id, axis);
if let Some(len) = stated.and_then(|len| len.declared()) {
if len.rel != Rel::ZERO {
inputs[axis].rel_base = Some(self.rel_base[axis]);
}
declared[axis] = Some(len.within_len(base));
}
if let Some(request) = rules[axis].deferred() {
inputs[axis].rel_base = Some(self.rel_base[axis]);
inputs[axis].region_len = Some(self.region[axis].len());
inputs[axis].window = Holds::at(window[axis]);
let offer = place.of(self.region, align)[axis].len();
let px = if place[axis].fit == PlaceFit::Allocated {
offer.to_px(window[axis])
} else {
let request = requests.import(request, base);
let request = requests.bounded(request, rules[axis].bound.within_len(base));
holds[axis].window = Holds::at(window[axis]);
requests
.allocate(&[request], offer.to_px(window[axis]), window[axis])
.next()
.unwrap()
};
let len = Len::from_parts(Rel::ZERO, px);
place[axis].rel_base = RelBase::Len(len);
declared[axis] = Some(len);
holds[axis].rel_base = Some(len);
continue;
}
// A share fills what the pixels and fraction beside it leave of
// the box and overflows where they are longer, which is the rule
// a span follows with one child. Only the overflow is a box of
// the widget's own: a share that fits is the box it was given,
// which is what this place already says.
let (share, kept) = self.share_past_the_offer(stated, window[axis], place, align, axis);
holds[axis].window = holds[axis].window.and(kept);
if let Some(len) = share {
place[axis] = len.as_desc().fills();
}
let bound = rules[axis].bound;
if bound != Bound::ANY {
if bound.has_fraction() {
inputs[axis].rel_base = Some(self.rel_base[axis]);
}
// A solved slot may narrow the widget's rel base, but the
// allocator evaluated its bounds against this parent's base.
let bound_base = if place[axis].fit == PlaceFit::Allocated {
self.rel_base[axis]
} else {
base
};
bounds[axis] = bound.within_len(bound_base);
let offer =
declared[axis].unwrap_or_else(|| place.of(self.region, align)[axis].len());
let (held, kept) = bounds[axis].outside(offer, window[axis]);
holds[axis].window = holds[axis].window.and(kept);
// The comparison reads the incoming box, before a bound
// replaces it. Placement keeps that decision; only an ask
// may compare a new offer.
if declared[axis].is_none() {
inputs[axis].region_len = Some(self.region[axis].len());
}
if let Some(len) = held {
declared[axis] = Some(len);
}
}
}
let (rel_base, region) =
place.rel_base_and_region(self.region, self.rel_base, declared, align);
Ask {
rel_base,
region,
place,
declared,
bounds,
holds,
inputs,
}
}
/// The box a widget's own share asks for where that is longer than the
/// box `place` gives it, and nothing where the share fits -- with the
/// windows that answer holds for, which is a range either way.
///
/// A share is a length only to whoever divides one, and nothing divides a
/// box handed to one child: what is left of it after the pixels and the
/// fraction beside the share is what the share takes, so the length comes
/// to the whole box until those are longer than it and to them once they
/// are. Only that second case is a box its parent did not give, and the
/// crossing between them is a question in pixels.
fn share_past_the_offer(
&self,
stated: Option<LayoutLen>,
window: Px,
place: PlaceDesc,
align: RegionAlign,
axis: Axis,
) -> (Option<Len>, Holds) {
// A place that is the widget's placement outright is a box its parent
// decided, and a parent that divides one has already given the share
// whatever it was owed. Only an offer -- a box with the answer still
// to be placed inside it -- is a box a share reads.
if place[axis].fit.fills() {
return (None, Holds::ANY);
}
// A share with nothing beside it is the box whatever the box is, so
// there is no comparison to make and no range to keep for one.
let Some(stated) = stated else {
return (None, Holds::ANY);
};
if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
return (None, Holds::ANY);
}
let fixed = stated
.without_leftover()
.within_len(place.base(axis, self.rel_base));
let offer = place.of(self.region, align)[axis].len();
let (longer, holds) = fixed.longer_than(offer, window);
(longer.then_some(fixed), holds)
} }
} }
@@ -750,12 +1003,10 @@ impl LayoutLen {
/// Whether what a widget reported along an axis is the whole of the box /// Whether what a widget reported along an axis is the whole of the box
/// it is in rather than a part to be placed inside it. A share fills, /// it is in rather than a part to be placed inside it. A share fills,
/// because a share is a length only to whoever divides one, and whoever /// because a share is a length only to whoever divides one, and whoever
/// did is the one that handed down this box. A declared axis does too: /// did is the one that handed down this box. An axis the parent decided
/// the rule already gave the region its length, and the rule's length is /// from the answer is the answer already.
/// what the widget reports there. And an axis the parent decided from pub(super) fn fills(&self, decided: bool) -> bool {
/// the answer is the answer already. self.leftover != Weight::ZERO || decided
pub(super) fn fills(&self, declared: Option<Len>, decided: bool) -> bool {
self.leftover != Weight::ZERO || declared.is_some() || decided
} }
} }
@@ -769,17 +1020,11 @@ impl PlaceDesc {
/// part. That is what makes a fraction the same fraction wherever the part /// part. That is what makes a fraction the same fraction wherever the part
/// it is placed in sits and however long it is -- the fraction is resolved /// it is placed in sits and however long it is -- the fraction is resolved
/// once, here, against the rel base it was reported of. /// once, here, against the rel base it was reported of.
pub(super) fn placement( pub(super) fn placement(self, region: UiRegion, size: Size, align: RegionAlign) -> UiRegion {
self,
region: UiRegion,
size: Size,
declared: Declared,
align: RegionAlign,
) -> UiRegion {
let mut placed = region; let mut placed = region;
for axis in Axis::BOTH { for axis in Axis::BOTH {
let reported = size[axis]; let reported = size[axis];
if reported.fills(declared[axis], self[axis].fills) { if reported.fills(self[axis].fit.fills()) {
continue; continue;
} }
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]); placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
@@ -792,11 +1037,8 @@ impl PlaceDesc {
/// ///
/// `own` is that widget's own box, and `place` what of it the child is /// `own` is that widget's own box, and `place` what of it the child is
/// given, including any rel base it states -- a row's slot, or padding's rel /// given, including any rel base it states -- a row's slot, or padding's rel
/// base less its pixels. That is a window length, like every other length /// base less its pixels. The ask has already resolved declarations into
/// here, since a slot of a row is not a fraction of anything the row can /// window lengths, so placement only aligns them inside the given box.
/// name. The child's declaration is a fraction of whichever reached it, and
/// is the only one that also places the box: a box the caller decided is
/// what `place` names.
pub(super) fn rel_base_and_region( pub(super) fn rel_base_and_region(
self, self,
own: UiRegion, own: UiRegion,
@@ -809,9 +1051,7 @@ impl PlaceDesc {
let mut region = given; let mut region = given;
for axis in Axis::BOTH { for axis in Axis::BOTH {
let base = self.base(axis, parent_rel_base); let base = self.base(axis, parent_rel_base);
let len = declared[axis] let len = declared[axis].unwrap_or(base);
.map(|len| len.within_len(base))
.unwrap_or(base);
rel_base[axis] = len; rel_base[axis] = len;
if declared[axis].is_some() { if declared[axis].is_some() {
region[axis] = given[axis].place(len, align[axis]); region[axis] = given[axis].place(len, align[axis]);
+25 -5
View File
@@ -12,10 +12,24 @@ use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDescAxis { pub struct PlaceDescAxis {
pub span: PlaceSpan, pub span: PlaceSpan,
pub fills: bool, pub fit: PlaceFit,
pub rel_base: RelBase, pub rel_base: RelBase,
} }
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlaceFit {
Align,
Fill,
/// The parent has already evaluated the child's size request.
Allocated,
}
impl PlaceFit {
pub fn fills(&self) -> bool {
!matches!(self, Self::Align)
}
}
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlaceSpan { pub enum PlaceSpan {
Within(UiSpan), Within(UiSpan),
@@ -44,7 +58,13 @@ impl PlaceDescAxis {
/// it again. A container uses it where it hands back exactly what the /// it again. A container uses it where it hands back exactly what the
/// child asked for -- a row placing a child at the length it reported. /// child asked for -- a row placing a child at the length it reported.
pub const fn fills(mut self) -> Self { pub const fn fills(mut self) -> Self {
self.fills = true; self.fit = PlaceFit::Fill;
self
}
/// A final allocation, including any comparisons in the child's request.
pub const fn allocated(mut self) -> Self {
self.fit = PlaceFit::Allocated;
self self
} }
@@ -153,7 +173,7 @@ impl UiSpan {
pub const fn within_desc(self) -> PlaceDescAxis { pub const fn within_desc(self) -> PlaceDescAxis {
PlaceDescAxis { PlaceDescAxis {
span: PlaceSpan::Within(self), span: PlaceSpan::Within(self),
fills: false, fit: PlaceFit::Align,
rel_base: RelBase::WithRegion, rel_base: RelBase::WithRegion,
} }
} }
@@ -171,7 +191,7 @@ impl UiSpan {
pub const fn shifted_desc(self) -> PlaceDescAxis { pub const fn shifted_desc(self) -> PlaceDescAxis {
PlaceDescAxis { PlaceDescAxis {
span: PlaceSpan::Shifted(self), span: PlaceSpan::Shifted(self),
fills: false, fit: PlaceFit::Align,
rel_base: RelBase::Inherit, rel_base: RelBase::Inherit,
} }
} }
@@ -185,7 +205,7 @@ impl Len {
pub const fn as_desc(self) -> PlaceDescAxis { pub const fn as_desc(self) -> PlaceDescAxis {
PlaceDescAxis { PlaceDescAxis {
span: PlaceSpan::Sized(self), span: PlaceSpan::Sized(self),
fills: false, fit: PlaceFit::Align,
rel_base: RelBase::Len(self), rel_base: RelBase::Len(self),
} }
} }
+265 -124
View File
@@ -1,9 +1,10 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::{ use crate::{
ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, ActiveData, Answer, Axis, Bounds, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len,
MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, MaskIdx, MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Size, StrongWidget, UiRegion,
UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
ui::painter::Ask,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -23,11 +24,18 @@ pub(super) struct DrawInfo {
/// The box the widget is asked in, in its parent region node's /// The box the widget is asked in, in its parent region node's
/// coordinates. /// coordinates.
pub region: UiRegion, pub region: UiRegion,
/// Where the widget is put, and where it was asked, as parts of the /// Where the widget is put, and what its parent offered it, as parts of
/// parent's box. See [`PlaceDesc`]. The two are one ask's place until the /// the parent's box. See [`PlaceDesc`]. The two are one place until a
/// parent puts the answer somewhere else. /// rule of the widget's own takes it past the offer, or the parent puts
/// the answer somewhere else.
pub placed: PlaceDesc, pub placed: PlaceDesc,
pub asked: PlaceDesc, pub asked: PlaceDesc,
/// What the ask made of the widget's own rules. See [`Ask::declared`]
/// and [`Ask::bounds`].
pub declared: Declared,
pub bounds: Bounds,
/// What the ask that gave it those two holds for. See [`Ask::holds`].
pub ask_holds: LayoutHolds,
/// Whether the parent already asked about this widget in this draw. /// Whether the parent already asked about this widget in this draw.
pub re_asked: bool, pub re_asked: bool,
} }
@@ -43,7 +51,8 @@ pub(super) struct Drawn {
/// What a widget's children are placed in: its own box, the coordinates its /// What a widget's children are placed in: its own box, the coordinates its
/// drawing is in, and what else one ask of a child is decided from. /// drawing is in, and what else one ask of a child is decided from.
pub(super) struct Placing { pub(super) struct Placing {
pub id: WidgetId, /// The widget whose box this is, and nothing for the window.
pub id: Option<WidgetId>,
pub region: UiRegion, pub region: UiRegion,
pub rel_base: UiVec2, pub rel_base: UiVec2,
pub depth: usize, pub depth: usize,
@@ -51,6 +60,21 @@ pub(super) struct Placing {
pub mask: MaskIdx, pub mask: MaskIdx,
} }
impl Placing {
/// The window, which is what the root is placed within. Nothing above the
/// root narrowed a box or chose where it goes, so it is asked in the whole
/// output and its fractions are of the whole output -- an ordinary ask,
/// from the one box nobody drew.
pub const WINDOW: Self = Self {
id: None,
region: UiRegion::FULL,
rel_base: UiVec2::FULL_SIZE,
depth: 0,
move_idx: MoveIdx::NONE,
mask: MaskIdx::NONE,
};
}
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers, pub layers: DrawLayers,
@@ -70,7 +94,10 @@ pub struct UiRenderState {
deferred: crate::util::HashSet<WidgetId>, deferred: crate::util::HashSet<WidgetId>,
/// What the walk has left to settle, deepest last. Ordered rather than /// What the walk has left to settle, deepest last. Ordered rather than
/// searched for, so finding the next one is not a pass over the marks. /// searched for, so finding the next one is not a pass over the marks.
pending: std::collections::BTreeSet<(usize, WidgetId)>, pending: std::collections::BinaryHeap<(usize, WidgetId)>,
pub(super) requests: crate::RequestArena,
changed: Vec<WidgetId>,
request_readers: HashMap<WidgetId, crate::util::HashSet<WidgetId>>,
pub moves: Moves, pub moves: Moves,
} }
@@ -84,6 +111,9 @@ impl UiRenderState {
slots: Default::default(), slots: Default::default(),
deferred: Default::default(), deferred: Default::default(),
pending: Default::default(), pending: Default::default(),
requests: Default::default(),
changed: Vec::new(),
request_readers: Default::default(),
moves: Default::default(), moves: Default::default(),
resized: false, resized: false,
} }
@@ -124,20 +154,23 @@ impl UiRenderState {
} }
} }
/// The root is asked about in the output. Its own rules narrow both its /// The root's first draw: the ask [`Placing::WINDOW`] answered, with the
/// rel base and box; nothing above it chose a different one. /// bookkeeping a widget with no parent carries.
fn root_info(&self, rel_base: UiVec2, region: UiRegion) -> DrawInfo { fn root_info(&self, ask: &Ask, region_node: bool) -> DrawInfo {
DrawInfo { DrawInfo {
layer: 0, layer: 0,
parent: None, parent: None,
depth: 1, depth: Placing::WINDOW.depth + 1,
parent_move: MoveIdx::NONE, parent_move: MoveIdx::NONE,
region_node: false, region_node,
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
rel_base, rel_base: ask.rel_base,
region, region: ask.region,
placed: PlaceDesc::WHOLE, placed: ask.place,
asked: PlaceDesc::WHOLE, asked: PlaceDesc::WHOLE,
declared: ask.declared,
bounds: ask.bounds,
ask_holds: ask.holds,
re_asked: false, re_asked: false,
} }
} }
@@ -167,6 +200,7 @@ impl UiRenderState {
weak widgets: {all:#?}" weak widgets: {all:#?}"
); );
} }
self.requests.reset();
let root = root.into(); let root = root.into();
if self.root_changed(root) { if self.root_changed(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
@@ -184,24 +218,18 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout); let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc); self.clear(rsc);
if let Some(id) = root { if let Some(id) = root {
let (rel_base, region) = Self::root_layout(id.id(), rsc.widgets()); let ask = Placing::WINDOW.ask(
let info = self.root_info(rel_base, region); rsc.widgets(),
&mut self.requests,
self.output_size,
id.id(),
PlaceDesc::WHOLE,
);
let info = self.root_info(&ask, rsc.widgets().is_region_node(id.id()));
self.draw_inner(id.id(), info, None, rsc); self.draw_inner(id.id(), info, None, rsc);
} }
} }
/// The root's rel base and box: the window, taken in by the root's own
/// rules. Nothing above it narrowed anything or chose where it goes, so
/// its declaration is the whole of what decides either.
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
PlaceDesc::WHOLE.rel_base_and_region(
UiRegion::FULL,
UiVec2::FULL_SIZE,
widgets.declared_lens(id),
widgets.alignment(id),
)
}
pub(super) fn draw_inner( pub(super) fn draw_inner(
&mut self, &mut self,
id: WidgetId, id: WidgetId,
@@ -226,7 +254,6 @@ impl UiRenderState {
); );
} }
let align = rsc.widgets().alignment(id); let align = rsc.widgets().alignment(id);
let declared = rsc.widgets().declared_lens(id);
// Nothing this widget measured can be dirty while it draws: layout is // Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to // one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked. // its own parent, and a deferred one leaves that parent marked.
@@ -240,7 +267,7 @@ impl UiRenderState {
.then(|| self.retained_answer(id, region, info)) .then(|| self.retained_answer(id, region, info))
.flatten() .flatten()
.and_then(|answer| { .and_then(|answer| {
let placed = info.placed.placement(region, answer.size, declared, align); let placed = info.placed.placement(region, answer.size, align);
self.try_reuse(id, region, placed, info, rsc) self.try_reuse(id, region, placed, info, rsc)
.then_some(answer) .then_some(answer)
}); });
@@ -252,7 +279,7 @@ impl UiRenderState {
// Where the drawing goes: the part its parent gave it, with the // Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left // answer placed inside that part on any axis the parent left
// open. // open.
let placed = info.placed.placement(region, answer.size, declared, align); let placed = info.placed.placement(region, answer.size, align);
if placed != region { if placed != region {
self.relocate(id, placed, info, rsc); self.relocate(id, placed, info, rsc);
} }
@@ -311,7 +338,27 @@ impl UiRenderState {
let mask_slot = old let mask_slot = old
.as_ref() .as_ref()
.and_then(|old| old.mask_region.map(|_| old.mask)); .and_then(|old| old.mask_region.map(|_| old.mask));
let old_children = old.map_or_else(Vec::new, |old| old.children); let (mut old_children, textures, primitives, request_deps, mut scratch) = match old {
Some(old) => (
old.children,
old.textures,
old.primitives,
old.request_deps,
old.scratch,
),
None => Default::default(),
};
// Every one of these is a buffer this widget's last draw filled and
// `remove` emptied, kept for its capacity alone. A drawing whose
// primitives were still in it would record them twice.
debug_assert!(
textures.is_empty() && primitives.is_empty() && request_deps.is_empty(),
"'{}' ({id:?}) was drawn again over what its last draw left",
rsc.widgets().label(id)
);
let children = std::mem::take(&mut scratch.children);
let size_deps = std::mem::take(&mut scratch.size_deps);
let under = std::mem::take(&mut scratch.under);
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
let window = self.output_size; let window = self.output_size;
let mut painter = Painter { let mut painter = Painter {
@@ -323,14 +370,16 @@ impl UiRenderState {
layer: info.layer, layer: info.layer,
own_layer: info.layer, own_layer: info.layer,
id, id,
textures: Vec::new(), textures,
primitives: Vec::new(), primitives,
mask_region: None, mask_region: None,
mask_slot, mask_slot,
children: Vec::new(), children,
size_deps: Vec::new(), size_deps,
request_deps,
scratch,
own: LayoutHolds::ANY, own: LayoutHolds::ANY,
under: Vec::new(), under,
answer_under: LayoutHolds::ANY, answer_under: LayoutHolds::ANY,
depth: info.depth, depth: info.depth,
move_idx, move_idx,
@@ -362,8 +411,10 @@ impl UiRenderState {
own, own,
answer_under, answer_under,
children, children,
size_deps, mut size_deps,
under, request_deps,
mut scratch,
mut under,
move_idx, move_idx,
layer, layer,
own_layer: _, own_layer: _,
@@ -379,23 +430,66 @@ impl UiRenderState {
// A rule wins on the axis it names, and the draw answers the rest. // A rule wins on the axis it names, and the draw answers the rest.
// Applied here so it is one place rather than every widget that could // Applied here so it is one place rather than every widget that could
// carry one, and so the widget under a rule never learns of it. The // carry one, and so the widget under a rule never learns of it. The
// rel base is the answer where the rule gave a length outright: it was // rel base is the answer wherever the ask declared a length: it was
// resolved into the rel base when the child was asked, and resolving it // resolved into the rel base when the widget was asked, and resolving
// again here would take the fraction of a fraction. // it again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id); let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() { let ruled = |axis: Axis, reported: LayoutLen| {
None => reported, if rules[axis].deferred().is_some() {
Some(len) if len.leftover == Weight::ZERO => LayoutLen { return info.rel_base[axis].into();
rel: info.rel_base[axis].rel, }
px: info.rel_base[axis].px, match rules[axis].exact() {
leftover: Weight::ZERO, None => reported,
}, Some(len) if len.leftover == Weight::ZERO => LayoutLen {
Some(len) => len.within_len(info.rel_base[axis]), rel: info.rel_base[axis].rel,
px: info.rel_base[axis].px,
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.rel_base[axis]),
}
}; };
let size = Size { let mut size = Size {
x: ruled(Axis::X, size.x), x: ruled(Axis::X, size.x),
y: ruled(Axis::Y, size.y), y: ruled(Axis::Y, size.y),
}; };
// What the drawing read is combined with what the ask decided rather
// than replacing it: a widget may widen its own ranges, and cannot
// widen the ask's.
let mut own_holds = own.and(info.ask_holds);
for axis in Axis::BOTH {
// A rule that is a fraction of the rel base is answered with the
// rel base's own length, so the answer is that rel base's and not
// just that many pixels of this window -- the same pin a widget
// that read its rel base took for its drawing. A bound counts:
// which side of it the box fell was decided against this rel
// base, and the same box of a different one can fall on the other.
if rules[axis].has_fraction() {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
// A bound is a promise about the length as well as about the box:
// a widget that drew past the box it was given -- a text too tall
// for it, an image at its own size under a cap -- is still held to
// what its rule allows.
//
// Held here rather than taken from the box, even where the bound
// decided that box. What a widget answers is its own, and a bound
// that replaced the answer would make a share into a fixed length
// the moment a box was long enough -- which is a length the span
// dividing that box decided from this answer, so the two would
// choose each other. A share is left alone here for the same
// reason: it is a length only to whoever divides one, and the box
// that divider gives is a box this widget is asked in, where the
// bound is applied to it.
let answer = size[axis];
if answer.leftover != Weight::ZERO {
continue;
}
let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]);
own_holds[axis].window = own_holds[axis].window.and(kept);
if let Some(held) = held {
size[axis] = held.into();
}
}
// A widget that clipped its contents to its box drew nothing outside // A widget that clipped its contents to its box drew nothing outside
// it, so reporting more than the box asks to be placed at a length it // it, so reporting more than the box asks to be placed at a length it
// does not occupy -- and its parent would place the part it cut off. // does not occupy -- and its parent would place the part it cut off.
@@ -423,21 +517,10 @@ impl UiRenderState {
if let Some(idx) = retired_move { if let Some(idx) = retired_move {
self.moves.remove(idx); self.moves.remove(idx);
} }
// A rule that is a fraction of the rel base is answered with the
// rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing.
let mut own_holds = own;
for axis in Axis::BOTH {
let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
if fraction {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
}
let answer_holds = own_holds.and(answer_under); let answer_holds = own_holds.and(answer_under);
let holds = under let holds = under
.into_iter() .iter()
.fold(answer_holds, |holds, (_, child)| holds.and(child)); .fold(answer_holds, |holds, (_, child)| holds.and(*child));
debug_assert!( debug_assert!(
holds.contains(self.output_size, info.rel_base, region), holds.contains(self.output_size, info.rel_base, region),
"'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}", "'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}",
@@ -462,6 +545,9 @@ impl UiRenderState {
region: UiRegion::FULL, region: UiRegion::FULL,
placed: PlaceDesc::WHOLE, placed: PlaceDesc::WHOLE,
asked: PlaceDesc::WHOLE, asked: PlaceDesc::WHOLE,
declared: Declared::NONE,
bounds: Bounds::ANY,
ask_holds: LayoutHolds::ANY,
re_asked: false, re_asked: false,
}, },
rsc, rsc,
@@ -470,6 +556,15 @@ impl UiRenderState {
} }
} }
for &dep in &request_deps {
self.request_readers.entry(dep).or_default().insert(id);
}
old_children.clear();
size_deps.clear();
under.clear();
scratch.children = old_children;
scratch.size_deps = size_deps;
scratch.under = under;
let active = ActiveData { let active = ActiveData {
id, id,
placement: region, placement: region,
@@ -489,7 +584,10 @@ impl UiRenderState {
primitives, primitives,
mask_region, mask_region,
children, children,
declared: rsc.widgets().declared_lens(id), request_deps,
scratch,
declared: info.declared,
bounds: info.bounds,
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
move_idx, move_idx,
parent_move: info.parent_move, parent_move: info.parent_move,
@@ -534,6 +632,7 @@ impl UiRenderState {
if !active.drawn if !active.drawn
|| active.is_region_node() != info.region_node || active.is_region_node() != info.region_node
|| active.parent_move != info.parent_move || active.parent_move != info.parent_move
|| active.bounds != info.bounds
{ {
return None; return None;
} }
@@ -659,6 +758,9 @@ impl UiRenderState {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.rel_base = info.rel_base; active.rel_base = info.rel_base;
active.placed = info.placed; active.placed = info.placed;
// What the ask made of its rules, which a re-place decides again.
active.declared = info.declared;
active.bounds = info.bounds;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
let (counter, outcome) = match (moved, is_region_node) { let (counter, outcome) = match (moved, is_region_node) {
@@ -692,12 +794,11 @@ impl UiRenderState {
let placed = place.placement( let placed = place.placement(
region, region,
active.measured().unwrap_or(active.size), active.measured().unwrap_or(active.size),
active.declared,
active.own_align, active.own_align,
); );
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: Some(at.id), parent: at.id,
depth: at.depth + 1, depth: at.depth + 1,
parent_move: at.move_idx, parent_move: at.move_idx,
region_node: active.is_region_node(), region_node: active.is_region_node(),
@@ -706,6 +807,10 @@ impl UiRenderState {
region, region,
placed: place, placed: place,
asked: active.asked, asked: active.asked,
declared: active.declared,
bounds: active.bounds,
// Placing decides no box: this is the one the ask already gave.
ask_holds: LayoutHolds::ANY,
re_asked: active.re_asked, re_asked: active.re_asked,
}; };
self.relocate(child, placed, info, rsc); self.relocate(child, placed, info, rsc);
@@ -713,8 +818,8 @@ impl UiRenderState {
/// The rel base and the box a widget already drawn is given at `place` of /// The rel base and the box a widget already drawn is given at `place` of
/// the box its parent is being taken as. What narrowed its rel base and what /// the box its parent is being taken as. What narrowed its rel base and what
/// it declared are its own record's, so both are resolved against that /// it declared are its own record's. Declared lengths stay as the ask
/// parent's rel base again exactly as the first ask resolved them. /// resolved them; the destination only decides where they sit.
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) { fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align) place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
} }
@@ -734,7 +839,7 @@ impl UiRenderState {
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed); rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed);
} }
let at = Placing { let at = Placing {
id, id: Some(id),
region: placed, region: placed,
rel_base: info.rel_base, rel_base: info.rel_base,
depth: info.depth, depth: info.depth,
@@ -783,6 +888,11 @@ impl UiRenderState {
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id); let mut active = self.active.remove(&id);
if let Some(active) = &mut active { if let Some(active) = &mut active {
for dep in active.request_deps.drain(..) {
if let Some(readers) = self.request_readers.get_mut(&dep) {
readers.remove(&id);
}
}
for primitive in &active.primitives { for primitive in &active.primitives {
let mask = self.layers.free(&primitive.handle); let mask = self.layers.free(&primitive.handle);
if mask != MaskIdx::NONE { if mask != MaskIdx::NONE {
@@ -856,8 +966,11 @@ impl UiRenderState {
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None, mask_region: None,
children: Vec::new(), children: Vec::new(),
request_deps: Vec::new(),
scratch: Default::default(),
move_idx: info.parent_move, move_idx: info.parent_move,
declared: Declared::NONE, declared: Declared::NONE,
bounds: Bounds::ANY,
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
parent_move: info.parent_move, parent_move: info.parent_move,
mask: info.mask, mask: info.mask,
@@ -868,6 +981,7 @@ impl UiRenderState {
} }
fn clear(&mut self, rsc: &mut dyn UiRsc) { fn clear(&mut self, rsc: &mut dyn UiRsc) {
self.request_readers.clear();
for (_, active) in self.active.drain() { for (_, active) in self.active.drain() {
if active.drawn { if active.drawn {
rsc.on_undraw(&active); rsc.on_undraw(&active);
@@ -888,6 +1002,7 @@ impl UiRenderState {
rsc.on_remove(id); rsc.on_remove(id);
self.remove(id, true, rsc); self.remove(id, true, rsc);
self.drop_slot(id); self.drop_slot(id);
self.request_readers.remove(&id);
} }
rsc.ui_mut().textures.free(); rsc.ui_mut().textures.free();
} }
@@ -895,6 +1010,18 @@ impl UiRenderState {
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::IncrementalLayout); let _layout = diag::timer(TimerKind::IncrementalLayout);
self.changed.clear();
self.changed
.extend(rsc.widgets().needs_redraw.iter().copied());
while let Some(id) = self.changed.pop() {
if let Some(readers) = self.request_readers.get(&id) {
for &reader in readers {
if rsc.widgets_mut().needs_redraw.insert(reader) {
self.changed.push(reader);
}
}
}
}
// Deepest first, and strictly: a widget that cannot settle where it // Deepest first, and strictly: a widget that cannot settle where it
// is defers to its parent rather than drawing the parent from // is defers to its parent rather than drawing the parent from
// inside itself. It marks the parent, stays marked, and waits here // inside itself. It marks the parent, stays marked, and waits here
@@ -906,21 +1033,21 @@ impl UiRenderState {
// something below is about to change it -- which is the whole class // something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears // of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved. // its mark there, and tells nobody its answer moved.
// The queue is that set, ordered: a mark made while the walk runs // A mark made while the walk runs queues itself through `mark`. What
// queues itself through `mark`. What ends the walk is still the set // ends the walk is the marks being spent rather than the queue being
// being spent, not the queue, so a mark that reached it another way // empty, so a mark that reached the queue twice, or that was settled
// cannot be left for the next frame. // another way, costs a pop and nothing else.
loop { loop {
for &id in rsc.widgets().needs_redraw.iter() { for &id in rsc.widgets().needs_redraw.iter() {
if !self.deferred.contains(&id) { if !self.deferred.contains(&id) {
let depth = self.depth(id); let depth = self.depth(id);
self.pending.insert((depth, id)); self.pending.push((depth, id));
} }
} }
if self.pending.is_empty() { if self.pending.is_empty() {
break; break;
} }
while let Some((depth, id)) = self.pending.pop_last() { while let Some((depth, id)) = self.pending.pop() {
// Settled inside an ancestor's draw, or deferred to one, // Settled inside an ancestor's draw, or deferred to one,
// since the mark that queued it. // since the mark that queued it.
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) { if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
@@ -931,7 +1058,7 @@ impl UiRenderState {
// depth it had under the parent it left. // depth it had under the parent it left.
let now = self.depth(id); let now = self.depth(id);
if now != depth { if now != depth {
self.pending.insert((now, id)); self.pending.push((now, id));
continue; continue;
} }
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -948,7 +1075,7 @@ impl UiRenderState {
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) { fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) { if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
let depth = self.depth(id); let depth = self.depth(id);
self.pending.insert((depth, id)); self.pending.push((depth, id));
} }
} }
@@ -1042,15 +1169,32 @@ impl UiRenderState {
let Some(active) = self.active.get(&id) else { let Some(active) = self.active.get(&id) else {
return true; return true;
}; };
// Its parent resolved its declared lengths into its box and decided // Asked where its parent asked it, which is what says whether the
// whether to draw it at all, so a change to either is the parent's // question is still this widget's own: its parent resolved its
// to draw -- with the mark left on, so the parent draws it rather // declared lengths into its box -- a bound of its own that the box
// than keeping it. So is a widget the parent asked twice: its // falls outside is one of them -- and decided whether to draw it at
// layout rests on an answer this widget cannot give again alone. // all, so a change to either is the parent's to draw, with the mark
let declared_changed = rsc.widgets().declared_lens(id) != active.declared; // left on so the parent draws it rather than keeping it. So is a
// widget the parent asked twice: its layout rests on an answer this
// widget cannot give again alone. The root's parent is the window,
// which no draw made and no answer can move.
let at = match active.parent {
Some(parent) => self.placing_of(parent, self.active[&parent].region),
None => Placing::WINDOW,
};
let ask = at.ask(
rsc.widgets(),
&mut self.requests,
self.output_size,
id,
active.asked,
);
let active = &self.active[&id];
// Even an inactive bound changes what the parent must track about its offer.
let constraints_changed = ask.declared != active.declared || ask.bounds != active.bounds;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align; let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent if let Some(parent) = active.parent
&& (declared_changed && (constraints_changed
|| alignment_changed || alignment_changed
|| active.re_asked || active.re_asked
|| !active.drawn || !active.drawn
@@ -1066,29 +1210,13 @@ impl UiRenderState {
if !active.drawn { if !active.drawn {
return true; return true;
} }
// Nothing above the root resolved its rules or its alignment, so its
// box is its own to work out again against the output. Every other
// widget was given one.
let Some(parent) = active.parent else {
let (rel_base, region) = Self::root_layout(id, rsc.widgets());
let info = DrawInfo {
mask: active.parent_mask,
..self.root_info(rel_base, region)
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
self.draw_inner(id, info, old, rsc);
return true;
};
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed); 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 // The place the ask above came to: the same place of the box the
// box the parent was asked in, which is the box the parent's own // parent was asked in, which is the box the parent's own draw ran in
// draw ran in and what its children's parts are of. Where the // and what its children's parts are of. Where the parent's answer put
// parent's answer put its own drawing is not a question anybody // its own drawing is not a question anybody asked, and nothing is
// asked, and nothing is asked in it here either. // asked in it here either.
let parent_at = self.placing_of(parent, self.active[&parent].region); let (rel_base, region) = (ask.rel_base, ask.region);
let (rel_base, region) = Self::ask_again(active, &parent_at, active.asked);
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: active.parent, parent: active.parent,
@@ -1098,8 +1226,11 @@ impl UiRenderState {
mask: active.parent_mask, mask: active.parent_mask,
rel_base, rel_base,
region, region,
placed: active.asked, placed: ask.place,
asked: active.asked, asked: active.asked,
declared: ask.declared,
bounds: ask.bounds,
ask_holds: ask.holds,
re_asked: false, re_asked: false,
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -1127,21 +1258,31 @@ impl UiRenderState {
if active.holds.covers(was_holds) && was_holds.contains(window, rel_base, region) { if active.holds.covers(was_holds) && was_holds.contains(window, rel_base, region) {
active.holds = was_holds; active.holds = was_holds;
} }
if active.answer != was_answer || active.holds != was_holds { let changed = active.answer != was_answer || active.holds != was_holds;
// The parent retains both the answer and the drawing's validity; // Nothing above the root retained either, so there is nobody to tell
// even an unchanged size can narrow the range safe for a resize. // and nowhere else the drawing has to go back to.
#[cfg(feature = "layout-diagnostics")] if let Some(parent) = active.parent {
{ match changed {
diag::bump(Counter::SizeChanges); // The parent retains both the answer and the drawing's
diag::bump(Counter::ReaderEdges); // validity; even an unchanged size can narrow the range safe
// for a resize.
true => {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::SizeChanges);
diag::bump(Counter::ReaderEdges);
}
self.mark(parent, rsc.widgets_mut());
}
// 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.
false => {
let at = self.placing_of(parent, self.active[&parent].placement);
self.place_in(id, &at, was_place, rsc);
}
} }
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].placement);
self.place_in(id, &at, was_place, rsc);
} }
true true
} }
@@ -1152,7 +1293,7 @@ impl UiRenderState {
fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing { fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing {
let active = &self.active[&id]; let active = &self.active[&id];
Placing { Placing {
id, id: Some(id),
region, region,
rel_base: active.rel_base, rel_base: active.rel_base,
depth: active.depth, depth: active.depth,
+8
View File
@@ -4,6 +4,7 @@ use std::any::Any;
mod data; mod data;
mod handle; mod handle;
mod like; mod like;
mod request;
mod size_rule; mod size_rule;
mod tag; mod tag;
mod view; mod view;
@@ -12,6 +13,7 @@ mod widgets;
pub use data::*; pub use data::*;
pub use handle::*; pub use handle::*;
pub use like::*; pub use like::*;
pub use request::*;
pub use size_rule::*; pub use size_rule::*;
pub use tag::*; pub use tag::*;
pub use view::*; pub use view::*;
@@ -21,6 +23,12 @@ pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. /// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size; fn draw(&mut self, painter: &mut Painter) -> Size;
/// Describes an axis before painting. Return `None` when discovering it
/// needs a concrete box or work performed by `draw`.
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
self.size_hint(axis).map(|len| requests.length(len))
}
/// An exact length the widget can give without a painter or its children. /// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that /// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion. /// disagrees with the eventual draw fails a debug assertion.
+609
View File
@@ -0,0 +1,609 @@
use crate::{
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, StrongWidget, UiNum, Weight, WidgetId,
Widgets, util::HashMap,
};
use std::cmp::Ordering;
impl<N: UiNum> From<N> for SizeRequest {
fn from(value: N) -> Self {
LayoutLen::px(value).into()
}
}
impl LayoutLen {
pub fn min(self, other: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).min(other)
}
pub fn max(self, other: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).max(other)
}
pub fn clamp(self, min: impl Into<SizeRequest>, max: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).clamp(min, max)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Op {
Sum,
Min,
Max,
}
/// One operand of a [`Node`]: a length, or another node. Node numbers are an
/// arena's own, so an operand says nothing about which arena it came from --
/// [`RequestedLen`] is the form that does, and the only one that leaves one.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Operand {
Linear(LayoutLen),
Node(u32),
}
impl Operand {
/// The length itself, where no comparison is waiting on an allocation.
fn linear(&self) -> Option<LayoutLen> {
match *self {
Self::Linear(len) => Some(len),
Self::Node(_) => None,
}
}
}
/// One sum or comparison, with its two operands. Whether anything under it
/// divides leftover space is carried on the node rather than walked for,
/// because every caller of one asks.
#[derive(Clone, Copy, Debug, PartialEq)]
struct Node {
op: Op,
a: Operand,
b: Operand,
leftover: bool,
}
/// The nodes of one expression, numbered from zero, and the folding that
/// happens as each is added. There are two kinds of owner and one kind of
/// arena: a rule's [`SizeRequest`] holds a small one for as long as the rule
/// lasts, and [`RequestArena`] holds the layout pass's.
#[derive(Clone, Debug, Default, PartialEq)]
struct Nodes(Vec<Node>);
impl Nodes {
fn node(&self, index: u32) -> Node {
self.0[index as usize]
}
fn leftover(&self, at: Operand) -> bool {
match at {
Operand::Linear(len) => len.leftover > Weight::ZERO,
Operand::Node(index) => self.node(index).leftover,
}
}
/// `a op b`, which is a node only where the answer needs one. Two
/// lengths that keep their order whatever the room comes to are already
/// decided, and so are two operands that are the same thing.
fn combine(&mut self, op: Op, a: Operand, b: Operand) -> Operand {
if let (Some(x), Some(y)) = (a.linear(), b.linear()) {
if matches!(op, Op::Sum) {
return Operand::Linear(x + y);
}
if let Some(order) = independent_order(x, y) {
let take_a = match op {
Op::Min => !order.is_gt(),
_ => !order.is_lt(),
};
return if take_a { a } else { b };
}
}
if a == b && !matches!(op, Op::Sum) {
return a;
}
let index = u32::try_from(self.0.len()).expect("more nodes than one arena can number");
let leftover = self.leftover(a) || self.leftover(b);
self.0.push(Node { op, a, b, leftover });
Operand::Node(index)
}
/// Copies `at` and everything under it out of `from`, with every length
/// it holds passed through `resolve`. Folded again on the way in, since
/// resolving a fraction can settle a comparison that was open before it.
fn graft(
&mut self,
from: &Self,
at: Operand,
resolve: impl Copy + Fn(LayoutLen) -> LayoutLen,
) -> Operand {
let index = match at {
Operand::Linear(len) => return Operand::Linear(resolve(len)),
Operand::Node(index) => index,
};
let Node { op, a, b, .. } = from.node(index);
let a = self.graft(from, a, resolve);
let b = self.graft(from, b, resolve);
self.combine(op, a, b)
}
fn write(&self, f: &mut std::fmt::Formatter<'_>, at: Operand) -> std::fmt::Result {
let index = match at {
Operand::Linear(len) => return write!(f, "{len}"),
Operand::Node(index) => index,
};
let Node { op, a, b, .. } = self.node(index);
write!(
f,
"{}(",
match op {
Op::Sum => "sum",
Op::Min => "min",
Op::Max => "max",
}
)?;
self.write(f, a)?;
write!(f, ", ")?;
self.write(f, b)?;
write!(f, ")")
}
}
/// A size request before a container has divided its leftover space.
/// Comparisons keep both operands until the share is known.
///
/// An expression is the same nodes the layout pass allocates, in an arena of
/// its own: importing one copies those nodes into the pass's arena, so there
/// is no second shape to keep in step and one place where folding is decided.
#[derive(Clone, PartialEq)]
pub enum SizeRequest {
Linear(LayoutLen),
/// Behind a pointer, because a plain length is what nearly every rule
/// holds and an expression should cost those rules nothing.
Expr(Box<Expr>),
}
/// An expression's own arena, and which of its nodes is the whole of it.
/// `root` is a node number rather than an operand, so an expression that
/// folded all the way down to a length cannot be written as one: that is a
/// [`SizeRequest::Linear`].
#[derive(Clone, Debug, PartialEq)]
pub struct Expr {
nodes: Nodes,
root: u32,
}
impl SizeRequest {
pub fn min(self, other: impl Into<Self>) -> Self {
self.join(Op::Min, other.into())
}
pub fn max(self, other: impl Into<Self>) -> Self {
self.join(Op::Max, other.into())
}
pub fn clamp(self, min: impl Into<Self>, max: impl Into<Self>) -> Self {
self.max(min).min(max)
}
/// `self op other`, keeping `self`'s arena and copying `other`'s into
/// it, so the two sets of node numbers become one. Two requests that are
/// the same request compare to the same thing whatever the room is, which
/// is a comparison worth not building -- but adding something to itself
/// is twice it.
fn join(self, op: Op, other: Self) -> Self {
if self == other && !matches!(op, Op::Sum) {
return self;
}
let (mut nodes, a) = match self {
Self::Linear(len) => (Nodes::default(), Operand::Linear(len)),
Self::Expr(expr) => {
let Expr { nodes, root } = *expr;
(nodes, Operand::Node(root))
}
};
let b = match other {
Self::Linear(len) => Operand::Linear(len),
Self::Expr(expr) => nodes.graft(&expr.nodes, Operand::Node(expr.root), |len| len),
};
match nodes.combine(op, a, b) {
Operand::Linear(len) => Self::Linear(len),
Operand::Node(root) => Self::Expr(Box::new(Expr { nodes, root })),
}
}
}
impl From<LayoutLen> for SizeRequest {
fn from(len: LayoutLen) -> Self {
Self::Linear(len)
}
}
impl From<Len> for SizeRequest {
fn from(len: Len) -> Self {
LayoutLen::from(len).into()
}
}
impl std::ops::Add for SizeRequest {
type Output = Self;
fn add(self, other: Self) -> Self {
self.join(Op::Sum, other)
}
}
impl std::fmt::Display for SizeRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Linear(len) => write!(f, "{len}"),
Self::Expr(expr) => expr.nodes.write(f, Operand::Node(expr.root)),
}
}
}
/// The same, since an arena printed as a struct is not a tree anyone can
/// write out again, which is what a request is printed for.
impl std::fmt::Debug for SizeRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
/// A discovered length. Deferred values belong to the current layout pass;
/// widgets must not retain them. Ordinary requests remain inline lengths.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RequestedLen(RequestValue);
#[derive(Clone, Copy, Debug, PartialEq)]
enum RequestValue {
Linear(LayoutLen),
Deferred {
index: u32,
epoch: u64,
leftover: bool,
},
}
impl From<LayoutLen> for RequestedLen {
fn from(len: LayoutLen) -> Self {
Self(RequestValue::Linear(len))
}
}
impl From<Len> for RequestedLen {
fn from(len: Len) -> Self {
LayoutLen::from(len).into()
}
}
impl RequestedLen {
/// The length itself, where no comparison is waiting on an allocation.
pub fn linear(&self) -> Option<LayoutLen> {
match self.0 {
RequestValue::Linear(len) => Some(len),
_ => None,
}
}
pub fn has_leftover(&self) -> bool {
match self.0 {
RequestValue::Linear(len) => len.leftover > Weight::ZERO,
RequestValue::Deferred { leftover, .. } => leftover,
}
}
}
#[derive(Default)]
pub(crate) struct RequestArena {
nodes: Nodes,
epoch: u64,
}
impl RequestArena {
pub(crate) fn reset(&mut self) {
self.nodes.0.clear();
self.epoch = self
.epoch
.checked_add(1)
.expect("layout generation exhausted");
}
/// An operand of this pass's arena as the handle that leaves it. The
/// epoch says which pass numbered the node, so a handle a widget kept
/// past its pass is caught rather than answering about whatever node
/// took its place.
fn handle(&self, at: Operand) -> RequestedLen {
RequestedLen(match at {
Operand::Linear(len) => RequestValue::Linear(len),
Operand::Node(index) => RequestValue::Deferred {
index,
epoch: self.epoch,
leftover: self.nodes.leftover(at),
},
})
}
/// The other direction, checked once where a handle comes back in rather
/// than again at every level of the walk it starts.
fn operand(&self, request: RequestedLen) -> Operand {
match request.0 {
RequestValue::Linear(len) => Operand::Linear(len),
RequestValue::Deferred { index, epoch, .. } => {
assert_eq!(epoch, self.epoch, "request retained beyond its layout pass");
Operand::Node(index)
}
}
}
/// A rule's expression in this pass's arena, with its fractions resolved
/// against the rel base the widget is being asked with.
pub(crate) fn import(&mut self, request: &SizeRequest, base: Len) -> RequestedLen {
let at = match request {
SizeRequest::Linear(len) => Operand::Linear(len.within_len(base)),
SizeRequest::Expr(expr) => {
self.nodes
.graft(&expr.nodes, Operand::Node(expr.root), |len| {
len.within_len(base)
})
}
};
self.handle(at)
}
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
let request = match bound.min {
Some(min) => self.combine(Op::Max, request, min.into()),
None => request,
};
match bound.max {
Some(max) => self.combine(Op::Min, request, max.into()),
None => request,
}
}
fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen {
let (a, b) = (self.operand(a), self.operand(b));
let at = self.nodes.combine(op, a, b);
self.handle(at)
}
pub(crate) fn minimum(&self, request: RequestedLen, window: Px) -> Px {
let at = self.operand(request);
Px::from_raw(self.segment(at, Ratio::ZERO, window).fixed as i32)
}
fn segment(&self, of: Operand, at: Ratio, window: Px) -> Segment {
let index = match of {
Operand::Linear(len) => {
debug_assert!(
len.leftover >= Weight::ZERO,
"a leftover weight cannot be negative"
);
return Segment {
fixed: i64::from(len.without_leftover().to_px(window).raw()),
weight: i64::from(len.leftover.raw()),
end: None,
};
}
Operand::Node(index) => index,
};
let Node { op, a, b, .. } = self.nodes.node(index);
let a = self.segment(a, at, window);
let b = self.segment(b, at, window);
if matches!(op, Op::Sum) {
return a + b;
}
// At a crossing choose the branch to its right, so the next
// iteration advances rather than selecting that crossing again.
let order = a.value(at).cmp(&b.value(at)).then(a.weight.cmp(&b.weight));
let take_a = match op {
Op::Min => !order.is_gt(),
_ => !order.is_lt(),
};
let mut selected = if take_a { a } else { b };
selected.end = first(a.end, b.end);
if a.weight != b.weight {
let crossing = Ratio::new(b.fixed - a.fixed, a.weight - b.weight);
if crossing > at {
selected.end = first(selected.end, Some(crossing));
}
}
selected
}
/// Divides `room` between requests whose weights are nonnegative, one
/// length per request. A floor can overflow the room and a cap can leave
/// part of it unused, so the lengths need not come to `room`. Each edge
/// is rounded from the running total rather than from the length before
/// it, so two neighbouring slots meet exactly.
pub(crate) fn allocate<'a>(
&'a self,
requests: &'a [RequestedLen],
room: Px,
window: Px,
) -> impl Iterator<Item = Px> + 'a {
let mut at = Ratio::ZERO;
loop {
let total = requests.iter().fold(Segment::ZERO, |total, request| {
total + self.segment(self.operand(*request), at, window)
});
if total.value(at) >= i128::from(room.raw()) * i128::from(at.den) {
break;
}
if total.weight != 0 {
let solution = Ratio::new(i64::from(room.raw()) - total.fixed, total.weight);
if total.end.is_none_or(|end| solution <= end) {
at = solution;
break;
}
}
match total.end {
Some(end) => at = end,
None => break,
}
}
let mut prefix = 0_i128;
let mut previous = 0_i128;
requests.iter().map(move |request| {
prefix += self.segment(self.operand(*request), at, window).value(at);
let den = i128::from(at.den);
// Half away from zero, which is what `Fixed` rounds a division
// to: the two decide the same edge, and a change to one of them
// is a change to the other.
let edge = prefix.signum() * ((prefix.abs() + den / 2) / den);
let len = Px::from_raw((edge - previous) as i32);
previous = edge;
len
})
}
}
#[derive(Clone, Copy, Debug, Eq)]
struct Ratio {
num: i64,
den: i64,
}
impl PartialEq for Ratio {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Ratio {
const ZERO: Self = Self { num: 0, den: 1 };
fn new(num: i64, den: i64) -> Self {
debug_assert_ne!(den, 0, "a ratio of nothing");
if den < 0 {
Self {
num: -num,
den: -den,
}
} else {
Self { num, den }
}
}
}
impl Ord for Ratio {
fn cmp(&self, other: &Self) -> Ordering {
(i128::from(self.num) * i128::from(other.den))
.cmp(&(i128::from(other.num) * i128::from(self.den)))
}
}
impl PartialOrd for Ratio {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Copy)]
struct Segment {
fixed: i64,
weight: i64,
end: Option<Ratio>,
}
impl Segment {
const ZERO: Self = Self {
fixed: 0,
weight: 0,
end: None,
};
fn value(self, at: Ratio) -> i128 {
i128::from(self.fixed) * i128::from(at.den) + i128::from(self.weight) * i128::from(at.num)
}
}
impl std::ops::Add for Segment {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
fixed: self.fixed + other.fixed,
weight: self.weight + other.weight,
end: first(self.end, other.end),
}
}
}
fn first(a: Option<Ratio>, b: Option<Ratio>) -> Option<Ratio> {
match (a, b) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
/// Read-only discovery of requests through a widget's children. A request is
/// expressed in window lengths; `rel_base` supplies the base for declarations.
pub struct SizeRequests<'a> {
pub(crate) arena: &'a mut RequestArena,
pub(crate) measured: Option<&'a HashMap<WidgetId, ActiveData>>,
pub(crate) widgets: &'a Widgets,
pub(crate) dependencies: &'a mut Vec<WidgetId>,
pub(crate) rel_base: Len,
}
impl SizeRequests<'_> {
pub fn sum(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Sum, a, b)
}
pub fn min(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Min, a, b)
}
pub fn max(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Max, a, b)
}
pub fn widget<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
) -> Option<RequestedLen> {
self.dependencies.push(child.id());
let rules = self.widgets.size_rules(child.id());
let rule = &rules[axis];
let request = match &rule.request {
Some(request) => self.arena.import(request, self.rel_base),
None => {
let widget = self.widgets.get_dyn(child.id())?;
widget.size_request(self, axis).or_else(|| {
self.measured?
.get(&child.id())?
.measured()
.map(|size| size[axis].into())
})?
}
};
Some(self.bounded(request, rule.bound))
}
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
self.arena.bounded(request, bound.within_len(self.rel_base))
}
pub fn length(&self, len: LayoutLen) -> RequestedLen {
len.within_len(self.rel_base).into()
}
pub fn inset<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
padding: Px,
) -> Option<RequestedLen> {
let base = self.rel_base;
self.rel_base.px -= padding;
let request = self.widget(child, axis);
self.rel_base = base;
request.map(|request| self.sum(request, Len::from_parts(Rel::ZERO, padding).into()))
}
}
// Equal fractions keep this valid even when padding makes a rel base negative.
fn independent_order(a: LayoutLen, b: LayoutLen) -> Option<Ordering> {
if a.rel == b.rel && a.leftover == b.leftover {
Some(a.px.cmp(&b.px))
} else if a.px == b.px && a.rel == b.rel {
Some(a.leftover.cmp(&b.leftover))
} else {
None
}
}
+131 -44
View File
@@ -1,58 +1,154 @@
use crate::util::impl_axis_index; use crate::util::impl_axis_index;
use crate::{Axis, LayoutLen, Len}; use crate::{LayoutLen, Len, Rel, SizeRequest, Weight};
/// What a widget's length on one axis is, as a rule its parent applies where /// A preferred length and independent bounds on one axis. Without a
/// it draws it rather than an answer the widget gives about itself. /// request, the widget's drawing supplies the preferred length.
/// #[derive(Debug, Clone, PartialEq, Default)]
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on pub struct SizeRule {
/// the axis it names, and the `Size` returned by `draw` answers only the axes pub request: Option<SizeRequest>,
/// with no rule. That is what lets a span divide its space around a length pub bound: Bound,
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
/// than inside it -- the widget under the rule never has to know about it.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum SizeRule {
/// Whatever the widget reports from drawing.
#[default]
Free,
/// This length, whatever the widget reports.
Exact(LayoutLen),
} }
impl SizeRule { impl SizeRule {
/// The length this rule gives without the widget being drawn, if it can pub const FREE: Self = Self {
/// give one. request: None,
pub fn declared(&self) -> Option<Len> { bound: Bound::ANY,
self.exact().and_then(|len| len.declared()) };
pub fn min(min: Len) -> Self {
Self::bounded(Bound {
min: Some(min),
max: None,
})
} }
/// The length this rule gives outright, whatever the widget reports -- pub fn max(max: Len) -> Self {
/// which makes the widget's answer on that axis moot. A share counts: it Self::bounded(Bound {
/// is a length the widget's parent still has to divide, so it is exact min: None,
/// here and resolved there, unlike `declared`, which is only the ones max: Some(max),
/// that give a box directly. })
}
pub fn clamp(min: Len, max: Len) -> Self {
Self::bounded(Bound {
min: Some(min),
max: Some(max),
})
}
/// A bound and no preferred length, so whatever the widget draws is held
/// to it.
fn bounded(bound: Bound) -> Self {
Self {
request: None,
bound,
}
}
pub fn has_fraction(&self) -> bool {
self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound.has_fraction()
}
/// A linear preferred length, before applying the independent bounds.
pub fn exact(&self) -> Option<LayoutLen> { pub fn exact(&self) -> Option<LayoutLen> {
match self { match self.request {
Self::Free => None, Some(SizeRequest::Linear(len)) => Some(len),
Self::Exact(len) => Some(*len), _ => None,
}
}
/// Requests whose final length needs allocation, including a linear
/// share constrained by an independent bound.
pub(crate) fn deferred(&self) -> Option<&SizeRequest> {
let request = self.request.as_ref()?;
match request {
SizeRequest::Linear(len)
if len.leftover == Weight::ZERO || self.bound == Bound::ANY =>
{
None
}
_ => Some(request),
} }
} }
} }
/// A floor, a cap, or both, independent of the preferred length.
/// Fractions use the incoming rel base. An allocated slot keeps the
/// allocator's base for bounds, even when the slot narrows the widget's own.
///
/// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Bound {
pub min: Option<Len>,
pub max: Option<Len>,
}
impl Bound {
/// Every length.
pub const ANY: Self = Self {
min: None,
max: None,
};
/// Whether either end is a fraction of the rel base, so that the same
/// bound against a different one binds at a different length.
pub fn has_fraction(&self) -> bool {
[self.min, self.max]
.into_iter()
.flatten()
.any(|len| len.rel != Rel::ZERO)
}
/// This bound as lengths of the window, from lengths of a rel base that
/// long.
pub fn within_len(&self, len: Len) -> Self {
Self {
min: self.min.map(|min| min.within_len(len)),
max: self.max.map(|max| max.within_len(len)),
}
}
}
/// One bound per axis, as [`SizeRules`] is one rule per axis.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Bounds {
pub x: Bound,
pub y: Bound,
}
impl Bounds {
pub const ANY: Self = Self {
x: Bound::ANY,
y: Bound::ANY,
};
}
impl_axis_index!(Bounds => Bound);
impl From<LayoutLen> for SizeRule { impl From<LayoutLen> for SizeRule {
fn from(len: LayoutLen) -> Self { fn from(len: LayoutLen) -> Self {
Self::Exact(len) SizeRequest::from(len).into()
}
}
impl From<SizeRequest> for SizeRule {
fn from(request: SizeRequest) -> Self {
Self {
request: Some(request),
bound: Bound::ANY,
}
} }
} }
impl From<Option<LayoutLen>> for SizeRule { impl From<Option<LayoutLen>> for SizeRule {
fn from(len: Option<LayoutLen>) -> Self { fn from(len: Option<LayoutLen>) -> Self {
len.map_or(Self::Free, Self::Exact) len.map_or(Self::FREE, Self::from)
} }
} }
/// One rule per axis, which is how a widget carries a length on one axis and /// One rule per axis, which is how a widget carries a length on one axis and
/// leaves the other to whatever it draws. /// leaves the other to whatever it draws.
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct SizeRules { pub struct SizeRules {
pub x: SizeRule, pub x: SizeRule,
pub y: SizeRule, pub y: SizeRule,
@@ -60,12 +156,10 @@ pub struct SizeRules {
impl_axis_index!(SizeRules => SizeRule); impl_axis_index!(SizeRules => SizeRule);
/// What a widget's box is on each axis where something says so outright, /// Box lengths chosen by the ask: a declaration, a natural-size hint, or
/// before it is drawn: a rule beside it, or a hint it gives about itself. /// a binding bound. These are window lengths, already resolved against the
/// Whoever draws the widget resolves these against its rel base. /// incoming rel base. Moving a drawing preserves them instead of resolving
/// /// a fraction against its destination a second time.
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
/// -- see [`LayoutLen::declared`].
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Declared { pub struct Declared {
pub x: Option<Len>, pub x: Option<Len>,
@@ -74,13 +168,6 @@ pub struct Declared {
impl Declared { impl Declared {
pub const NONE: Self = Self { x: None, y: None }; pub const NONE: Self = Self { x: None, y: None };
pub fn from_axes(f: impl Fn(Axis) -> Option<Len>) -> Self {
Self {
x: f(Axis::X),
y: f(Axis::Y),
}
}
} }
impl_axis_index!(Declared => Option<Len>); impl_axis_index!(Declared => Option<Len>);
+40 -4
View File
@@ -1,8 +1,8 @@
use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::mpsc::{Receiver, Sender, channel};
use crate::{ use crate::{
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, Axis, AxisAlign, Bound, IdLike, Len, RegionAlign, SizeRequest, SizeRule, SizeRules,
WidgetData, WidgetId, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut}, util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
}; };
@@ -128,8 +128,8 @@ impl Widgets {
} }
/// The length rules whoever draws this widget applies to its box. /// The length rules whoever draws this widget applies to its box.
pub fn size_rules(&self, id: impl IdLike) -> SizeRules { pub fn size_rules(&self, id: impl IdLike) -> &SizeRules {
self.data(id).unwrap().size &self.data(id).unwrap().size
} }
/// Sets one axis's rule. The widget is marked rather than its parent /// Sets one axis's rule. The widget is marked rather than its parent
@@ -145,6 +145,42 @@ impl Widgets {
self.needs_redraw.insert(id); self.needs_redraw.insert(id);
} }
/// Changes the preferred length, leaving the bounds beside it alone.
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<SizeRequest>) {
let id = id.id();
let request = Some(len.into());
let rule = &mut self.data_mut(id).unwrap().size[axis];
if rule.request == request {
return;
}
rule.request = request;
self.needs_redraw.insert(id);
}
/// Puts a floor under this widget's length on one axis, keeping a cap it
/// already had and the preferred length beside it.
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
self.edit_bound(id.id(), axis, |bound| bound.min = Some(min));
}
/// Puts a cap over it, keeping a floor it already had.
pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) {
self.edit_bound(id.id(), axis, |bound| bound.max = Some(max));
}
/// Edits one axis's bound where it sits, rather than reading the whole
/// rule out and writing it back: an expression beside the bound is not
/// this edit's business, and copying it to move one end would be the
/// only thing here that ever copies one.
fn edit_bound(&mut self, id: WidgetId, axis: Axis, edit: impl FnOnce(&mut Bound)) {
let bound = &mut self.data_mut(id).unwrap().size[axis].bound;
let before = *bound;
edit(bound);
if *bound != before {
self.needs_redraw.insert(id);
}
}
/// Where this widget sits in a box longer than the length it takes. /// Where this widget sits in a box longer than the length it takes.
pub fn alignment(&self, id: impl IdLike) -> RegionAlign { pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
self.data(id).unwrap().align self.data(id).unwrap().align
+10 -3
View File
@@ -18,6 +18,7 @@ struct Input {
} }
struct InputFn { struct InputFn {
attrs: Vec<Attribute>,
sig: Signature, sig: Signature,
body: Block, body: Block,
} }
@@ -32,9 +33,10 @@ impl Parse for Input {
input.parse::<Token![;]>()?; input.parse::<Token![;]>()?;
let mut fns = Vec::new(); let mut fns = Vec::new();
while !input.is_empty() { while !input.is_empty() {
let attrs = input.call(Attribute::parse_outer)?;
let sig = input.parse()?; let sig = input.parse()?;
let body = input.parse()?; let body = input.parse()?;
fns.push(InputFn { sig, body }) fns.push(InputFn { attrs, sig, body })
} }
if !input.is_empty() { if !input.is_empty() {
input.error("function expected"); input.error("function expected");
@@ -59,10 +61,15 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
fns, fns,
} = parse_macro_input!(input as Input); } = parse_macro_input!(input as Input);
let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect(); // What a method says about itself belongs on the trait, where a reader
// looks it up; the implementation is the same text and says it again.
let sigs: Vec<_> = fns
.iter()
.map(|InputFn { attrs, sig, .. }| quote! { #(#attrs)* #sig })
.collect();
let impls: Vec<_> = fns let impls: Vec<_> = fns
.iter() .iter()
.map(|InputFn { sig, body }| quote! { #sig #body }) .map(|InputFn { attrs, sig, body }| quote! { #(#attrs)* #sig #body })
.collect(); .collect();
let Some(GenericParam::Type(state)) = generics.params.first() else { let Some(GenericParam::Type(state)) = generics.params.first() else {
+4 -4
View File
@@ -164,11 +164,11 @@ impl Harness {
self.render.resize(size, self.rsc.widgets_mut()); self.render.resize(size, self.rsc.widgets_mut());
} }
/// Changes a length rule after the fact, the way `.width()` sets one. /// Changes a length rule after the fact, the way `.width()` sets one --
/// which leaves a bound beside it alone, where writing the whole rule
/// would drop it and pass the case for the wrong reason.
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) { pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
self.rsc self.rsc.widgets_mut().set_len(id, axis, len.into());
.widgets_mut()
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
} }
/// Sets the root and lays it out, so a pointer event has something to hit. /// Sets the root and lays it out, so a pointer event has something to hit.
+56 -11
View File
@@ -292,7 +292,7 @@ impl Plan {
align: None, align: None,
..self.clone() ..self.clone()
}), }),
self.size.map(|_| Plan { self.size.as_ref().map(|_| Plan {
size: None, size: None,
..self.clone() ..self.clone()
}), }),
@@ -342,6 +342,19 @@ impl Plan {
at(self); at(self);
} }
/// Drops every intrinsic bound from this tree, leaving the rest of it
/// -- and the generator's draws -- exactly as they were. That isolates
/// the ordinary path from the deferred one over the same shapes, which
/// is what says whether a difference is the bounds or the trees.
pub fn drop_bounds(&mut self) {
self.walk_mut(&mut |node| {
let Some(rules) = &mut node.size else { return };
for axis in Axis::BOTH {
rules[axis].bound = Bound::ANY;
}
});
}
/// The same tree with `edits` applied, by the indices the generator would /// The same tree with `edits` applied, by the indices the generator would
/// have used for them. /// have used for them.
/// ///
@@ -373,7 +386,7 @@ impl Plan {
} }
if plan.size.is_some() { if plan.size.is_some() {
if let Some(lens) = edits.sizes.get(&sized) { if let Some(lens) = edits.sizes.get(&sized) {
plan.size = Some(*lens); plan.size = Some(lens.clone());
} }
sized += 1; sized += 1;
} }
@@ -644,11 +657,43 @@ impl Sow<'_> {
}) })
} }
fn len(&mut self) -> Option<LayoutLen> { fn len(&mut self) -> LayoutLen {
match self.rng.below(4) { LayoutLen::px(20.0 + self.rng.below(180) as f32)
0 => Some(LayoutLen::px(20.0 + self.rng.below(180) as f32)), }
1 => Some(LayoutLen::LEFTOVER),
_ => None, /// A length of a box rather than a length of the window, which is what a
/// bound is.
///
/// Pixels, because a fraction is resolved against the rel base the widget
/// was asked with and `deferred_generated.rs` is where that is varied:
/// its relative-bound corpus rewrites these as fractions, so growing them
/// that way here would buy overlap and move every box in the cold dump.
/// Two depth-5 trees once disagreed warm against cold with fractions in
/// them, which is why this was written as pixels; they were named by
/// seed, and seeds stopped naming those trees when the leaves grew
/// images. Re-measured 2026-09-20: 600 depth-5 trees over all sixteen
/// cases agree with every bound here a fraction.
fn bound(&mut self) -> Len {
Len::px(20.0 + self.rng.below(180) as f32)
}
fn rule(&mut self) -> SizeRule {
match self.rng.below(8) {
0 | 1 => self.len().into(),
2 => LayoutLen::LEFTOVER.into(),
3 => SizeRule::min(self.bound()),
4 => SizeRule::max(self.bound()),
// Both in pixels, so one can be put under the other: a floor and
// a cap that change sides with the window bound nothing, which
// is a caller's bug rather than a tree to grow.
5 => {
let (a, b) = (
Px::from_f32(20.0 + self.rng.below(180) as f32),
Px::from_f32(20.0 + self.rng.below(180) as f32),
);
SizeRule::clamp(Len::px(a.min(b).to_f32()), Len::px(a.max(b).to_f32()))
}
_ => SizeRule::FREE,
} }
} }
@@ -674,15 +719,15 @@ impl Sow<'_> {
fn sized(&mut self, inner: &mut Plan) { fn sized(&mut self, inner: &mut Plan) {
let take = self.rng.chance(); let take = self.rng.chance();
let lens = SizeRules { let lens = SizeRules {
x: self.len().into(), x: self.rule(),
y: self.len().into(), y: self.rule(),
}; };
if !take || inner.size.is_some() { if !take || inner.size.is_some() {
return; return;
} }
let idx = self.sized; let idx = self.sized;
self.sized += 1; self.sized += 1;
inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens)); inner.size = Some(self.edits.sizes.get(&idx).cloned().unwrap_or(lens));
} }
/// An alignment over some of the tree, kept where a test can change it. /// An alignment over some of the tree, kept where a test can change it.
@@ -818,7 +863,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
fn node(&mut self, plan: &Plan) -> StrongWidget { fn node(&mut self, plan: &Plan) -> StrongWidget {
let built = self.kind(&plan.kind); let built = self.kind(&plan.kind);
let id = built.id(); let id = built.id();
if let Some(lens) = plan.size { if let Some(lens) = plan.size.clone() {
self.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y); self.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
self.tree.sized.push(id); self.tree.sized.push(id);
} }
+1 -1
View File
@@ -17,7 +17,7 @@ impl Widget for Image {
} }
impl Image { impl Image {
/// One texture already uploaded, for a caller holding its handle: [`image`] /// One texture already uploaded, for a caller holding its handle: [`image()`]
/// uploads what it is given, and several widgets showing one picture want /// uploads what it is given, and several widgets showing one picture want
/// one upload and one slot between them. /// one upload and one slot between them.
pub fn new(handle: TextureHandle) -> Self { pub fn new(handle: TextureHandle) -> Self {
+4
View File
@@ -6,6 +6,10 @@ pub struct LayerOffset {
} }
impl Widget for LayerOffset { impl Widget for LayerOffset {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
requests.widget(&self.inner, axis)
}
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
+4
View File
@@ -6,6 +6,10 @@ pub struct Offset {
} }
impl Widget for Offset { impl Widget for Offset {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
requests.widget(&self.inner, axis)
}
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter painter
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt)) .widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
+16 -2
View File
@@ -6,6 +6,10 @@ pub struct Pad {
} }
impl Widget for Pad { impl Widget for Pad {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
requests.inset(&self.inner, axis, self.padding.along(axis))
}
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// The inner's own alignment, not the near edge. This reports the // The inner's own alignment, not the near edge. This reports the
// inner's size plus the padding, so where the box is that answer the // inner's size plus the padding, so where the box is that answer the
@@ -22,11 +26,11 @@ impl Widget for Pad {
let inner = painter.widget_at(&self.inner, self.padding.region()).size(); let inner = painter.widget_at(&self.inner, self.padding.region()).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.along(Axis::X),
..inner.x ..inner.x
}, },
y: LayoutLen { y: LayoutLen {
px: inner.y.px + self.padding.top + self.padding.bottom, px: inner.y.px + self.padding.along(Axis::Y),
..inner.y ..inner.y
}, },
} }
@@ -57,6 +61,16 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
/// Both sides of one axis together, which is what this padding takes
/// of a length along it.
pub fn along(&self, axis: Axis) -> Px {
match axis {
Axis::X => self.left + self.right,
Axis::Y => self.top + self.bottom,
}
}
/// `region` less this padding on each side. /// `region` less this padding on each side.
pub fn region_of(&self, mut region: UiRegion) -> UiRegion { pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left; region.x.start.px += self.left;
+126 -54
View File
@@ -8,57 +8,73 @@ pub struct Span {
} }
impl Widget for Span { impl Widget for Span {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
if axis != self.dir.axis {
// A share can be hidden when the other axis has no room. Its
// cross-axis length then contributes nothing to the drawn answer.
return None;
}
let mut total = RequestedLen::from(Len::from_parts(Rel::ZERO, self.gaps()));
for child in &self.children {
let child = requests.widget(child, axis)?;
total = requests.sum(total, child);
}
Some(total)
}
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.with_requests(|painter, lens, values| self.layout(painter, lens, values))
}
}
impl Span {
fn layout(
&self,
painter: &mut Painter,
lens: &mut Vec<RequestedLen>,
values: &mut Vec<Px>,
) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The row this span lays its children out along, as a length of the // The row this span lays its children out along, as a length of the
// rel base they are laid out against. Where it starts is nothing's // rel base they are laid out against. Where it starts is nothing's
// business -- a slot is a length from there -- so what this reads is // business -- a slot is a length from there -- so what this reads is
// the length alone. // the length alone.
let row = painter.region_len(axis); let row = painter.region_len(axis);
// A length for every child before their final slots are chosen: from self.collect(painter, row, lens, true);
// a hint where one says, and from drawing otherwise. The rel base passes let gaps = self.gaps();
// through unchanged, so `rel(0.5)` is half the area this span was let fixed = lens
// given whatever else is in it and wherever this child sits among .iter()
// them; what a drawn child is asked in is the room left from the .try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
// cursor, because a text has to wrap at the width actually there. Some(sum + len.linear()?.without_leftover())
let mut cursor = Len::ZERO; });
let mut lens = Vec::with_capacity(self.children.len()); if let Some(fixed) = fixed
for child in &self.children { && lens.iter().any(|len| len.has_leftover())
let len = match painter.size_hint(child, axis) { && !painter.longer_than(row, fixed, axis)
Some(len) => len, {
None => { // With no share to assign, intrinsic drawings keep the remaining
// Across itself the child sits where its own alignment // offer, including overflow. Their answer is only moved into a slot.
// says, in the whole of the row: a span is what contains self.collect(painter, row, lens, false);
// its children there, and nothing divides that axis.
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
painter.widget_at(child, room).len(axis)
}
};
cursor += len.without_leftover();
cursor.px += self.gap;
lens.push(len);
} }
let nonlinear = lens.iter().any(|len| len.linear().is_none());
let gaps = self if nonlinear {
.gap painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
.mul_int(self.children.len().saturating_sub(1) as i32); }
let total = lens.iter().fold( let allocated = nonlinear.then(|| &values[..]);
LayoutLen { let total = match allocated {
px: gaps, Some(allocated) => LayoutLen {
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
..LayoutLen::ZERO ..LayoutLen::ZERO
}, },
|sum, len| sum + *len, None => lens.iter().fold(
); LayoutLen {
px: gaps,
// What is left for the shares to divide: the row less everything ..LayoutLen::ZERO
// fixed, as a length of the rel base rather than a number of pixels. },
|sum, len| sum + len.linear().unwrap(),
),
};
let all_fixed = total.without_leftover(); let all_fixed = total.without_leftover();
let room = row - all_fixed; let room = row - all_fixed;
// The three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which the range `longer_than` keeps already
// reads. What the generated oracle checks is the consequence, since
// which children exist at all turns on this.
let any_leftover = total.leftover > Weight::ZERO; let any_leftover = total.leftover > Weight::ZERO;
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis); let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
@@ -83,17 +99,34 @@ impl Widget for Span {
false => fixed, false => fixed,
true => fixed + room.scale(Rel::ratio(taken, total.leftover)), true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
}; };
for (child, &len) in self.children.iter().zip(&lens) { for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
// A child asking for nothing but a part of what is left over, // An allocated row already has a length for every child; without
// when nothing is, is not drawn at all. One that also asked for // one the request is the length and the room is divided here.
// pixels or a fraction keeps those and overflows. // Either way a child asking for nothing but a part of what is
if len.is_only_leftover() && !has_room { // left over, when nothing is, is not drawn at all -- one that
// also asked for pixels or a fraction keeps those and overflows.
let (len, shares, nothing_left) = match allocated {
Some(allocated) => {
let len = LayoutLen {
px: allocated[index],
..LayoutLen::ZERO
};
let shares = request.has_leftover();
(len, shares, shares && len.px == Px::ZERO)
}
None => {
let len = request.linear().unwrap();
let shares = len.leftover > Weight::ZERO && has_room;
(len, shares, len.is_only_leftover() && !has_room)
}
};
if nothing_left {
painter.undraw(child); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let from = reached(fixed, taken); let from = reached(fixed, taken);
if len.leftover > Weight::ZERO && has_room { if shares {
taken += len.leftover; taken += len.leftover;
} }
fixed += len.without_leftover(); fixed += len.without_leftover();
@@ -106,8 +139,8 @@ impl Widget for Span {
// fixed child's slot is its own answer, so a drawing made in the // fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here. // room is put there as it is, and one not made yet is made here.
let slot = self.slot(row, from, to); let slot = self.slot(row, from, to);
let mut place = slot.shifted_desc().fills().on_axis(axis); let mut place = slot.shifted_desc().allocated().on_axis(axis);
if len.leftover > Weight::ZERO && has_room { if shares {
place = place.rel_base(axis, slot.len()); place = place.rel_base(axis, slot.len());
} }
let used = painter.place_at(child, place).len(!axis); let used = painter.place_at(child, place).len(!axis);
@@ -125,13 +158,15 @@ impl Widget for Span {
fixed.px += self.gap; fixed.px += self.gap;
} }
// Carried whole rather than collapsed to one share: a span that sizes // Where nothing was allocated the weight is carried whole rather
// from its children does not resolve `leftover`, it passes the weight up, // than collapsed to one share, so nesting spans divides the same
// so nesting spans divides the same space rather than re-dividing a // space rather than re-dividing a share of it: four `leftover(1)`
// share of it. Four `leftover(1)` children under two spans under one span // children under two spans under one span get a quarter each, which
// get a quarter each, which collapsing to `leftover(1)` per level does // one share per level does not give. Resolution happens at the
// not give. Resolution happens at the nearest ancestor with a length, // nearest ancestor with a length, and the root always has one --
// and the root always has one. // or, where a comparison deferred the row, at the ancestor whose
// allocation discovery carried these requests to, and `total` is
// pixels by the time it gets here.
let ortho = match shrinks { let ortho = match shrinks {
true => ortho, true => ortho,
false => LayoutLen::rel(1.0), false => LayoutLen::rel(1.0),
@@ -141,6 +176,43 @@ impl Widget for Span {
} }
impl Span { impl Span {
/// What the gaps between this span's children take, which is a length of
/// the row before anything is divided.
fn gaps(&self) -> Px {
self.gap
.mul_int(self.children.len().saturating_sub(1) as i32)
}
fn collect(
&self,
painter: &mut Painter,
row: Len,
lens: &mut Vec<RequestedLen>,
discover: bool,
) {
let axis = self.dir.axis;
let mut cursor = Len::ZERO;
lens.clear();
for child in &self.children {
let request = if discover {
painter.size_request(child, axis)
} else {
painter.size_hint(child, axis).map(Into::into)
};
let len = match request {
Some(len) => len,
None => {
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
let len = painter.widget_at(child, room).len(axis);
painter.measured_request(child, axis, len)
}
};
cursor += painter.minimum_request(&len, axis);
cursor.px += self.gap;
lens.push(len);
}
}
/// The stretch of the row between two distances from where this span /// The stretch of the row between two distances from where this span
/// starts laying children out, as a span of its own box. A negative /// starts laying children out, as a span of its own box. A negative
/// direction lays out from the far end, so the same two distances mirror /// direction lays out from the far end, so the same two distances mirror
+13
View File
@@ -8,6 +8,19 @@ pub struct Stack {
} }
impl Widget for Stack { impl Widget for Stack {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
let sizing = match self.size {
StackSize::Default => None,
StackSize::Child(i) => self.children.get(i),
};
// With nothing sizing it a stack is a share of the box it is given,
// which is what its draw answers too.
match sizing {
Some(child) => requests.widget(child, axis),
None => Some(LayoutLen::LEFTOVER.into()),
}
}
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let sizing = match self.size { let sizing = match self.size {
StackSize::Default => None, StackSize::Default => None,
+44 -12
View File
@@ -53,32 +53,64 @@ widget_trait! {
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
let widgets = &mut state.ui_mut().widgets; let widgets = &mut state.ui_mut().widgets;
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x)); widgets.set_len(id, Axis::X, size.x);
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y)); widgets.set_len(id, Axis::Y, size.y);
id id
} }
} }
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn width(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into(); let len = len.into();
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
state state.ui_mut().widgets.set_len(id, Axis::X, len);
.ui_mut()
.widgets
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
id id
} }
} }
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> { /// Sets a floor on this widget's offered width and reported width.
fn min_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into(); let len = len.into();
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
state state.ui_mut().widgets.set_min_len(id, Axis::X, len);
.ui_mut() id
.widgets }
.set_size_rule(id, Axis::Y, SizeRule::Exact(len)); }
fn min_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| {
let id = self.add(state);
state.ui_mut().widgets.set_min_len(id, Axis::Y, len);
id
}
}
/// Caps this widget's offered width and reported width.
fn max_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| {
let id = self.add(state);
state.ui_mut().widgets.set_max_len(id, Axis::X, len);
id
}
}
fn max_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| {
let id = self.add(state);
state.ui_mut().widgets.set_max_len(id, Axis::Y, len);
id
}
}
fn height(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| {
let id = self.add(state);
state.ui_mut().widgets.set_len(id, Axis::Y, len);
id id
} }
} }
+80
View File
@@ -0,0 +1,80 @@
use iris::{harness::Harness, prelude::*};
use std::{
alloc::{GlobalAlloc, Layout, System},
cell::Cell,
};
struct Counting;
thread_local! {
static COUNT: Cell<Option<usize>> = const { Cell::new(None) };
}
fn count() {
COUNT.with(|count| {
if let Some(n) = count.get() {
count.set(Some(n + 1));
}
});
}
// The wrapper preserves System's allocation and deallocation contracts;
// observing calls here also counts allocations hidden inside layout helpers.
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
count();
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
count();
unsafe { System.realloc(ptr, layout, size) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
#[test]
fn unchanged_tree_reuses_layout_storage() {
for rule in [
SizeRule::FREE,
leftover(1).clamp(20, 80).into(),
SizeRule::clamp(20.into(), 80.into()),
] {
let mut h = Harness::new((600, 200));
let mut children: Vec<StrongWidget> = Vec::new();
for _ in 0..8 {
let a = rect(Color::RED).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rule(a, Axis::X, rule.clone());
let row = (a, rect(Color::BLUE))
.span(Dir::RIGHT)
.add_strong(&mut h.rsc);
children.push(row);
}
let root = h.rsc.widgets_mut().add_strong(Span {
children,
dir: Dir::DOWN,
gap: Px::ZERO,
});
h.state.root = Some(root);
h.frame();
let ids: Vec<_> = h.render.active.keys().copied().collect();
for frame in 0..8 {
for &id in &ids {
h.rsc.widgets_mut().mark_for_redraw(id);
}
h.resize((600 + frame % 2, 200));
h.frame();
}
COUNT.set(Some(0));
for frame in 0..100 {
for &id in &ids {
h.rsc.widgets_mut().mark_for_redraw(id);
}
h.resize((600 + frame % 2, 200));
h.frame();
}
let allocations = COUNT.replace(None).unwrap();
println!("rule={rule:?}: {allocations} allocations over 100 resize frames");
assert_eq!(allocations, 0);
}
}
+88
View File
@@ -0,0 +1,88 @@
//! CPU comparison of a bounds attribute against the `MaxSize` wrapper it
//! replaced, using the same builder calls and the same geometry. The wrapper
//! is gone from this tree, so its side of the comparison is run by checking
//! out a commit that still has it: the fixture is written to build the same
//! way at both.
//!
//! MODE=cap FRAMES=2000 cargo test --release --test bounds_cost \
//! -- --ignored --nocapture
//!
//! `MODE` is `plain`, `exact` or `cap`; `REDRAW=1` marks every widget for
//! redraw each frame; `FRAMES` is how many resize frames to measure. Use
//! repeated `perf stat -e instructions:u` runs on the executable directly.
//! Process totals include the cold frame, so compare identical modes and
//! frame counts. Wall time on this machine is not a stable comparison.
mod rig;
use iris::{harness::Harness, prelude::*};
use rig::env;
/// The two widths the loop alternates. The cap of 80 binds at 300 and does
/// not at 100, so the measured frames cross it in both directions.
const WIDTHS: [i32; 2] = [100, 300];
#[test]
#[ignore = "instruction-count measurement"]
fn bounds_cost() {
let mode = env("MODE", "cap".to_string());
let redraw = env("REDRAW", 0_u8) != 0;
let frames = env("FRAMES", 2000_usize);
let mut h = Harness::new((300, 512));
let mut column = Span::empty(Dir::DOWN);
let mut leaves = Vec::new();
for _ in 0..128 {
let first = match mode.as_str() {
"plain" => rect(Color::RED).add_strong(&mut h.rsc).any(),
"exact" => rect(Color::RED).width(40).add_strong(&mut h.rsc).any(),
"cap" => rect(Color::RED).max_width(80).add_strong(&mut h.rsc).any(),
_ => panic!("unknown MODE {mode}"),
};
leaves.push(first.id());
let second = rect(Color::BLUE).add_strong(&mut h.rsc);
let row = Span {
children: vec![first, second],
dir: Dir::RIGHT,
gap: Px::ZERO,
};
column.push(row.height(4).add_strong(&mut h.rsc));
}
h.set_root(column);
let ids: Vec<_> = h.render.active.keys().copied().collect();
println!(
"mode={mode}, widgets={}, rule_bytes={}",
ids.len(),
std::mem::size_of::<SizeRule>()
);
// That the fixture measures what it says is checked on both sides of the
// crossing here rather than inside the measured loop, which is what the
// other rigs do. Per frame it measured only 0.65% of the total (5.780B
// against 5.743B instructions at MODE=cap, FRAMES=2000), but none of it
// is the layout the number is about.
for width in WIDTHS {
h.resize((width, 512));
h.frame();
let expected = match mode.as_str() {
"plain" => width / 2,
"exact" => 40,
"cap" => (width / 2).min(80),
_ => unreachable!("the mode was checked while building"),
};
for id in &leaves {
assert_eq!(h.region(id).unwrap().size().x, Px::from_int(expected));
}
println!(
"width {width}: first leaf {}",
h.region(&leaves[0]).unwrap()
);
}
for frame in 0..frames {
if redraw {
for &id in &ids {
h.rsc.widgets_mut().mark_for_redraw(id);
}
}
h.resize((WIDTHS[frame % WIDTHS.len()], 512));
h.frame();
}
}
+421
View File
@@ -0,0 +1,421 @@
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
struct Counted {
draws: Rc<Cell<usize>>,
}
impl Widget for Counted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.px_size();
painter.primitive(RectPrimitive::color(Color::RED));
Size::LEFTOVER
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
}
#[test]
fn a_capped_share_returns_room_to_its_sibling() {
let mut h = Harness::new((300, 100));
let first = rect(Color::RED).max_width(80).add(&mut h.rsc);
let second = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((first, second).span(Dir::RIGHT));
assert_corners!(h, first, (0, 0), (80, 100));
assert_corners!(h, second, (80, 0), (300, 100));
h.resize((100, 100));
h.frame();
assert_corners!(h, first, (0, 0), (50, 100));
assert_corners!(h, second, (50, 0), (100, 100));
h.resize((300, 100));
h.frame();
assert_corners!(h, first, (0, 0), (80, 100));
assert_corners!(h, second, (80, 0), (300, 100));
}
#[test]
fn nested_shares_are_discovered_without_provisional_paint() {
let mut h = Harness::new((400, 100));
let draws = Rc::new(Cell::new(0));
let leaf = h.rsc.ui_mut().widgets.add_strong(Counted {
draws: draws.clone(),
});
let leaf_id = leaf.id();
let mut inner: StrongWidget = leaf;
for _ in 0..8 {
let sibling = rect(Color::BLUE).add_strong(&mut h.rsc);
inner = h.rsc.ui_mut().widgets.add_strong(Span {
children: vec![inner, sibling],
dir: Dir::RIGHT,
gap: Px::ZERO,
});
}
h.state.root = Some(inner);
h.frame();
assert_eq!(draws.get(), 1);
assert!(h.region(&leaf_id).is_some());
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), 2);
}
#[test]
fn nested_bounds_are_resolved_in_the_outer_allocation() {
let mut h = Harness::new((300, 100));
let a = rect(Color::RED).max_width(40).add(&mut h.rsc);
let b = rect(Color::GREEN).max_width(60).add(&mut h.rsc);
let inner = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((inner, tail).span(Dir::RIGHT));
assert_corners!(h, a, (0, 0), (40, 100));
assert_corners!(h, b, (40, 0), (100, 100));
assert_corners!(h, tail, (100, 0), (300, 100));
}
#[test]
fn request_edits_in_a_nested_child_reach_the_allocator() {
let mut h = Harness::new((300, 100));
let a = rect(Color::RED).max_width(80).add(&mut h.rsc);
let inner = (a,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((inner, tail).span(Dir::RIGHT));
h.rsc.widgets_mut().set_max_len(a, Axis::X, Len::px(40.0));
h.frame();
assert_corners!(h, a, (0, 0), (40, 100));
assert_corners!(h, tail, (40, 0), (300, 100));
}
#[test]
fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() {
for hinted in [true, false] {
let mut h = Harness::new((300, 100));
let a = if hinted {
rect(Color::RED).add_strong(&mut h.rsc).any()
} else {
h.rsc.widgets_mut().add_strong(Unhinted).any()
};
let id = a.id();
let b = rect(Color::BLUE).add(&mut h.rsc);
let mut row = Span::empty(Dir::RIGHT);
row.push(a);
row.push(b.add_strong(&mut h.rsc));
h.set_root(row);
h.resize((400, 100));
h.frame();
h.rsc
.widgets_mut()
.set_size_rule(id, Axis::X, SizeRule::max(Len::px(80.0)));
h.frame();
assert_corners!(h, id, (0, 0), (80, 100));
assert_corners!(h, b, (80, 0), (400, 100));
}
}
#[test]
fn a_deferred_comparison_can_compare_two_different_weights() {
let a = SizeRequest::from(leftover(1.0) + px(30.0)).min(leftover(2.0));
let b = SizeRequest::from(leftover(1.0)).clamp(px(20.0), px(100.0));
let mut h = Harness::new((60, 100));
let a = rect(Color::RED).width(a).add(&mut h.rsc);
let b = rect(Color::BLUE).width(b).add(&mut h.rsc);
h.set_root((a, b).span(Dir::RIGHT));
assert_corners!(h, a, (0, 0), (40, 100));
assert_corners!(h, b, (40, 0), (60, 100));
h.resize((300, 100));
h.frame();
assert_corners!(h, a, (0, 0), (200, 100));
assert_corners!(h, b, (200, 0), (300, 100));
}
#[test]
fn a_comparison_between_two_comparisons_keeps_both_of_them() {
// Joining two expressions is the one path that copies a request's nodes
// into another's arena; the floor puts a node under the copied one, so
// its operands have to be renumbered as they land.
let capped = leftover(1).min(px(40));
let floored = leftover(2).min(px(70)).max(px(10));
let mut h = Harness::new((90, 100));
let both = rect(Color::RED).width(capped.max(floored)).add(&mut h.rsc);
let rest = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((both, rest).span(Dir::RIGHT));
// Under either cap, so the doubled share is the longer of the two.
assert_corners!(h, both, (0, 0), (60, 100));
assert_corners!(h, rest, (60, 0), (90, 100));
h.resize((300, 100));
h.frame();
// Over both caps, so the comparison is between 40 and 70.
assert_corners!(h, both, (0, 0), (70, 100));
assert_corners!(h, rest, (70, 0), (300, 100));
}
#[test]
fn a_length_expression_is_resolved_before_wrapping_text() {
let mut h = Harness::new((300, 500));
let text = wtext("one two three four five six seven eight nine ten")
.size(16)
.wrap(true)
.width(leftover(1).clamp(40, 80))
.add(&mut h.rsc);
let other = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((text, other).span(Dir::RIGHT));
let box_ = h.region(&text).unwrap();
assert_eq!(box_.top_left.x, Px::ZERO);
assert_eq!(box_.bot_right.x, Px::from_int(80));
assert!(box_.bot_right.y - box_.top_left.y > Px::from_int(30));
assert_corners!(h, other, (80, 0), (300, 500));
}
#[test]
fn relative_bounds_keep_the_allocators_base() {
let mut h = Harness::new((300, 100));
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
let bounded = rect(Color::RED)
.width(leftover(1).min(rel(0.25)))
.add(&mut h.rsc);
let tail = rect(Color::GREEN).add(&mut h.rsc);
h.set_root((head, bounded, tail).span(Dir::RIGHT));
assert_corners!(h, bounded, (30, 0), (105, 100));
assert_corners!(h, tail, (105, 0), (300, 100));
h.resize((400, 100));
h.frame();
assert_corners!(h, bounded, (30, 0), (130, 100));
assert_corners!(h, tail, (130, 0), (400, 100));
}
#[test]
fn the_root_resolves_a_deferred_request_again_after_resize() {
let mut h = Harness::new((300, 100));
let bounded = rect(Color::RED)
.width(leftover(1).min(rel(0.25)))
.add(&mut h.rsc);
h.set_root(bounded);
assert_corners!(h, bounded, (112.5, 0), (187.5, 100));
h.resize((400, 100));
h.frame();
assert_corners!(h, bounded, (150, 0), (250, 100));
}
#[test]
fn filling_a_stack_does_not_mean_its_sizing_child_was_already_allocated() {
let mut h = Harness::new((300, 100));
let child = rect(Color::RED).width(leftover(1).min(80)).add(&mut h.rsc);
let overlay = rect(Color::BLUE).add(&mut h.rsc);
let children: Vec<StrongWidget> =
vec![child.add_strong(&mut h.rsc), overlay.add_strong(&mut h.rsc)];
h.set_root(Stack {
children,
size: StackSize::Child(0),
});
assert_corners!(h, child, (110, 0), (190, 100));
assert_corners!(h, overlay, (110, 0), (190, 100));
}
struct Unhinted;
impl Widget for Unhinted {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.px_size();
painter.primitive(RectPrimitive::color(Color::RED));
Size::LEFTOVER
}
}
#[test]
fn bounds_also_apply_to_shares_discovered_by_drawing() {
let mut h = Harness::new((300, 100));
let a = h.rsc.widgets_mut().add_strong(Unhinted);
h.rsc
.widgets_mut()
.set_size_rule(a.id(), Axis::X, SizeRule::max(Len::px(80.0)));
let id = a.id();
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
let b_id = b.id();
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
children: vec![a, b],
dir: Dir::RIGHT,
gap: Px::ZERO,
}));
h.frame();
assert_corners!(h, id, (0, 0), (80, 100));
assert_corners!(h, b_id, (80, 0), (300, 100));
h.resize((100, 100));
h.frame();
assert_corners!(h, id, (0, 0), (50, 100));
assert_corners!(h, b_id, (50, 0), (100, 100));
}
#[test]
fn comparisons_with_a_known_order_remain_plain_lengths() {
assert_eq!(leftover(2).max(leftover(5)), SizeRequest::from(leftover(5)));
assert_eq!(leftover(2).min(leftover(5)), SizeRequest::from(leftover(2)));
assert_eq!(
(px(10) + rel(0.5)).max(px(30) + rel(0.5)),
SizeRequest::from(px(30) + rel(0.5))
);
assert_eq!(LayoutLen::px(10).clamp(20, 80), SizeRequest::from(20));
}
#[test]
fn a_measured_nested_share_keeps_its_comparison_for_the_outer_span() {
let mut h = Harness::new((300, 100));
let a = h.rsc.widgets_mut().add_strong(Unhinted);
let a_id = a.id();
h.rsc
.widgets_mut()
.set_size_rule(a_id, Axis::X, SizeRule::max(Len::px(80.0)));
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
let b_id = b.id();
let inner = h.rsc.widgets_mut().add_strong(Span {
children: vec![a, b],
dir: Dir::RIGHT,
gap: Px::ZERO,
});
let c = rect(Color::GREEN).add_strong(&mut h.rsc);
let c_id = c.id();
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
children: vec![inner, c],
dir: Dir::RIGHT,
gap: Px::ZERO,
}));
h.frame();
assert_corners!(h, a_id, (0, 0), (80, 100));
assert_corners!(h, b_id, (80, 0), (190, 100));
assert_corners!(h, c_id, (190, 0), (300, 100));
h.rsc
.widgets_mut()
.mark_for_redraw(h.state.root.as_ref().unwrap().id());
h.frame();
assert_corners!(h, c_id, (190, 0), (300, 100));
}
struct Natural;
impl Widget for Natural {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(RectPrimitive::color(Color::RED));
Size::from_axis(Axis::X, LayoutLen::px(64), LayoutLen::px(64))
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(64))
}
}
#[test]
fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size() {
fn tree(h: &mut Harness) -> WidgetId {
let natural = h.rsc.widgets_mut().add_strong(Natural);
let id = natural.id();
h.rsc
.widgets_mut()
.set_size_rule(id, Axis::X, SizeRule::max(Len::rel(0.75)));
h.rsc.widgets_mut().set_size_rule(
id,
Axis::Y,
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
);
let inner = h.rsc.widgets_mut().add_strong(Stack {
children: vec![natural],
size: StackSize::Child(0),
});
let inner_id = inner.id();
let fill = rect(Color::BLUE).add_strong(&mut h.rsc);
let overlay = h.rsc.widgets_mut().add_strong(Stack {
children: vec![fill, inner],
size: StackSize::Child(0),
});
let share = rect(Color::GREEN).add_strong(&mut h.rsc);
let bounded = rect(Color::GREEN).add_strong(&mut h.rsc);
h.rsc.widgets_mut().set_size_rule(
bounded.id(),
Axis::X,
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
);
let fixed = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(&mut h.rsc);
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
children: vec![share, bounded, fixed, overlay],
dir: Dir::LEFT,
gap: Px::from_int(8),
}));
h.rsc.widgets_mut().set_size_rule(
h.state.root.as_ref().unwrap().id(),
Axis::Y,
LayoutLen::rel(1).into(),
);
h.frame();
inner_id
}
let mut warm = Harness::new((1920, 1200));
let a = tree(&mut warm);
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let b = tree(&mut cold);
assert_eq!(warm.region(&a), cold.region(&b));
}
#[test]
fn a_bound_can_extend_an_explicit_share_request() {
let mut h = Harness::new((300, 100));
let a = rect(Color::RED)
.width(leftover(1))
.min_width(100)
.add(&mut h.rsc);
let b = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((a, b).span(Dir::RIGHT));
assert_corners!(h, a, (0, 0), (150, 100));
h.resize((120, 100));
h.frame();
assert_corners!(h, a, (0, 0), (100, 100));
assert_corners!(h, b, (100, 0), (120, 100));
}
#[test]
fn moving_scroll_content_preserves_its_resolved_expression_size() {
let mut h = Harness::new((300, 300));
let leaf = rect(Color::BLUE).add_strong(&mut h.rsc);
let leaf_id = leaf.id();
let content = h.rsc.widgets_mut().add_strong(Stack {
children: vec![leaf],
size: StackSize::Child(0),
});
let content_id = content.id();
h.rsc
.widgets_mut()
.set_size_rule(content_id, Axis::Y, leftover(1).min(120).into());
let scroll = h
.rsc
.widgets_mut()
.add_strong(Scroll::new(content, Axis::Y));
let scroll_id = scroll.id();
h.state.root = Some(scroll);
for _ in 0..3 {
h.rsc.widgets_mut().mark_for_redraw(scroll_id);
h.frame();
assert_corners!(h, content_id, (0, 90), (300, 210));
assert_corners!(h, leaf_id, (0, 90), (300, 210));
}
h.resize((300, 600));
h.frame();
assert_corners!(h, leaf_id, (0, 240), (300, 360));
}
#[test]
fn an_intrinsic_share_cap_uses_the_allocators_fractional_base() {
let mut h = Harness::new((300, 100));
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
let bounded = rect(Color::RED).max_width(Len::rel(0.25)).add(&mut h.rsc);
let tail = rect(Color::GREEN).add(&mut h.rsc);
h.set_root((head, bounded, tail).span(Dir::RIGHT));
assert_corners!(h, bounded, (30, 0), (105, 100));
assert_corners!(h, tail, (105, 0), (300, 100));
h.resize((400, 100));
h.frame();
assert_corners!(h, bounded, (30, 0), (130, 100));
assert_corners!(h, tail, (130, 0), (400, 100));
}
+402 -35
View File
@@ -260,32 +260,52 @@ fn a_share_rule_beats_the_widgets_own_pixel_size() {
assert_eq!(asked.get(), 400.0, "the share is all of the box"); assert_eq!(asked.get(), 400.0, "the share is all of the box");
} }
/// A share with pixels or a fraction beside it is the longer of the two: it /// Every box a widget is given comes of one ask, and the window is one of
/// fills what they leave of the box and overflows the box where they are /// them: the root is asked in it exactly as a child is asked in its parent's
/// longer than it. A parent that divides nothing gives the same length as a /// box, so a rule of its own reads the same way at either place.
/// span with one child, because in both there is nobody else to divide with. #[derive(Clone, Copy, Debug)]
#[test] enum Asked {
fn a_share_is_a_minimum_wherever_nothing_divides_it() { Root,
let asked = |rule: LayoutLen, in_a_span: bool| { Wrapped,
InASpan,
}
impl Asked {
const ALL: [Self; 3] = [Self::Root, Self::Wrapped, Self::InASpan];
/// The width the probe is given under this parent, in a 400 px window.
fn width(&self, rule: LayoutLen) -> Px {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc); let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, rule); h.set_len(probe, Axis::X, rule);
match in_a_span { match self {
true => h.set_root((probe,).span(Dir::RIGHT)), Self::Root => h.set_root(probe),
false => h.set_root(probe.wrapper()), Self::Wrapped => h.set_root(probe.wrapper()),
Self::InASpan => h.set_root((probe,).span(Dir::RIGHT)),
} }
h.region(&probe).unwrap().size().x h.region(&probe).unwrap().size().x
}; }
}
/// A share with pixels or a fraction beside it is the longer of the two: it
/// fills what they leave of the box and overflows the box where they are
/// longer than it. A parent that divides nothing gives the same length as a
/// span with one child, because in both there is nobody else to divide with --
/// and so does the window, which divides nothing either.
#[test]
fn a_share_is_a_minimum_wherever_nothing_divides_it() {
for (rule, want) in [ for (rule, want) in [
(LayoutLen::LEFTOVER, 400), (LayoutLen::LEFTOVER, 400),
(LayoutLen::px(50) + LayoutLen::LEFTOVER, 400), (LayoutLen::px(50.0) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500) + LayoutLen::LEFTOVER, 500), (LayoutLen::px(500.0) + LayoutLen::LEFTOVER, 500),
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400), (LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500), 500), (LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800),
(LayoutLen::px(500.0), 500),
] { ] {
let want = Px::from_int(want); let want = Px::from_int(want);
assert_eq!(asked(rule, false), want, "{rule:?} where nothing divides"); for asked in Asked::ALL {
assert_eq!(asked(rule, true), want, "{rule:?} in a span"); assert_eq!(asked.width(rule), want, "{rule:?} asked {asked:?}");
}
} }
} }
@@ -297,27 +317,36 @@ fn a_share_is_a_minimum_wherever_nothing_divides_it() {
/// way, so it reaches the parent as a length only the parent can resolve. /// way, so it reaches the parent as a length only the parent can resolve.
#[test] #[test]
fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() { fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() {
let mut h = Harness::new((400, 200)); // At the root as well as under a parent: the comparison is the same one,
let probe = rect(Color::RED).add(&mut h.rsc); // and nothing above the root will make it again on its behalf, so the
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); // range it holds for is the root's own.
h.set_root(probe.wrapper()); for wrapped in [false, true] {
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500)); let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
match wrapped {
true => h.set_root(probe.wrapper()),
false => h.set_root(probe),
}
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
h.resize((900, 200)); h.resize((900, 200));
h.frame(); h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900)); assert_eq!(width(&h), Px::from_int(900), "wrapped: {wrapped}");
h.resize((400, 200)); h.resize((400, 200));
h.frame(); h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500)); assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
h.set_len(probe, Axis::X, LayoutLen::px(50) + LayoutLen::LEFTOVER); h.set_len(probe, Axis::X, LayoutLen::px(50.0) + LayoutLen::LEFTOVER);
h.frame(); h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400)); assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}");
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
h.frame(); h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500)); assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
}
} }
#[test] #[test]
@@ -727,7 +756,7 @@ fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
let mut h = Harness::new((100, 20)); let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc); let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let mixed = rect(Color::BLUE) let mixed = rect(Color::BLUE)
.width(LayoutLen::px(20) + LayoutLen::LEFTOVER) .width(LayoutLen::px(20.0) + LayoutLen::LEFTOVER)
.add(&mut h.rsc); .add(&mut h.rsc);
h.set_root((fixed, mixed).span(Dir::RIGHT)); h.set_root((fixed, mixed).span(Dir::RIGHT));
@@ -795,7 +824,7 @@ fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>
if let Some(len) = kid { if let Some(len) = kid {
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len)); .set_size_rule(r.id(), Axis::X, SizeRule::from(len));
} }
ids.push(r.id()); ids.push(r.id());
kids.push(r.add_strong(&mut h.rsc)); kids.push(r.add_strong(&mut h.rsc));
@@ -959,3 +988,341 @@ fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
} }
} }
} }
/// The root is asked the way any child is, so what it says about itself is
/// read there too: a root that opted into a region node gets one, where the
/// path it used to have ignored the flag.
#[test]
fn a_region_node_root_is_a_region_node() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
let root = (probe,).span(Dir::RIGHT).region_node().add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400));
h.resize((900, 200));
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
}
/// A bound holds the length a widget reports as well as narrowing the box it
/// is offered, and the two are not the same question. Here two 200-wide rects
/// fill a row in a 250 window, so a cap of 300 leaves the box alone and only
/// cuts what the row reports -- which the window then centres, past both its
/// edges -- while a floor raises the report and the rects stay where the 250
/// box put them.
#[test]
fn a_bound_holds_what_a_widget_answers() {
let bounded_row = |rule: SizeRule| {
let mut h = Harness::new((250, 200));
let left = rect(Color::RED).width(200).add(&mut h.rsc);
let right = rect(Color::BLUE).width(200).add(&mut h.rsc);
let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rule(row, Axis::X, rule);
h.set_root(row);
(h, row, left)
};
let (h, row, left) = bounded_row(SizeRule::max(Len::px(300.0)));
assert_eq!(
h.region(&row).unwrap().size().x,
Px::from_int(300),
"the cap, not the 400 drawn"
);
assert_eq!(
h.region(&left).unwrap().size().x,
Px::from_int(200),
"the box the children were given"
);
assert_corners!(h, row, (-25, 0), (275, 200));
let (h, row, _) = bounded_row(SizeRule::min(Len::px(600.0)));
assert_eq!(
h.region(&row).unwrap().size().x,
Px::from_int(600),
"the floor, not the 400 drawn"
);
let (h, row, _) = bounded_row(SizeRule::FREE);
assert_eq!(
h.region(&row).unwrap().size().x,
Px::from_int(400),
"what it drew"
);
}
/// A cap narrows the box the widget is asked in, whether a declaration of its
/// own decides that box or the allocator divides a share into it. The cap is
/// an attribute of the widget rather than something wrapped around it, which
/// is what the id assertions say.
#[test]
fn a_cap_attribute_narrows_the_widgets_box() {
let mut h = Harness::new((400, 200));
// A fraction of its box, so it says what box it was asked in.
let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc);
let capped = fills.max_width(300).add(&mut h.rsc);
assert_eq!(fills.id(), capped.id());
h.set_root(capped);
assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300));
// A share with nothing beside it: the cap is composed into the request
// and the allocator answers with it rather than the whole 400.
let mut h = Harness::new((400, 200));
let share = rect(Color::RED).width(leftover(1)).add(&mut h.rsc);
let capped = share.max_width(300).add(&mut h.rsc);
assert_eq!(share.id(), capped.id());
h.set_root(capped);
assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300));
}
#[test]
fn a_cap_attribute_is_decided_again_on_either_side_of_the_crossing() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_root(probe.max_width(300));
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
assert_eq!(width(&h), Px::from_int(300));
h.resize((250, 200));
h.frame();
assert_eq!(
width(&h),
Px::from_int(250),
"its box, which is under the cap"
);
h.resize((400, 200));
h.frame();
assert_eq!(width(&h), Px::from_int(300));
}
/// A fraction in a cap is a fraction of the box the widget capping it was
/// given, which is the box a declared length of its own would be a fraction
/// of -- not of the window, and not of what the cap itself decided.
#[test]
fn a_cap_is_a_fraction_of_the_box_it_was_given() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_root(probe.max_width(Len::rel(0.5)).pad(Padding::uniform(50)));
// Half of the 300 left by the padding, not half of the window.
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(150));
}
struct Offered {
seen: Rc<Cell<PxVec2>>,
answer: Size,
}
impl Widget for Offered {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.seen.set(painter.px_size());
painter.primitive(RectPrimitive::color(Color::RED));
self.answer
}
}
#[test]
fn bounds_constrain_the_offer_without_replacing_an_intrinsic_answer() {
for axis in Axis::BOTH {
for node in [false, true] {
let mut h = Harness::new((400, 400));
let seen = Rc::new(Cell::new(PxVec2::ZERO));
let probe = h.rsc.widgets_mut().add_strong(Offered {
seen: seen.clone(),
answer: Size::px(Vec2::new(40.0, 40.0)),
});
let id = probe.id();
h.rsc.widgets_mut().set_region_node(id, node);
h.rsc.widgets_mut().set_max_len(id, axis, 100.into());
h.state.root = Some(probe);
h.frame();
assert_eq!(seen.get()[axis], Px::from_int(100));
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
h.rsc.widgets_mut().set_min_len(id, axis, 60.into());
h.frame();
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(60));
h.resize((50, 50));
h.frame();
assert_eq!(seen.get()[axis], Px::from_int(60));
h.rsc.widgets_mut().set_size_rule(id, axis, SizeRule::FREE);
h.frame();
assert_eq!(seen.get()[axis], Px::from_int(50));
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
}
}
}
#[test]
fn a_cap_attribute_is_the_scroll_viewport() {
for node in [false, true] {
let mut h = Harness::new((400, 400));
let content = rect(Color::RED).height(400).add(&mut h.rsc);
let inner = content.add_strong(&mut h.rsc);
let scroll = Scroll::new(inner, Axis::Y).max_height(100).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(scroll, node);
h.set_root(scroll);
assert_corners!(h, scroll, (0, 150), (400, 250));
assert_corners!(h, content, (0, -150), (400, 250));
h.rsc.widgets_mut().get_mut(&scroll).unwrap().scroll(1000.0);
h.frame();
assert_corners!(h, content, (0, 150), (400, 550));
h.resize((400, 80));
h.frame();
assert_corners!(h, scroll, (0, 0), (400, 80));
assert_corners!(h, content, (0, 0), (400, 400));
}
}
#[test]
fn a_cap_attribute_wraps_text_before_it_answers() {
let mut h = Harness::new((400, 500));
let text = wtext("one two three four five six seven eight nine ten")
.size(16)
.wrap(true)
.max_width(80)
.align(Align::TOP_LEFT)
.add(&mut h.rsc);
h.set_root(text);
let capped = h.region(&text).unwrap().size();
assert!(capped.x <= Px::from_int(80));
assert!(capped.y > Px::from_int(30));
h.rsc
.widgets_mut()
.set_size_rule(text, Axis::X, SizeRule::FREE);
h.frame();
let free = h.region(&text).unwrap().size();
assert!(free.x > capped.x);
assert!(free.y < capped.y);
}
#[test]
fn dimensions_and_bounds_are_independent_attributes_in_either_order() {
for bounds_first in [false, true] {
let mut h = Harness::new((400, 400));
let probe = rect(Color::RED).add(&mut h.rsc);
let bounded = if bounds_first {
probe
.max_width(80)
.min_height(60)
.width(120)
.height(40)
.add(&mut h.rsc)
} else {
probe
.width(120)
.height(40)
.max_width(80)
.min_height(60)
.add(&mut h.rsc)
};
assert_eq!(probe.id(), bounded.id());
h.set_root(bounded);
assert_eq!(
h.region(&probe).unwrap().size(),
PxVec2::from_f32((80, 60).into())
);
h.rsc.widgets_mut().set_len(probe, Axis::X, 50);
h.rsc.widgets_mut().set_len(probe, Axis::Y, 100);
h.frame();
assert_eq!(
h.region(&probe).unwrap().size(),
PxVec2::from_f32((50, 100).into())
);
}
}
#[test]
fn changing_a_share_cap_replaces_it_without_losing_the_share() {
let mut h = Harness::new((300, 100));
let capped = rect(Color::RED)
.max_width(80)
.width(leftover(1))
.add(&mut h.rsc);
let sibling = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((capped, sibling).span(Dir::RIGHT));
assert_corners!(h, capped, (0, 0), (80, 100));
assert_corners!(h, sibling, (80, 0), (300, 100));
h.rsc.widgets_mut().set_max_len(capped, Axis::X, 160.into());
h.frame();
assert_corners!(h, capped, (0, 0), (150, 100));
assert_corners!(h, sibling, (150, 0), (300, 100));
}
// Reduced from seed 104 at depth 5: a widget widening its own window
// contract must not erase the bound's crossing at a quarter-window of 173.
#[test]
fn a_widgets_window_contract_cannot_widen_its_bounds_contract() {
fn tree(h: &mut Harness) -> WidgetId {
let content = rect(Color::RED)
.width(137)
.min_height(194)
.add_strong(&mut h.rsc);
let scroll = Scroll::new(content, Axis::X).add_strong(&mut h.rsc);
let probe = rect(Color::RED).add_strong(&mut h.rsc);
let id = probe.id();
let wide = rect(Color::GREEN).add_strong(&mut h.rsc);
let narrow = rect(Color::BLUE).add_strong(&mut h.rsc);
let branch = iris::random::Branch {
probe,
wide,
narrow,
threshold: 459.0,
}
.min_width(94)
.max_width(173)
.add_strong(&mut h.rsc);
let stack = Stack {
children: vec![scroll, branch],
size: StackSize::Child(0),
}
.add_strong(&mut h.rsc);
let mut children: Vec<StrongWidget> = (0..3)
.map(|_| rect(Color::RED).add_strong(&mut h.rsc).any())
.collect();
children.push(stack);
h.set_root(
Span {
children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.height(rel(1)),
);
id
}
let mut warm = Harness::new((1920, 1200));
let probe = tree(&mut warm);
warm.resize((640, 900));
warm.frame();
assert_corners!(warm, probe, (480, 0), (640, 40));
let mut cold = Harness::new((640, 900));
let other = tree(&mut cold);
assert_eq!(warm.region(&probe), cold.region(&other));
}
#[test]
fn a_fixed_declaration_keeps_its_cap_when_the_row_has_no_leftover() {
let mut h = Harness::new((100, 100));
let fixed = rect(Color::RED).width(200).max_width(100).add(&mut h.rsc);
let share = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((fixed, share).span(Dir::RIGHT));
assert_corners!(h, fixed, (0, 0), (100, 100));
}
#[test]
fn a_new_bound_reaches_the_parent_even_when_the_current_answer_is_unchanged() {
let mut h = Harness::new((400, 100));
let leaf = rect(Color::RED).add(&mut h.rsc);
let inner = leaf.add_strong(&mut h.rsc);
let root = Scroll::new(inner, Axis::Y)
.pad(Padding::uniform(0))
.width(154)
.height(100)
.add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&leaf).unwrap().size().x, Px::from_int(154));
h.rsc.widgets_mut().set_min_len(leaf, Axis::X, 140.into());
h.rsc.widgets_mut().set_len(root, Axis::X, 78);
h.frame();
assert_corners!(h, leaf, (130, 0), (270, 100));
}
+2 -2
View File
@@ -31,8 +31,8 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
( (
i, i,
SizeRules { SizeRules {
x: SizeRule::Exact(LayoutLen::LEFTOVER), x: SizeRule::from(LayoutLen::LEFTOVER),
y: SizeRule::Free, y: SizeRule::FREE,
}, },
) )
}) })
+23
View File
@@ -161,3 +161,26 @@ fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
assert_corners!(h, scroll, (0, 100), (400, 400)); assert_corners!(h, scroll, (0, 100), (400, 400));
assert_corners!(h, inner, (0, 225), (400, 275)); assert_corners!(h, inner, (0, 225), (400, 275));
} }
/// A cap narrows the box the widget is asked in, which is what a scroll
/// measures its viewport from: the content scrolls within the cap rather than
/// within the room the cap was cut from.
#[test]
fn a_capped_scroll_takes_its_viewport_from_the_cap() {
let mut h = Harness::new((400, 200));
let top = rect(Color::RED).height(200).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
let scroll = (top, bottom).span(Dir::DOWN).scrollable().add(&mut h.rsc);
let capped = scroll.max_height(100).add(&mut h.rsc);
h.set_root(capped);
h.move_to((200, 50));
// 400 of content in a viewport of 100, so 300 to scroll and the end
// showing: the top is 300 above the box, which the window centres.
assert_eq!(h.region(&scroll).unwrap().size().y, Px::from_int(100));
assert_corners!(h, top, (0, -250), (400, -50));
h.scroll((0, 1));
h.frame();
assert_corners!(h, top, (0, -200), (400, 0));
}
+85
View File
@@ -0,0 +1,85 @@
mod rig;
#[path = "scenario/mod.rs"]
mod scenario;
use iris::prelude::*;
use iris::random::{Edits, Plan, plan};
fn check_requests(edit: impl Fn(&mut Plan) + Sync) {
let depth = rig::env("IRIS_DEFERRED_DEPTH", 4_usize);
let seeds = rig::seeds("IRIS_DEFERRED_SEED", "IRIS_DEFERRED_SEEDS", 20);
scenario::over_seeds(seeds, |seed| {
let mut grown = plan(seed, depth, &Edits::default());
edit(&mut grown);
for case in scenario::ALL {
if let Some(how) = scenario::diverges(&grown, case, seed) {
panic!(
"request seed {seed} depth {depth} after {}: {how}",
case.name()
);
}
}
});
}
#[test]
fn deferred_requests_agree_warm_and_cold() {
check_requests(|grown| {
let mut index = 0;
grown.walk_mut(&mut |node| {
if let Some(rules) = &mut node.size {
for axis in Axis::BOTH {
index += 1;
rules[axis] = match index % 7 {
0 => leftover(1).clamp(20, 120).into(),
1 => leftover(1).min(rel(0.5)).into(),
2 => (leftover(1) + px(30)).min(leftover(2)).into(),
// Both sides an expression, the one shape that
// copies a request's nodes into another's.
3 => leftover(1).min(px(40)).max(leftover(2).min(px(70))).into(),
_ => rules[axis].clone(),
};
}
}
});
});
}
#[test]
fn relative_intrinsic_bounds_agree_warm_and_cold() {
check_requests(|grown| {
grown.walk_mut(&mut |node| {
if let Some(rules) = &mut node.size {
for axis in Axis::BOTH {
let bound = &mut rules[axis].bound;
if bound.min.is_some() {
bound.min = Some(Len::rel(0.25));
}
if bound.max.is_some() {
bound.max = Some(Len::rel(0.75));
}
}
}
});
});
}
#[test]
fn preferred_requests_with_independent_bounds_agree_warm_and_cold() {
check_requests(|grown| {
let mut index = 0;
grown.walk_mut(&mut |node| {
if let Some(rules) = &mut node.size {
for axis in Axis::BOTH {
index += 1;
rules[axis].request = Some(match index % 4 {
0 => leftover(1).into(),
1 => rel(0.5).into(),
2 => px(80).into(),
_ => (leftover(1) + px(30)).min(leftover(2)),
});
}
}
});
});
}
+4 -8
View File
@@ -11,11 +11,13 @@
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH` //! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
//! select what the long run covers. //! select what the long run covers.
mod rig;
#[path = "scenario/mod.rs"] #[path = "scenario/mod.rs"]
mod scenario; mod scenario;
use iris::random::{Edits, Plan, plan}; use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds}; use rig::env;
use scenario::{ALL, Case, diverges, over_seeds};
/// How deep the generator branches. The generator widens two to four ways per /// How deep the generator branches. The generator widens two to four ways per
/// level, so depth is exponential in width and a deep narrow tree is not /// level, so depth is exponential in width and a deep narrow tree is not
@@ -111,13 +113,7 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
#[ignore = "as many seeds as it is asked for, rather than the ten the others check"] #[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
fn a_long_run_of_seeds_agrees() { fn a_long_run_of_seeds_agrees() {
let depth = depth(); let depth = depth();
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED") let seeds = rig::seeds("IRIS_GENERATED_SEED", "IRIS_GENERATED_SEEDS", 100);
.ok()
.and_then(|v| v.parse().ok())
{
Some(seed) => vec![seed],
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
};
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default()); let grown = plan(seed, depth, &Edits::default());
for case in ALL { for case in ALL {
+33 -12
View File
@@ -13,11 +13,15 @@
//! //!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or //! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and //! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
//! `IRIS_DIRTY` how many widgets `many` marks at once. //! `IRIS_DIRTY` how many widgets `many` marks at once. `IRIS_UNBOUNDED=1`
//! removes intrinsic bounds while preserving the rest of the generated tree.
mod rig;
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::{Edits, Tree, grow}; use iris::random::{Edits, Tree, build, plan};
use rig::env;
use std::time::Instant; use std::time::Instant;
const OUTPUT: (f32, f32) = (1920.0, 1200.0); const OUTPUT: (f32, f32) = (1920.0, 1200.0);
@@ -93,13 +97,6 @@ fn a_selected_widget_retains_its_layout_events() {
diagnostics::clear_traced_widgets(); diagnostics::clear_traced_widgets();
} }
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)
}
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
fn trace_selected(tree: &Tree) { fn trace_selected(tree: &Tree) {
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else { let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
@@ -125,9 +122,17 @@ fn rig_edits() -> Edits {
} }
} }
fn fixture(harness: &mut Harness, seed: u64, depth: usize) -> (StrongWidget, Tree) {
let mut plan = plan(seed, depth, &rig_edits());
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
plan.drop_bounds();
}
build(&mut harness.rsc, &plan)
}
fn warm(seed: u64, depth: usize) -> (Harness, Tree) { fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits()); let (root, tree) = fixture(&mut harness, seed, depth);
harness.state.root = Some(root); harness.state.root = Some(root);
harness.frame(); harness.frame();
println!( println!(
@@ -209,7 +214,7 @@ fn layout_cost() {
if selected("cold") { if selected("cold") {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits()); let (root, tree) = fixture(&mut harness, seed, depth);
harness.state.root = Some(root); harness.state.root = Some(root);
println!( println!(
"fixture: seed {seed}, depth {depth}, {} widgets", "fixture: seed {seed}, depth {depth}, {} widgets",
@@ -255,7 +260,7 @@ fn layout_cost() {
harness harness
.rsc .rsc
.widgets_mut() .widgets_mut()
.set_size_rule(sized, Axis::X, SizeRule::Exact(len)); .set_size_rule(sized, Axis::X, SizeRule::from(len));
}); });
} }
@@ -276,3 +281,19 @@ fn layout_cost() {
}); });
} }
} }
#[cfg(feature = "layout-diagnostics")]
#[test]
fn repainting_measured_text_does_not_invalidate_its_span() {
use iris::core::layout_diagnostics as diag;
let mut h = Harness::new((400, 200));
let text = wtext("a paragraph that fits").wrap(true).add(&mut h.rsc);
h.set_root((text, wtext("another paragraph")).span(Dir::DOWN));
let _ = diag::take();
h.rsc.widgets_mut().mark_for_redraw(text);
h.frame();
let report = diag::take();
assert_eq!(report.distinct_widgets(), 1);
assert_eq!(report.hot_widgets()[0].id, text.id());
}
+11 -10
View File
@@ -8,17 +8,14 @@
//! //!
//! then the same after, and `diff` the two. A line is one widget: the seed, //! 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 //! its index in creation order, and its box in window pixels, or `-` where
//! it is not drawn. //! it is not drawn. `IRIS_UNBOUNDED=1` drops the trees' intrinsic bounds, as
//! in the diagnostics rig, which compares the two paths over the same shapes.
mod rig;
use iris::harness::Harness; use iris::harness::Harness;
use iris::random::{Edits, grow}; use iris::random::{Edits, build, plan};
use rig::env;
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] #[test]
#[ignore = "a dump to diff across commits, not a check"] #[ignore = "a dump to diff across commits, not a check"]
@@ -28,7 +25,11 @@ fn every_cold_layout_is_printed() {
let mut out = String::new(); let mut out = String::new();
for seed in 1..=seeds { for seed in 1..=seeds {
let mut harness = Harness::new((1920.0, 1200.0)); let mut harness = Harness::new((1920.0, 1200.0));
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); let mut plan = plan(seed, depth, &Edits::default());
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
plan.drop_bounds();
}
let (root, tree) = build(&mut harness.rsc, &plan);
harness.state.root = Some(root); harness.state.root = Some(root);
harness.frame(); harness.frame();
for (index, id) in tree.ids.iter().enumerate() { for (index, id) in tree.ids.iter().enumerate() {
+85 -19
View File
@@ -1,22 +1,28 @@
//! What a resize frame costs and what it holds, on a tree the revision before //! Text-layout workloads with stable paragraphs for comparisons across revisions.
//! #16 also builds. //! PR #19's base uses the older spelling of the fixed 40-pixel width and has
//! //! no diagnostics. The random generator changed with layout, so it cannot
//! Deliberately written in the API subset `43ce8c7` and this branch share, so //! provide the same workload across the full PR.
//! the same source can be dropped into an old worktree and measured there:
//! that is the only like-for-like comparison with the code the retained
//! layout replaced. The random tree cannot carry one, because the generator
//! itself changed with the work.
//! //!
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \ //! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
//! -- --ignored --nocapture resize_cost //! -- --ignored --nocapture resize_cost
//! PHASE=edit ROWS=40 FRAMES=2000 cargo test --release --test revision_cost \
//! -- --ignored --nocapture text_updates_cost
//! ROWS=2000 cargo test --release --test revision_cost \ //! ROWS=2000 cargo test --release --test revision_cost \
//! -- --ignored --nocapture text_memory //! -- --ignored --nocapture text_memory
//! //!
//! Wall time on this machine varies with CPU frequency; take the number from //! `text_updates_cost` selects idle, repaint, edit, or scroll with `PHASE`.
//! `perf stat -e instructions:u` on the test binary directly. //! It alternates a short suffix for edits so later frames do not get a longer
//! paragraph than earlier ones. These are CPU fixtures, with no GPU submission.
//!
//! Use repeated `perf stat -e instructions:u` runs on the executable directly;
//! process totals include font loading and the cold frame, so compare identical
//! row and frame counts. Wall time on this machine is not a stable comparison.
mod rig;
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use rig::env;
use std::time::Instant; use std::time::Instant;
/// xorshift64, so one seed is one set of paragraphs on any machine. /// xorshift64, so one seed is one set of paragraphs on any machine.
@@ -78,13 +84,6 @@ fn words(rng: &mut Rng, least: usize, most: usize) -> String {
const OUTPUT: (f32, f32) = (900.0, 1200.0); const OUTPUT: (f32, f32) = (900.0, 1200.0);
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)
}
/// A row of a fixed-width rect beside a column of one wrapping and one /// A row of a fixed-width rect beside a column of one wrapping and one
/// overflowing text: the shape that makes a container measure a child in a /// overflowing text: the shape that makes a container measure a child in a
/// box it will not keep. /// box it will not keep.
@@ -137,9 +136,10 @@ fn resize_cost() {
println!("paragraph {at}: {:?}", h.region(id)); println!("paragraph {at}: {:?}", h.region(id));
} }
// Two widths in turn is the friendly case for anything that remembers an // The sweep cycles 256 widths, avoiding the two-width cache-friendly case.
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
let sweep = env("SWEEP", 0_usize) != 0; let sweep = env("SWEEP", 0_usize) != 0;
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
let mut elapsed = Vec::with_capacity(frames); let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames { for frame in 0..frames {
let narrower = match sweep { let narrower = match sweep {
@@ -151,6 +151,11 @@ fn resize_cost() {
h.frame(); h.frame();
elapsed.push(start.elapsed().as_secs_f64() * 1000.0); elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
} }
#[cfg(feature = "layout-diagnostics")]
print!(
"{}",
iris::core::layout_diagnostics::take().per_frame(frames)
);
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap()); elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!( println!(
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \ "resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
@@ -203,3 +208,64 @@ fn text_memory() {
} }
report("after settling"); report("after settling");
} }
#[test]
#[ignore = "measurement, not a check"]
fn text_updates_cost() {
let rows = env("ROWS", 40_usize);
let frames = env("FRAMES", 1000_usize);
let phase = env("PHASE", String::from("edit"));
assert!(rows > 0 && frames > 0);
assert!(["idle", "repaint", "edit", "scroll"].contains(&phase.as_str()));
let mut h = Harness::new(OUTPUT);
let mut rng = Rng(1);
let mut col = Span::empty(Dir::DOWN);
let first = wtext(words(&mut rng, 12, 52))
.size(16)
.wrap(true)
.add(&mut h.rsc);
col.push(first.add_strong(&mut h.rsc));
for _ in 1..rows {
col.push(
wtext(words(&mut rng, 12, 52))
.size(16)
.wrap(true)
.add_strong(&mut h.rsc),
);
}
let root = col.scrollable().add(&mut h.rsc);
h.set_root(root);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
let original = h.rsc[first].content.to_string();
let alternate = format!("{original} another word");
let start = Instant::now();
for frame in 0..frames {
match phase.as_str() {
"idle" => {}
"repaint" => {
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
}
"edit" => {
h.rsc[first].content.clear();
h.rsc[first].content.push_str(if frame % 2 == 0 {
&alternate
} else {
&original
});
}
"scroll" => h.rsc[root].scroll(if frame % 2 == 0 { -12.0 } else { 12.0 }),
_ => unreachable!(),
}
h.frame();
}
println!(
"{phase}: {rows} rows, {frames} frames, {:.1} ms",
start.elapsed().as_secs_f64() * 1000.0
);
#[cfg(feature = "layout-diagnostics")]
print!(
"{}",
iris::core::layout_diagnostics::take().per_frame(frames)
);
}
+25
View File
@@ -0,0 +1,25 @@
//! What the rigs need and none of them should spell its own way. Every
//! fuzzer and measurement here is run by hand with its parameters in the
//! environment, so one reader is shared rather than copied into each target.
/// A rig's parameter from the environment, or its default. A switch is
/// `env("NAME", 0_u8) != 0`, so `NAME=1` turns it on.
pub 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)
}
/// The seeds a scan runs: the one `one` names on its own, or `1..=` the
/// count `many` gives. One seed replaces the range rather than narrowing
/// it, which is how a tree a scan failed on is run again by itself.
// This module is compiled into each rig target separately, so a helper the
// measurement rigs have no seeds to choose is dead code in those builds.
#[allow(dead_code)]
pub fn seeds(one: &str, many: &str, count: u64) -> Vec<u64> {
match std::env::var(one).ok().and_then(|seed| seed.parse().ok()) {
Some(seed) => vec![seed],
None => (1..=env(many, count)).collect(),
}
}
+30 -17
View File
@@ -30,13 +30,6 @@ pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
}); });
} }
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
/// The window a tree is grown in, and the one a resize takes it to. /// The window a tree is grown in, and the one a resize takes it to.
const OUTER: (f32, f32) = (1920.0, 1200.0); const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0); const INNER: (f32, f32) = (640.0, 900.0);
@@ -194,18 +187,26 @@ fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
} }
} }
fn a_len(rng: &mut Rng) -> Option<LayoutLen> { /// A length in pixels, or a cap over one: a rule that reads the box it is
Some(LayoutLen::px(20.0 + rng.below(180) as f32)) /// given is the one a resize can change the effect of without changing the
/// rule, so a tree that never grows one leaves that unexercised.
fn a_rule(rng: &mut Rng) -> SizeRule {
let len = Len::px(20.0 + rng.below(180) as f32);
match rng.below(4) {
0 => SizeRule::max(len),
1 => SizeRule::min(len),
_ => LayoutLen::from(len).into(),
}
} }
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules { fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules {
let lens = SizeRules { let lens = SizeRules {
x: a_len(rng).into(), x: a_rule(rng),
y: a_len(rng).into(), y: a_rule(rng),
}; };
warm.rsc warm.rsc
.widgets_mut() .widgets_mut()
.set_size_rules(tree.sized[idx], lens.x, lens.y); .set_size_rules(tree.sized[idx], lens.x.clone(), lens.y.clone());
lens lens
} }
@@ -349,9 +350,21 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
/// buildable from what the failure printed. /// buildable from what the failure printed.
fn describe(id: WidgetId, h: &Harness) -> String { fn describe(id: WidgetId, h: &Harness) -> String {
let rules = h.rsc.widgets().size_rules(id); let rules = h.rsc.widgets().size_rules(id);
let rule = |r: SizeRule| match r.exact() { // The tree a failure names is written out again from what it printed, so
Some(len) => format!("{len}"), // every part of a rule prints: the preferred length and the bounds are
None => "-".into(), // independent, and a bound lumped into "no rule" could not be rebuilt.
let rule = |r: &SizeRule| {
let mut out = r
.request
.as_ref()
.map_or_else(String::new, ToString::to_string);
if let Some(min) = r.bound.min {
out += &format!(">{}", LayoutLen::from(min));
}
if let Some(max) = r.bound.max {
out += &format!("<{}", LayoutLen::from(max));
}
if out.is_empty() { "-".into() } else { out }
}; };
let align = h.rsc.widgets().alignment(id); let align = h.rsc.widgets().alignment(id);
let side = |a: AxisAlign| { let side = |a: AxisAlign| {
@@ -368,8 +381,8 @@ fn describe(id: WidgetId, h: &Harness) -> String {
// A rule and an alignment are properties of whatever carries them, so // A rule and an alignment are properties of whatever carries them, so
// they print with that widget rather than as widgets of their own. // they print with that widget rather than as widgets of their own.
let mut out = describe_widget(id, h); let mut out = describe_widget(id, h);
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) { if *rules != SizeRules::default() {
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y)); out += &format!("[x:{},y:{}]", rule(&rules.x), rule(&rules.y));
} }
if align != RegionAlign::default() { if align != RegionAlign::default() {
out += &format!("@{},{}", side(align.x), side(align.y)); out += &format!("@{},{}", side(align.x), side(align.y));
+4 -8
View File
@@ -17,11 +17,13 @@
//! It is a fuzzer: run it once the ordinary tests pass, and turn what it //! It is a fuzzer: run it once the ordinary tests pass, and turn what it
//! finds into a test of its own rather than leaving a seed as the record. //! finds into a test of its own rather than leaving a seed as the record.
mod rig;
#[path = "scenario/mod.rs"] #[path = "scenario/mod.rs"]
mod scenario; mod scenario;
use iris::random::{Edits, Plan, plan}; use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds}; use rig::env;
use scenario::{ALL, Case, diverges, over_seeds};
/// Takes the first simplification that still fails, until none does. The /// Takes the first simplification that still fails, until none does. The
/// simplifications come biggest first, so this walks down rather than /// simplifications come biggest first, so this walks down rather than
@@ -58,13 +60,7 @@ fn cases() -> Vec<Case> {
fn no_grown_tree_lays_out_differently_warm_than_cold() { fn no_grown_tree_lays_out_differently_warm_than_cold() {
let depth: usize = env("SHRINK_DEPTH", 5); let depth: usize = env("SHRINK_DEPTH", 5);
let cases = cases(); let cases = cases();
let seeds: Vec<u64> = match std::env::var("SHRINK_SEED") let seeds = rig::seeds("SHRINK_SEED", "SHRINK_SEEDS", 400);
.ok()
.and_then(|v| v.parse().ok())
{
Some(seed) => vec![seed],
None => (1..=env("SHRINK_SEEDS", 400_u64)).collect(),
};
let count = seeds.len(); let count = seeds.len();
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
+2
View File
@@ -8,6 +8,8 @@
//! The rigs stay their own targets: `shrink` and `generated` are fuzzers run //! The rigs stay their own targets: `shrink` and `generated` are fuzzers run
//! on their own, and the `*_cost` and `*_diagnostics` ones are measurements. //! on their own, and the `*_cost` and `*_diagnostics` ones are measurements.
#[path = "cases/deferred.rs"]
mod deferred;
#[path = "cases/determinism.rs"] #[path = "cases/determinism.rs"]
mod determinism; mod determinism;
#[path = "cases/drift.rs"] #[path = "cases/drift.rs"]