Compare commits

...
15 Commits
Author SHA1 Message Date
iris-aiandClaude Fable 5.1 f61e8936f1 Restore abs() in the rect shader, and validate every shader without a device
`7c50a3e` renamed a length's `abs` component to `px` and took the WGSL `abs()`
builtin in the rounded-rect distance with it, so every window failed shader
validation on the first frame while `cargo test` stayed green. `naga` is
reachable through `wgpu`, so a unit test now composes each shader file with the
prelude the way the renderer does and parses and validates it; it reads the
shader directory rather than naming primitives, so a new one is covered by
adding its file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 02:16:56 -04:00
iris-aiandClaude Fable 5.1 02ff8c7454 Measure a dirty widget where its parent asked, not in a box its answer decided
A local redraw drew a dirty widget in the box it was placed in. When a reader
decided that box from the widget's own answer -- an aligned span sized to its
children, a text at the tail of a row, a scroll's content -- the old answer is a
fixed point of measuring there whatever the content now says, so the layout had
two stable answers and which one it reached depended on the tree's history.
`tests/unsettled.rs` has the two shrunk cases: the four-widget aligned span,
and a scroll placing a pass-through `SetSize` in a box the content decided,
where the span under it was placed once and nothing at its own edge said so.

`ActiveData::offered_px` keeps the pixel size of the box the parent first asked
about the child in, whether through `known_len` or a first `place`, beside `px`,
the box it drew against. A dirty widget whose size reads an axis on which some
reader up its chain gave what it read a box other than the one it asked in is
not drawn locally: the chain is marked and the parent of the highest such
placement draws, since above it every box is a constraint rather than an
answer. The walk goes up the whole reader chain because a pass-through hands a
derived box down unchanged.

`Scroll` read its box's length for the clamp through `px_len`, which records
the reported size as depending on it, and it does not: its size is its
content's. That made every scroll tick a size question asked in a derived box,
at 34x the instructions. `Painter::px_len_for_draw` is the read that records
nothing. Instructions per frame on the depth-8 rig against the previous head:
`many` at 32 dirty 0.66M to 0.74M, at 130 dirty 27.7M to 26.5M, `resize` 15.8M
to 15.0M, `scroll`, `repaint` and `size` unchanged. The shrinking fuzzer passes
200 trees at depth 7 in all four cases, the hundred-seed sweep passes, and the
five reference renders and the resize render are byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 02:16:56 -04:00
iris-aiandClaude Opus 5 65f68bbb8a Reorder a span's children in the fuzzer, and find two fixed points
The shrinking fuzzer had no case for what `generated.rs` calls a reshuffle,
which was the only thing still failing there. `Case::Reorder` rotates every
span's children after a warm frame and compares against a tree grown that
way -- which needs a span's creation order kept apart from the order its
children are attached in, or the two trees make the same widgets in
different orders and cannot be lined up.

It found a four-widget tree, from 486, and the trace says the layout has
more than one answer rather than one answer reached twice.

    Aligned(mid, -, Span[ Text(wrap), OneLine ])

A span measures its children in its own box. Its own box is what its parent
gave it, from the size it reported, from those children. So with the
wrapping text second it is offered `cursor..end` of a span 663.376 wide and
asked for 357.44, which is what it already holds -- the size is valid, the
span reports 663.376 again, and nothing moves. Grown in that order from
scratch the span is offered the window, the text is asked for 334.06 and
answers 318.45, and the span settles at 624.38. Both are stable. Which one
you get depends on what the tree was before.

So this is not a stale drawing kept too long, and no rule about when to
keep one will fix it: it is a circular dependency with two solutions.
`Painter::settle` in `Aligned` -- place into the child's own size without
measuring there -- makes all four cases in `unsettled.rs` pass and breaks
two in `generated.rs`, whether or not the child is drawn first. Not kept;
the shape of the fix is the constraint a container measures under being
something it is given rather than something it ends up with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 01:38:26 -04:00
iris-aiandClaude Opus 5 99131940ab Answer a break from the one in hand wherever it is still the same break
A parent that sizes to a child offers it back the length it just reported,
so a wrapping text was re-broken at exactly its own longest line. That is a
knife edge: the length is composed back through the box chain, so it lands
an ulp either side of where it started, and which side decides whether the
longest line still fits. One side kept three lines at 167.41, the other
took four at 163.49 -- from the same text in the same box, differing only
in what the output size had been.

A greedy break does not need recomputing there. Breaking at one width gives
lines that each fit, none of which could have taken another word; at any
narrower width down to the longest of them, every line still fits and none
can take a word that did not fit in more room. So one break answers a whole
interval, and the cache now hits across it rather than on the exact width.

The tolerance is what makes it hold at the edge, which is the case that
matters: sub-pixel, so no break it admits is one anybody could see.

The generated sweep passes at depth 6, where it failed; the shrinking
fuzzer agrees over 800 trees at depth 7 on all three scenarios, where two
of them failed. `tests/unsettled.rs` is green, so the whole suite is.
Depth 7 of the generated sweep still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 01:29:24 -04:00
iris-aiandClaude Opus 5 e5a3e640d4 Add the second shrunk case, and a trace rig for what box a text is drawn in
Six widgets from 905, and it fails in 0.06s: everything inside a declared
189x176 box is the same size whatever the output is, so a resize may not
reach any of it, and the text still comes out 3.92px narrower warm than
cold.

`tests/trace_unsettled.rs` says why, and it is not what it looked like. A
span measures a content-sized child in the space remaining, is told 167.41,
and then offers that back as the child's box -- so the text is re-broken at
exactly its own longest line, which is a knife edge: warm lands on four
lines and 163.49, cold stays on three and 167.41. Measuring an answer
against itself is unstable precisely at the fixed point.

`Painter::settle` -- move the child's slot, keep the drawing, never measure
again -- is the shape of the fix and does not work yet. In a span it breaks
five cases, because a container child may have laid its own children out as
fractions of the box it drew in, so moving it into a shorter one shrinks
them; reporting a length in pixels does not mean the drawing is positioned
in pixels. In `Aligned` alone it breaks two. Recorded rather than kept: the
condition wants to be something a widget declares, near `OnResize`, rather
than something its caller infers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 01:10:45 -04:00
iris-aiandClaude Opus 5 c596bf12c6 Measure a child in the length its parent declared, not the box it was offered
`SetSize` drew its child in whatever box it had been given and then
reported its declared length, so the child answered about a box it was
never going to have -- and the answer on the *other* axis was taken under
that. A wrapping text under `SetSize(x: 76px)` was measured in the whole
640 available, reported one line, and the parent sized itself to one line.
The text was then drawn again at 76 and reported two, but by then its box
was settled and nothing revisited it. A repaint put it right, which is why
the first frame and the second disagreed.

So the layout was not a function of the state, and "cold" was not a fixed
point -- which means the warm-against-cold oracle has been measuring
against a tree that had not settled, and some of what it reported as a
retained-layout defect was the cold side being wrong. Nothing about
retained state is involved in this: it reproduces in six widgets on a
first frame.

The declared length is what the child gets, so that is where it is
measured. `apply_rest` carries `rel` and `rest` through unchanged, and a
`px` length composes as an offset, so the child's box does not move again
when this widget's own box shrinks to what it declared.

`tests/unsettled.rs` passes, and the generated sweep now passes at depth 5
where it failed. Depth 6 and 7 still fail; there is more than one of these.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 01:01:57 -04:00
iris-aiandClaude Opus 5 f0c7df06ac Let the unsettled-layout tests fail
Ignoring is for cost, not for status: a fuzzer earns it, a known defect
does not. Hiding this one behind an attribute turns a loud failure into a
quiet one nobody goes looking for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:52:07 -04:00
iris-aiandClaude Opus 5 b7caab3b9e Grow trees that can be taken apart, and find that a first frame is wrong
Reconstructing a generated failure by hand had failed three times: a seed
reproduces a tree of hundreds of widgets, and the printed chain is not
enough to see which part matters. `tests/shrink.rs` grows trees from a
description it can simplify -- drop a child, unwrap a wrapper, shorten a
text, drop a declared length -- and takes the first simplification that
still fails until none does. It lives in the tests; nothing in the library
knows about it.

It works: with the box-length check in `try_reuse` deliberately disabled
it reduced a 96-widget tree to 2. That check is worth keeping, because a
fuzzer that cannot fail is a fuzzer that agrees with everything.

What it found is not what any of this was looking for. Six widgets, shrunk
from 402:

    Span[ Stack[ Text("Wrapping"), Aligned(pos,pos,
          SetSize(x: 76px, Text("Wrapping shapes", wrap))) ] ]

The wrapping text is one line on the first frame and two after a repaint,
and two is right for a 76px box -- so the *cold* tree is the one that has
not settled. `generated.rs` has been comparing a warm frame against a cold
one and calling the difference a retained-layout defect, while at least
some of it is the first frame shaping a text at a width it was measured in
rather than the one it was given. Retained state is not involved.

`tests/unsettled.rs` is that case by hand, in 0.06s. Both of its tests
fail, so both are ignored with the reason rather than left to break the
build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:49:00 -04:00
iris-aiandClaude Opus 5 386a0d1b8f Steer the fuzzer, and print enough of a failure to rebuild it by hand
`DEPTH` was a constant at 4, and the generator widens two to four ways per
level, so raising it buys overlap between dependency paths rather than
ancestry. `IRIS_GENERATED_DEPTH` and `IRIS_GENERATED_SEEDS` select the
load; the default is what it was.

Depth 4 was hiding divergences. At depth 5 and beyond the sweep fails on
the tree as it stands, with no `Branch` node and every span filling across
its axis, so it is neither of the things I suspected -- it predates both.

A failure printed a chain of type names, which is not enough to write the
tree out again, and hand-reconstruction from one has failed three times
now. `describe` prints what each ancestor was configured with, so a run
says `Text < SetSize{x:34 px;} < Aligned{x:neg,y:pos} < SetSize{x:35 px;}
< Stack{n:2}` and the fast test that replaces the seed can be built from
that. `Widget: Any`, so this needs no new plumbing.

Two fixtures assumed every tree grows a declared size to change, and one
assumed a span it shuffles is drawn -- a span behind a branch nobody took
is not. Both are vacuous seeds rather than failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:27:41 -04:00
iris-aiandClaude Opus 5 1b1378b05a Branch on a measurement, so a wrong one shows as a different tree
Comparing boxes catches a widget that moved. It does not catch one that
measured a child, was handed an answer a cold start would not have given,
and took the other branch -- the same defect, arriving where a pixel
comparison cannot see it. Branching on what the painter tells you is
something a widget is allowed to do, so the library owes the same answer
warm and cold; only a widget changing its own configuration is exempt.

`random::Branch` measures a child and draws one of two others on the
result, with both grown either way so the ids match whichever is drawn.
It joins the generator, which makes every existing scenario a control-flow
oracle as well as a geometric one. `tests/determinism.rs` is the same
widget by hand across eight thresholds, including either side of the
answer, and is the fast check -- the sweep is a fuzzer and confirms at the
end rather than being iterated against.

A span behind a branch nobody took is not drawn, so shuffling it cannot
move anything; `reshuffled` now treats that as vacuous, the way it already
treats a tree with no spans, rather than as a shuffle that had no effect.

Both new tests pass, and the sweep passes at depth 4 and 5 over 200 seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:06:54 -04:00
iris-aiandClaude Opus 5 60175c3821 Check that measuring a text and giving it that width is a fixed point
A span that sizes to its children measures one, is told a length, and
hands that length back -- so whether measurement is idempotent decides
whether the two chase each other. Nothing checked it.

It holds: a wrapping text in a `Dir::RIGHT` span, which is the wrap axis
and the span's own axis together, stays at 881.84 across six repaints
that change nothing. So the narrowing recorded against LAYOUT.md §4 is
not something text does on its own, and looking for the cause there is
looking in the wrong place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:51:29 -04:00
iris-aiandClaude Opus 5 b165164e59 Carry a span's rest weight up instead of collapsing it to one share
A span reporting `Len::default()` whenever a child had a share threw away
how many shares it was holding, so each level of nesting re-divided a
share rather than dividing the same space. One span of a rect beside a
span of three gave 1/2 and 1/6 each, where the same four rects directly
in one span get a quarter.

A span that sizes from its children does not resolve `rest`, it passes
the weight up; resolution belongs at the nearest ancestor with a length,
and since the output became a box there is always one. The placement loop
already divides by `len.rest / total.rest`, so it consumes carried
weights unchanged -- only what the span reported was wrong.

The uneven nesting is the case that fails without this; the even one
passes either way and is here as the statement of intent.

Decided by the owner, 2026-09-14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:29:25 -04:00
iris-aiandClaude Opus 5 ef815dadfd Let OnResize answer for the window too, and delete the second rule
A resize had its own mechanism: `reads_output` recorded that a widget had
looked at the output, `update` scanned every active widget for one whose
`output_px` had moved, marked it and its whole reader chain, and
`resize_marks` kept those marks from counting as content dirtiness --
while `on_resize` answered the same question for every other box. Two
answers to "does this drawing survive its box changing length", and the
one that applied to the window ignored what the widget had declared.

With the output held as the root of the chain there is one question. A
resize offers the root widget its box again, `try_reuse` answers per axis
from `on_resize`, and `redraws_under` prices the subtree. Gone with it:
`reads_output`, `resized`, `resize_marks`, the scan, the eager reader
marking, and the shallowest-first branch in `redraw_updates`, which only
existed because resize marking worked differently -- the settle loop now
has one order.

Two things this needed. An unslotted widget may be reused when only its
parent's box changed length: it has nothing of its own to write, and what
it drew is a fraction of that box, so the slot above it already carries
the change. And `root_readers` holds the widgets whose size came from the
output rather than their own box -- `MaxSize` -- since no box of theirs
need have changed; they are marked per axis, from a set kept as they draw
rather than by scanning.

`a_resize_does_not_redraw_what_the_shader_can_move` now says `Scale`,
which is what it was always describing, and `a_resize_redraws_what_does
_not_scale` is its other half. `ReadsWidth` declares `Scale` across the
axis it does not read, so per-axis precision comes from the widget rather
than from which output axis it happened to touch.

Resize phase, seed 1 depth 8: 6.45M instructions per frame to 5.84M.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:50:06 -04:00
iris-aiandClaude Opus 5 9f4311774b Hold the output as the box every chain bottoms out in
A position was composed up the slot chain to a normalized region and then
multiplied by the output's size, so the window was the one box in the
system that was not a box. Seeding the chain with a root slot holding it
in pixels makes composing through it leave everything below in pixels,
which is what the multiplication was doing.

`within` already does the arithmetic: a child at `rel` 1 inside a span of
`px` 0 to `px` 1920 composes to `px` 1920 and `rel` 0, so the trailing
`to_px` becomes the identity rather than a step. The shader walks the
same chain and needs no change for the same reason.

This is the shape the resize machinery wants before it can be deleted: a
resize becomes one slot written, which `try_reuse` and `redraws_under`
already carry. Nothing is removed yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:32:53 -04:00
iris-aiandClaude Opus 5 7c50a3e51b Rename a length's abs component to px
`dp` is coming, and then `abs` says which of the two it is not. The
component has always been a pixel count, so the name that admits it is
the one that leaves room for a second unit beside it.

Mechanical: the field on `Len` and `UiScalar`, their constructors,
`to_abs`/`get_abs`, the matching WGSL struct member and the locals
composing it. Field order and types are unchanged, so the `Pod` layout
the shader reads is the same bytes. `f32::abs` is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:29:20 -04:00
30 changed files with 1712 additions and 253 deletions

No files matched your search

+2 -2
View File
@@ -144,10 +144,10 @@ impl UiScalar {
pub const fn align(&self, align: AxisAlign) -> UiSpan { pub const fn align(&self, align: AxisAlign) -> UiSpan {
let rel = align.rel(); let rel = align.rel();
let mut start = UiScalar::rel(rel); let mut start = UiScalar::rel(rel);
start.abs -= self.abs * rel; start.px -= self.px * rel;
start.rel -= self.rel * rel; start.rel -= self.rel * rel;
let mut end = UiScalar::rel(rel); let mut end = UiScalar::rel(rel);
end.abs += self.abs * (1.0 - rel); end.px += self.px * (1.0 - rel);
end.rel += self.rel * (1.0 - rel); end.rel += self.rel * (1.0 - rel);
UiSpan { start, end } UiSpan { start, end }
} }
+20 -20
View File
@@ -9,14 +9,14 @@ pub struct Size {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Len { pub struct Len {
pub abs: f32, pub px: f32,
pub rel: f32, pub rel: f32,
pub rest: f32, pub rest: f32,
} }
impl<N: UiNum> From<N> for Len { impl<N: UiNum> From<N> for Len {
fn from(value: N) -> Self { fn from(value: N) -> Self {
Len::abs(value.to_f32()) Len::px(value.to_f32())
} }
} }
@@ -46,10 +46,10 @@ impl Size {
y: Len::REST, y: Len::REST,
}; };
pub fn abs(v: Vec2) -> Self { pub fn px(v: Vec2) -> Self {
Self { Self {
x: Len::abs(v.x), x: Len::px(v.x),
y: Len::abs(v.y), y: Len::px(v.y),
} }
} }
@@ -97,13 +97,13 @@ impl Size {
impl Len { impl Len {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
abs: 0.0, px: 0.0,
rel: 0.0, rel: 0.0,
rest: 0.0, rest: 0.0,
}; };
pub const REST: Self = Self { pub const REST: Self = Self {
abs: 0.0, px: 0.0,
rel: 0.0, rel: 0.0,
rest: 1.0, rest: 1.0,
}; };
@@ -111,27 +111,27 @@ impl Len {
pub fn apply_rest(&self) -> UiScalar { pub fn apply_rest(&self) -> UiScalar {
UiScalar { UiScalar {
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 }, rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
abs: self.abs, px: self.px,
} }
} }
pub fn abs(abs: impl UiNum) -> Self { pub fn px(px: impl UiNum) -> Self {
Self { Self {
abs: abs.to_f32(), px: px.to_f32(),
rel: 0.0, rel: 0.0,
rest: 0.0, rest: 0.0,
} }
} }
pub fn rel(rel: impl UiNum) -> Self { pub fn rel(rel: impl UiNum) -> Self {
Self { Self {
abs: 0.0, px: 0.0,
rel: rel.to_f32(), rel: rel.to_f32(),
rest: 0.0, rest: 0.0,
} }
} }
pub fn rest(ratio: impl UiNum) -> Self { pub fn rest(ratio: impl UiNum) -> Self {
Self { Self {
abs: 0.0, px: 0.0,
rel: 0.0, rel: 0.0,
rest: ratio.to_f32(), rest: ratio.to_f32(),
} }
@@ -141,31 +141,31 @@ impl Len {
pub mod len_fns { pub mod len_fns {
use super::*; use super::*;
pub fn abs(abs: impl UiNum) -> Len { pub fn px(px: impl UiNum) -> Len {
Len { Len {
abs: abs.to_f32(), px: px.to_f32(),
rel: 0.0, rel: 0.0,
rest: 0.0, rest: 0.0,
} }
} }
pub fn rel(rel: impl UiNum) -> Len { pub fn rel(rel: impl UiNum) -> Len {
Len { Len {
abs: 0.0, px: 0.0,
rel: rel.to_f32(), rel: rel.to_f32(),
rest: 0.0, rest: 0.0,
} }
} }
pub fn rest(ratio: impl UiNum) -> Len { pub fn rest(ratio: impl UiNum) -> Len {
Len { Len {
abs: 0.0, px: 0.0,
rel: 0.0, rel: 0.0,
rest: ratio.to_f32(), rest: ratio.to_f32(),
} }
} }
} }
impl_op!(Len Add add; abs rel rest); impl_op!(Len Add add; px rel rest);
impl_op!(Len Sub sub; abs rel rest); impl_op!(Len Sub sub; px rel rest);
impl_op!(Size Add add; x y); impl_op!(Size Add add; x y);
impl_op!(Size Sub sub; x y); impl_op!(Size Sub sub; x y);
@@ -184,8 +184,8 @@ impl std::fmt::Display for Size {
impl std::fmt::Display for Len { impl std::fmt::Display for Len {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.abs != 0.0 { if self.px != 0.0 {
write!(f, "{} abs;", self.abs)?; write!(f, "{} px;", self.px)?;
} }
if self.rel != 0.0 { if self.rel != 0.0 {
write!(f, "{} rel;", self.rel)?; write!(f, "{} rel;", self.rel)?;
+38 -38
View File
@@ -23,11 +23,11 @@ impl UiVec2 {
Self { x, y } Self { x, y }
} }
pub const fn abs(abs: impl const Into<Vec2>) -> Self { pub const fn px(px: impl const Into<Vec2>) -> Self {
let abs = abs.into(); let px = px.into();
Self { Self {
x: UiScalar::abs(abs.x), x: UiScalar::px(px.x),
y: UiScalar::abs(abs.y), y: UiScalar::px(px.y),
} }
} }
@@ -70,10 +70,10 @@ impl UiVec2 {
} }
} }
pub fn to_abs(&self, rel: Vec2) -> Vec2 { pub fn to_px(&self, rel: Vec2) -> Vec2 {
Vec2 { Vec2 {
x: self.x.to_abs(rel.x), x: self.x.to_px(rel.x),
y: self.y.to_abs(rel.y), y: self.y.to_px(rel.y),
} }
} }
@@ -92,8 +92,8 @@ impl UiVec2 {
} }
} }
pub fn get_abs(&self) -> Vec2 { pub fn get_px(&self) -> Vec2 {
(self.x.abs, self.y.abs).into() (self.x.px, self.y.px).into()
} }
pub fn get_rel(&self) -> Vec2 { pub fn get_rel(&self) -> Vec2 {
@@ -102,15 +102,15 @@ impl UiVec2 {
pub fn abs_mut(&mut self) -> Vec2View<'_> { pub fn abs_mut(&mut self) -> Vec2View<'_> {
Vec2View { Vec2View {
x: &mut self.x.abs, x: &mut self.x.px,
y: &mut self.y.abs, y: &mut self.y.px,
} }
} }
} }
impl Display for UiVec2 { impl Display for UiVec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rel{};abs{}", self.get_rel(), self.get_abs()) write!(f, "rel{};px{}", self.get_rel(), self.get_px())
} }
} }
@@ -118,8 +118,8 @@ impl_op!(UiVec2 Add add; x y);
impl_op!(UiVec2 Sub sub; x y); impl_op!(UiVec2 Sub sub; x y);
const impl From<Vec2> for UiVec2 { const impl From<Vec2> for UiVec2 {
fn from(abs: Vec2) -> Self { fn from(px: Vec2) -> Self {
Self::abs(abs) Self::px(px)
} }
} }
@@ -127,8 +127,8 @@ const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
fn from(abs: (T, U)) -> Self { fn from(px: (T, U)) -> Self {
Self::abs(abs) Self::px(px)
} }
} }
@@ -136,34 +136,34 @@ where
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
pub struct UiScalar { pub struct UiScalar {
pub rel: f32, pub rel: f32,
pub abs: f32, pub px: f32,
} }
impl Eq for UiScalar {} impl Eq for UiScalar {}
impl Hash for UiScalar { impl Hash for UiScalar {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u32(self.rel.to_bits()); state.write_u32(self.rel.to_bits());
state.write_u32(self.abs.to_bits()); state.write_u32(self.px.to_bits());
} }
} }
impl_op!(UiScalar Add add; rel abs); impl_op!(UiScalar Add add; rel px);
impl_op!(UiScalar Sub sub; rel abs); impl_op!(UiScalar Sub sub; rel px);
impl UiScalar { impl UiScalar {
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 }; pub const ZERO: Self = Self { rel: 0.0, px: 0.0 };
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 }; pub const FULL: Self = Self { rel: 1.0, px: 0.0 };
pub const fn new(rel: f32, abs: f32) -> Self { pub const fn new(rel: f32, px: f32) -> Self {
Self { rel, abs } Self { rel, px }
} }
pub const fn rel(rel: f32) -> Self { pub const fn rel(rel: f32) -> Self {
Self { rel, abs: 0.0 } Self { rel, px: 0.0 }
} }
pub const fn abs(abs: f32) -> Self { pub const fn px(px: f32) -> Self {
Self { rel: 0.0, abs } Self { rel: 0.0, px }
} }
pub const fn rel_min() -> Self { pub const fn rel_min() -> Self {
@@ -177,28 +177,28 @@ impl UiScalar {
pub const fn max(&self, other: Self) -> Self { pub const fn max(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.max(other.rel), rel: self.rel.max(other.rel),
abs: self.abs.max(other.abs), px: self.px.max(other.px),
} }
} }
pub const fn min(&self, other: Self) -> Self { pub const fn min(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.min(other.rel), rel: self.rel.min(other.rel),
abs: self.abs.min(other.abs), px: self.px.min(other.px),
} }
} }
pub const fn offset(mut self, amt: f32) -> Self { pub const fn offset(mut self, amt: f32) -> Self {
self.abs += amt; self.px += amt;
self self
} }
pub const fn within(&self, span: &UiSpan) -> Self { pub const fn within(&self, span: &UiSpan) -> Self {
let anchor = self.rel.lerp(span.start.rel, span.end.rel); let anchor = self.rel.lerp(span.start.rel, span.end.rel);
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs); let offset = self.px + self.rel.lerp(span.start.px, span.end.px);
Self { Self {
rel: anchor, rel: anchor,
abs: offset, px: offset,
} }
} }
@@ -215,15 +215,15 @@ impl UiScalar {
pub const fn flip(&mut self) { pub const fn flip(&mut self) {
self.rel = 1.0 - self.rel; self.rel = 1.0 - self.rel;
self.abs = -self.abs; self.px = -self.px;
} }
pub const fn to(&self, end: Self) -> UiSpan { pub const fn to(&self, end: Self) -> UiSpan {
UiSpan { start: *self, end } UiSpan { start: *self, end }
} }
pub const fn to_abs(&self, rel: f32) -> f32 { pub const fn to_px(&self, rel: f32) -> f32 {
self.rel * rel + self.abs self.rel * rel + self.px
} }
} }
@@ -255,7 +255,7 @@ impl UiSpan {
self.start.flip(); self.start.flip();
self.end.flip(); self.end.flip();
std::mem::swap(&mut self.start.rel, &mut self.end.rel); std::mem::swap(&mut self.start.rel, &mut self.end.rel);
std::mem::swap(&mut self.start.abs, &mut self.end.abs); std::mem::swap(&mut self.start.px, &mut self.end.px);
} }
pub const fn shift(&mut self, offset: UiScalar) { pub const fn shift(&mut self, offset: UiScalar) {
@@ -338,8 +338,8 @@ impl UiRegion {
pub fn to_px(&self, size: Vec2) -> PixelRegion { pub fn to_px(&self, size: Vec2) -> PixelRegion {
PixelRegion { PixelRegion {
top_left: self.top_left().get_rel() * size + self.top_left().get_abs(), top_left: self.top_left().get_rel() * size + self.top_left().get_px(),
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(), bot_right: self.bot_right().get_rel() * size + self.bot_right().get_px(),
} }
} }
+24
View File
@@ -106,6 +106,13 @@ impl Default for TextAttrs {
} }
} }
/// How far below the longest line a width may fall and still be answered by
/// the break in hand. A parent that offers a child the length it reported
/// composes that length back through the box chain, so the two differ in the
/// last bits -- and at exactly the longest line, that decides whether a line
/// fits. Sub-pixel, so no break it admits is one a reader could see.
const BREAK_EPSILON_PX: f32 = 0.05;
/// Keeps text and its corresponding layout from getting out of sync. /// Keeps text and its corresponding layout from getting out of sync.
pub struct TextBuffer { pub struct TextBuffer {
text: String, text: String,
@@ -189,6 +196,23 @@ impl TextBuffer {
diag::bump(Counter::TextShapeHits); diag::bump(Counter::TextShapeHits);
return; return;
} }
// A greedy break at one width is the same break at every width down
// to the longest line it produced: each line still fits, and none can
// take a word that would not fit in the wider box. So the layout in
// hand already answers, and re-breaking would only be a chance to
// disagree with itself -- which is what happens when a parent offers
// a child the length that child just reported, and the two land
// either side of a float.
if let Some(key) = &self.layout_key
&& key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at
&& want + BREAK_EPSILON_PX >= self.layout.width()
{
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
return;
}
let same_shaping = self let same_shaping = self
.layout_key .layout_key
.as_ref() .as_ref()
+37 -1
View File
@@ -22,6 +22,10 @@ pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
fn module_source(wgsl: &str) -> String {
format!("{PRELUDE}\n{wgsl}")
}
pub struct UiRenderNode { pub struct UiRenderNode {
shared_layout: BindGroupLayout, shared_layout: BindGroupLayout,
shared_group: BindGroup, shared_group: BindGroup,
@@ -222,7 +226,7 @@ impl UiRenderNode {
) -> RenderPipeline { ) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor { let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label), label: Some(label),
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()), source: ShaderSource::Wgsl(module_source(wgsl).into()),
}); });
device.create_render_pipeline(&RenderPipelineDescriptor { device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label), label: Some(label),
@@ -401,3 +405,35 @@ impl ListBuffers {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::module_source;
use wgpu::naga::{
front::wgsl,
valid::{Capabilities, ValidationFlags, Validator},
};
/// Every shader file, composed as the renderer composes it, parses and
/// validates with no device -- so an edit that breaks one fails here and
/// not in the first window opened.
#[test]
fn every_shader_validates() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
let mut checked = 0;
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
continue;
}
let source = module_source(&std::fs::read_to_string(&path).unwrap());
let module = wgsl::parse_str(&source)
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
Validator::new(ValidationFlags::all(), Capabilities::all())
.validate(&module)
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
checked += 1;
}
assert!(checked > 0, "no shaders found in {dir}");
}
}
+10 -10
View File
@@ -40,7 +40,7 @@ const CHAIN_LIMIT: u32 = 64u;
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar { fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
return UiScalar( return UiScalar(
mix(p.start.rel, p.end.rel, s.rel), mix(p.start.rel, p.end.rel, s.rel),
s.abs + mix(p.start.abs, p.end.abs, s.rel), s.px + mix(p.start.px, p.end.px, s.rel),
); );
} }
@@ -69,7 +69,7 @@ struct UiSpan {
struct UiScalar { struct UiScalar {
rel: f32, rel: f32,
abs: f32, px: f32,
} }
struct InstanceInput { struct InstanceInput {
@@ -104,12 +104,12 @@ fn vs_main(
); );
let r = resolve_move(in.move_idx, local); let r = resolve_move(in.move_idx, local);
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel); let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
let top_left_abs = vec2(r.x.start.abs, r.y.start.abs); let top_left_px = vec2(r.x.start.px, r.y.start.px);
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel); let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_abs = vec2(r.x.end.abs, r.y.end.abs); let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs); let top_left = floor(top_left_rel * window.dim) + floor(top_left_px);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs); let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_px);
let size = bot_right - top_left; let size = bot_right - top_left;
let uv = vec2<f32>( let uv = vec2<f32>(
@@ -136,12 +136,12 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
// clips content that moves inside it. // clips content that moves inside it.
let m = resolve_move(mask.move_idx, Region(mask.x, mask.y)); let m = resolve_move(mask.move_idx, Region(mask.x, mask.y));
let tl = vec2(m.x.start.rel, m.y.start.rel); let tl = vec2(m.x.start.rel, m.y.start.rel);
let tl_abs = vec2(m.x.start.abs, m.y.start.abs); let tl_px = vec2(m.x.start.px, m.y.start.px);
let br = vec2(m.x.end.rel, m.y.end.rel); let br = vec2(m.x.end.rel, m.y.end.rel);
let br_abs = vec2(m.x.end.abs, m.y.end.abs); let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = floor(tl * window.dim) + floor(tl_abs); let top_left = floor(tl * window.dim) + floor(tl_px);
let bot_right = floor(br * window.dim) + floor(br_abs); let bot_right = floor(br * window.dim) + floor(br_px);
let pos = in.clip_position.xy; let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0; return color * 0.0;
+4 -2
View File
@@ -13,6 +13,10 @@ pub struct ActiveData {
/// it is a fraction of a slot's box, and the same fraction of a box that /// it is a fraction of a slot's box, and the same fraction of a box that
/// has since changed is a different number of pixels. /// has since changed is a different number of pixels.
pub px: Vec2, pub px: Vec2,
/// The pixel size of the box its parent first asked about it in, before
/// knowing what it came to. `px` may be a box derived from that answer,
/// and a size measured there is only the same answer asked again.
pub offered_px: Vec2,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a /// How far down the tree it was drawn, the root being 1. Carried down a
/// draw rather than worked out by walking up, so it is right for every /// draw rather than worked out by walking up, so it is right for every
@@ -31,8 +35,6 @@ pub struct ActiveData {
pub size_output_inputs: [bool; 2], pub size_output_inputs: [bool; 2],
/// The output dimensions against which those dependencies were observed. /// The output dimensions against which those dependencies were observed.
pub output_px: Vec2, pub output_px: Vec2,
/// Output axes it read directly or while resolving its offered box.
pub reads_output: [bool; 2],
/// The slot its primitives are positioned through: its own if its parent /// The slot its primitives are positioned through: its own if its parent
/// placed it, otherwise the nearest ancestor that has one. /// placed it, otherwise the nearest ancestor that has one.
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
+33 -11
View File
@@ -21,12 +21,14 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked
/// about is the one recorded as its offer.
pub(super) offered: 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>,
/// Offered pixel axes which can affect the size this draw reports. /// Offered pixel axes which can affect the size this draw reports.
pub(super) size_box_inputs: [bool; 2], pub(super) size_box_inputs: [bool; 2],
pub(super) size_output_inputs: [bool; 2], pub(super) size_output_inputs: [bool; 2],
pub(super) reads_output: [bool; 2],
/// The slot this widget's primitives are positioned through: its own if /// The slot this widget's primitives are positioned through: its own if
/// its parent placed it, otherwise the nearest ancestor that has one. /// its parent placed it, otherwise the nearest ancestor that has one.
pub(super) move_idx: MoveIdx, pub(super) move_idx: MoveIdx,
@@ -142,6 +144,7 @@ impl<'a> Painter<'a> {
None, None,
self.rsc, self.rsc,
); );
self.offer(id.id(), region);
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
@@ -183,6 +186,8 @@ impl<'a> Painter<'a> {
axis: Axis, axis: Axis,
region: UiRegion, region: UiRegion,
) -> Option<Len> { ) -> Option<Len> {
let region = region.within(&self.region);
self.offer(child.id(), region);
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return Some(hint);
} }
@@ -190,12 +195,12 @@ impl<'a> Painter<'a> {
.map(|size| size.axis(axis)) .map(|size| size.axis(axis))
} }
/// `region` in this widget's own coordinates.
fn retained_size<W: ?Sized>( fn retained_size<W: ?Sized>(
&mut self, &mut self,
child: &StrongWidget<W>, child: &StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> Option<Size> { ) -> Option<Size> {
let region = region.within(&self.region);
let (size, box_inputs, output_inputs) = let (size, box_inputs, output_inputs) =
self.state self.state
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?; .retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
@@ -205,6 +210,20 @@ impl<'a> Painter<'a> {
Some(size) Some(size)
} }
/// Records the box a child was first asked about in this draw. Any later
/// box this draw gives it was decided knowing its answer, so a size the
/// child measures there is not an answer to this widget's question.
fn offer(&mut self, child: WidgetId, region: UiRegion) {
if self.offered.contains(&child) {
return;
}
self.offered.push(child);
let px = self.state.px_of(self.move_idx, region);
if let Some(active) = self.state.active.get_mut(&child) {
active.offered_px = px;
}
}
/// Depends on a length the child gave without being drawn. A hint is /// Depends on a length the child gave without being drawn. A hint is
/// context-free, so this depends on the child but on no pixel axis. /// context-free, so this depends on the child but on no pixel axis.
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
@@ -260,9 +279,9 @@ impl<'a> Painter<'a> {
let mut region = origin; let mut region = origin;
region.x.end = region.x.start; region.x.end = region.x.start;
region.y.end = region.y.start; region.y.end = region.y.start;
let mut region = region.offset(UiVec2::abs(glyph.offset)); let mut region = region.offset(UiVec2::px(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32); region.x.end = region.x.start + UiScalar::px(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32); region.y.end = region.y.start + UiScalar::px(glyph.entry.height as f32);
self.write( self.write(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
@@ -286,7 +305,6 @@ impl<'a> Painter<'a> {
/// The output's size in pixels. A widget that reads it draws again when /// The output's size in pixels. A widget that reads it draws again when
/// the output changes, since nothing else can put that right. /// the output changes, since nothing else can put that right.
pub fn output_size(&mut self) -> Vec2 { pub fn output_size(&mut self) -> Vec2 {
self.reads_output = [true; 2];
self.size_output_inputs = [true; 2]; self.size_output_inputs = [true; 2];
self.state.output_size self.state.output_size
} }
@@ -294,7 +312,6 @@ impl<'a> Painter<'a> {
/// One axis of the output in pixels. Prefer this to [`Self::output_size`] /// One axis of the output in pixels. Prefer this to [`Self::output_size`]
/// when the other axis cannot affect the size this widget reports. /// when the other axis cannot affect the size this widget reports.
pub fn output_len(&mut self, axis: Axis) -> f32 { pub fn output_len(&mut self, axis: Axis) -> f32 {
self.reads_output[axis as usize] = true;
self.size_output_inputs[axis as usize] = true; self.size_output_inputs[axis as usize] = true;
self.state.output_size.axis(axis) self.state.output_size.axis(axis)
} }
@@ -303,22 +320,27 @@ impl<'a> Painter<'a> {
/// the boxes it sits within, so a widget that reads it draws again when /// the boxes it sits within, so a widget that reads it draws again when
/// the output changes. /// the output changes.
pub fn px_size(&mut self) -> Vec2 { pub fn px_size(&mut self) -> Vec2 {
self.reads_output = [true; 2];
self.size_box_inputs = [true; 2]; self.size_box_inputs = [true; 2];
let region = self.state.moves.resolve(self.move_idx, self.region); let region = self.state.moves.resolve(self.move_idx, self.region);
region.size().to_abs(self.state.output_size) region.size().to_px(self.state.output_size)
} }
/// One axis of this widget's box in pixels. Prefer this to /// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the reported size. /// [`Self::px_size`] when the other axis cannot affect the reported size.
pub fn px_len(&mut self, axis: Axis) -> f32 { pub fn px_len(&mut self, axis: Axis) -> f32 {
self.reads_output[axis as usize] = true;
self.size_box_inputs[axis as usize] = true; self.size_box_inputs[axis as usize] = true;
self.px_len_for_draw(axis)
}
/// One axis of this widget's box in pixels, for a draw whose reported
/// size does not follow from it -- a clamp or a position. Nothing records
/// the read, so a size that does depend on it would go stale.
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
let region = self.state.moves.resolve(self.move_idx, self.region); let region = self.state.moves.resolve(self.move_idx, self.region);
region region
.size() .size()
.axis(axis) .axis(axis)
.to_abs(self.state.output_size.axis(axis)) .to_px(self.state.output_size.axis(axis))
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
+163 -89
View File
@@ -2,7 +2,7 @@
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, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2}, util::{HashMap, HashSet, Vec2},
}; };
@@ -19,14 +19,14 @@ pub struct UiRenderState {
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
resized: [bool; 2], /// The slot every chain bottoms out in, holding the output as a box.
root_move: MoveIdx,
/// Widgets whose reported size depends on the root box rather than on
/// their own, so nothing below them changing length can reach them.
root_readers: HashSet<WidgetId>,
/// Content/state dirtiness whose retained size cannot answer a layout /// Content/state dirtiness whose retained size cannot answer a layout
/// question until that widget has drawn again. /// question until that widget has drawn again.
invalid_sizes: HashSet<WidgetId>, invalid_sizes: HashSet<WidgetId>,
/// Marks introduced only to traverse resize dependency paths. Unlike
/// content dirtiness, these may retain an answer whose observed pixel
/// axes did not change.
resize_marks: HashSet<WidgetId>,
/// What has already been drawn during the pass under way, so a widget /// What has already been drawn during the pass under way, so a widget
/// reached by redrawing an ancestor is not drawn again on its own /// reached by redrawing an ancestor is not drawn again on its own
/// account. Emptied when the pass ends. /// account. Emptied when the pass ends.
@@ -44,21 +44,48 @@ impl UiRenderState {
layers: Default::default(), layers: Default::default(),
output_size: Vec2::ZERO, output_size: Vec2::ZERO,
old_root: None, old_root: None,
resized: [false; 2],
invalid_sizes: Default::default(), invalid_sizes: Default::default(),
resize_marks: Default::default(),
draw_started: Default::default(), draw_started: Default::default(),
slots: Default::default(), slots: Default::default(),
moves: Default::default(), moves: Default::default(),
root_move: MoveIdx::NONE,
root_readers: Default::default(),
}
}
/// The window as a box, so a chain bottoms out in one rather than in a
/// multiplication applied after it. Composing through a box held in
/// pixels leaves everything below it in pixels, which is why nothing
/// downstream has to know the output's size to resolve a position.
fn write_root(&mut self) {
let region = UiRegion::new(
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)),
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)),
);
match self.root_move == MoveIdx::NONE {
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
false => self.moves.set(self.root_move, region),
} }
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
let size = size.into(); self.output_size = size.into();
for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) { self.write_root();
*resized |= size.axis(axis) != self.output_size.axis(axis);
} }
self.output_size = size;
/// Which axes of the root widget's box are no longer the ones the root
/// slot holds, which is all a resize now is: one slot written, found by
/// the same comparison every other box change is found by.
fn root_axes_changed(&self) -> [bool; 2] {
let Some(active) = self.old_root.and_then(|root| self.active.get(&root)) else {
return [false; 2];
};
let px = self.px_of(active.parent_move, active.region);
let mut changed = [false; 2];
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
*c = pixel_len_changed(active.px.axis(axis), px.axis(axis));
}
changed
} }
pub fn output_size(&self) -> Vec2 { pub fn output_size(&self) -> Vec2 {
@@ -73,7 +100,6 @@ impl UiRenderState {
self.invalid_sizes.clear(); self.invalid_sizes.clear();
self.invalid_sizes self.invalid_sizes
.extend(rsc.widgets().needs_redraw.iter().copied()); .extend(rsc.widgets().needs_redraw.iter().copied());
self.resize_marks.clear();
// safety mechanism for memory leaks; might wanna return a result instead so user can // safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not // decide whether to panic or not
if !rsc.widgets().waiting.is_empty() { if !rsc.widgets().waiting.is_empty() {
@@ -94,53 +120,52 @@ impl UiRenderState {
if self.root_changed(root) { if self.root_changed(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
} else if self.resized.iter().any(|&resized| resized) { } else if self.root_axes_changed().iter().any(|&c| c) {
// A region is a fraction of the output plus an offset, resolved // Every box is a part of the root box, so writing it is a box
// against the window in the shader, so a resize moves the whole // that changed length like any other. Offering the root widget
// drawing on its own. Only a widget that read pixels can be wrong. // its box again puts that through `try_reuse`, which answers per
{ // axis and lets `redraws_under` price the subtree -- rather than
// marking it, which would redraw it whichever axis moved. What
// that cannot reach is a widget whose size came from the root box
// instead of its own, since its own box need not have changed.
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _marking = diag::timer(TimerKind::ResizeMarking); let _marking = diag::timer(TimerKind::ResizeMarking);
let dependents: Vec<_> = self let changed = self.root_axes_changed();
for id in self.root_readers.clone() {
let reads = self
.active .active
.iter() .get(&id)
.filter_map(|(&id, active)| { .map_or([false; 2], |active| active.size_output_inputs);
AXES.into_iter() if !AXES
.zip(self.resized) .into_iter()
.any(|(axis, changed)| { .zip(changed)
changed .any(|(axis, c)| c && reads[axis as usize])
&& active.reads_output[axis as usize] {
&& pixel_len_changed( continue;
active.output_px.axis(axis), }
self.output_size.axis(axis),
)
})
.then_some(id)
})
.collect();
for id in dependents {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeDependents); diag::bump(Counter::ResizeDependents);
rsc.widgets_mut().needs_redraw.insert(id); rsc.widgets_mut().needs_redraw.insert(id);
if let Some(top) = self.mark_readers(id, rsc) {
rsc.widgets_mut().needs_redraw.insert(top);
} }
} if let Some(root) = root {
self.resize_marks.extend( self.draw_inner(
rsc.widgets() 0,
.needs_redraw root.id(),
.iter() UiRegion::FULL,
.filter(|id| !self.invalid_sizes.contains(id)) None,
.copied(), 1,
self.root_move,
false,
MaskIdx::NONE,
None,
rsc,
); );
} }
} }
if rsc.widgets().has_updates() { if rsc.widgets().has_updates() {
self.redraw_updates(rsc); self.redraw_updates(rsc);
} }
self.resized = [false; 2];
self.invalid_sizes.clear(); self.invalid_sizes.clear();
self.resize_marks.clear();
self.draw_started.clear(); self.draw_started.clear();
} }
@@ -149,6 +174,7 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout); let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc); self.clear(rsc);
// free all resources & cache // free all resources & cache
self.write_root();
if let Some(id) = root { if let Some(id) = root {
self.draw_inner( self.draw_inner(
0, 0,
@@ -156,7 +182,7 @@ impl UiRenderState {
UiRegion::FULL, UiRegion::FULL,
None, None,
1, 1,
MoveIdx::NONE, self.root_move,
false, false,
MaskIdx::NONE, MaskIdx::NONE,
None, None,
@@ -177,7 +203,7 @@ impl UiRenderState {
parent_move: MoveIdx, parent_move: MoveIdx,
slotted: bool, slotted: bool,
mask: MaskIdx, mask: MaskIdx,
old_children: Option<Vec<WidgetId>>, mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Size { ) -> Size {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -185,14 +211,12 @@ impl UiRenderState {
diag::bump(Counter::DrawRequests); diag::bump(Counter::DrawRequests);
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted); diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
} }
let mut old_children = old_children.unwrap_or_default();
if self.active.contains_key(&id) { if self.active.contains_key(&id) {
if let Some(size) = self.try_reuse(id, region, depth, parent_move, rsc) { if let Some(size) = self.try_reuse(id, region, depth, parent_move, rsc) {
return size; return size;
} }
// if not, then maintain resize and track old children to remove unneeded // if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, rsc).unwrap(); old = self.remove(id, false, rsc);
old_children = active.children;
} }
// draw widget // draw widget
@@ -206,6 +230,12 @@ impl UiRenderState {
} }
}; };
let px = self.px_of(move_idx, local); let px = self.px_of(move_idx, local);
// Drawn again in a box its parent already decided: the offer is the
// one recorded when the parent first asked, not this box.
let (old_children, offered_px) = match old {
Some(old) => (old.children, old.offered_px),
None => (Vec::new(), px),
};
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id); self.draw_started.insert(id);
@@ -218,11 +248,11 @@ impl UiRenderState {
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
children: Vec::new(), children: Vec::new(),
offered: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
depth, depth,
size_box_inputs: [false; 2], size_box_inputs: [false; 2],
size_output_inputs: [false; 2], size_output_inputs: [false; 2],
reads_output: [false; 2],
move_idx, move_idx,
rsc, rsc,
}; };
@@ -246,10 +276,10 @@ impl UiRenderState {
textures, textures,
primitives, primitives,
children, children,
offered: _,
size_deps, size_deps,
size_box_inputs, size_box_inputs,
size_output_inputs, size_output_inputs,
reads_output,
move_idx, move_idx,
layer, layer,
depth: _, depth: _,
@@ -268,6 +298,7 @@ impl UiRenderState {
region, region,
size, size,
px, px,
offered_px,
parent, parent,
depth, depth,
textures, textures,
@@ -277,7 +308,6 @@ impl UiRenderState {
size_box_inputs, size_box_inputs,
size_output_inputs, size_output_inputs,
output_px: self.output_size, output_px: self.output_size,
reads_output,
move_idx, move_idx,
parent_move, parent_move,
mask, mask,
@@ -290,10 +320,13 @@ impl UiRenderState {
} }
} }
match active.size_output_inputs.iter().any(|&reads| reads) {
true => self.root_readers.insert(id),
false => self.root_readers.remove(&id),
};
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, active); self.active.insert(id, active);
self.invalid_sizes.remove(&id); self.invalid_sizes.remove(&id);
self.resize_marks.remove(&id);
size size
} }
@@ -321,11 +354,11 @@ impl UiRenderState {
} }
/// The pixel size of a region held in `slot`'s coordinates. /// The pixel size of a region held in `slot`'s coordinates.
fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 { pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
self.moves self.moves
.resolve(slot, region) .resolve(slot, region)
.size() .size()
.to_abs(self.output_size) .to_px(self.output_size)
} }
/// A clean widget's retained size, when the offered pixel axes which /// A clean widget's retained size, when the offered pixel axes which
@@ -370,8 +403,7 @@ impl UiRenderState {
} }
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool { fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
self.invalid_sizes.contains(&id) self.invalid_sizes.contains(&id) || widgets.needs_redraw.contains(&id)
|| (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id))
} }
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool { fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
@@ -438,10 +470,14 @@ impl UiRenderState {
self.keep_depth(id, depth); self.keep_depth(id, depth);
return Some(size); return Some(size);
} }
// Only a placed widget can be given a different box without drawing // Only a placed widget can be given a different *region* without
// again: everything it drew is a fraction of its slot's box, so one // drawing again: it has an entry of its own to say where it went,
// entry says where all of it went. // where an unslotted one shares its parent's and has nothing to
if slot == parent_move { // write. Its parent's box changing length is not that -- everything
// it drew is a fraction of that box, so the slot already above it
// carries the change and `on_resize` below decides whether the
// drawing survives it.
if slot == parent_move && old_region != region {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::ReuseUnslotted); diag::bump(Counter::ReuseUnslotted);
@@ -475,7 +511,9 @@ impl UiRenderState {
return None; return None;
} }
} }
if slot != parent_move {
self.moves.set(slot, region); self.moves.set(slot, region);
}
self.keep_depth(id, depth); self.keep_depth(id, depth);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.region = region; active.region = region;
@@ -589,9 +627,9 @@ impl UiRenderState {
} }
self.slots.clear(); self.slots.clear();
self.moves.clear(); self.moves.clear();
self.root_move = MoveIdx::NONE;
self.layers.clear(); self.layers.clear();
self.invalid_sizes.clear(); self.invalid_sizes.clear();
self.resize_marks.clear();
self.draw_started.clear(); self.draw_started.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
rsc.free(); rsc.free();
@@ -610,10 +648,7 @@ impl UiRenderState {
// reader and gives each changing box its final constraints first. // reader and gives each changing box its final constraints first.
while let Some(id) = { while let Some(id) = {
let dirty = rsc.widgets().needs_redraw.iter().copied(); let dirty = rsc.widgets().needs_redraw.iter().copied();
match self.resized.iter().any(|&resized| resized) { dirty.max_by_key(|&id| self.depth(id))
true => dirty.min_by_key(|&id| self.depth(id)),
false => dirty.max_by_key(|&id| self.depth(id)),
}
} { } {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops); diag::bump(Counter::QueuePops);
@@ -664,7 +699,7 @@ impl UiRenderState {
widgets: &Widgets, widgets: &Widgets,
) -> bool { ) -> bool {
self.root_changed(root) self.root_changed(root)
|| self.resized.iter().any(|&resized| resized) || self.root_axes_changed().iter().any(|&c| c)
|| widgets.has_updates() || widgets.has_updates()
} }
@@ -702,24 +737,31 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn) /// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
self.draw_started.remove(&id); self.draw_started.remove(&id);
if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) { if rsc.widgets().needs_redraw.contains(&id) {
self.invalid_sizes.insert(id); self.invalid_sizes.insert(id);
} }
// A widget can only answer whether its size changed by drawing in the // A widget can only answer whether its size changed by drawing in the
// box its parent chose. If that box changed in pixels, its retained // box its parent chose. If that box changed in pixels, its retained
// placement is stale and the highest size reader must choose the new // placement is stale and the highest size reader must choose the new
// box first. Otherwise the widget can draw locally, and its readers // box first. The same holds when the box was decided from the
// only matter if the returned size actually changed. // widget's own answer: measuring there again can only repeat it,
// whatever the content now says. Otherwise the widget can draw
// locally, and its readers only matter if the returned size actually
// changed.
let box_changed = self.active.get(&id).is_some_and(|active| { let box_changed = self.active.get(&id).is_some_and(|active| {
let px = self.px_of(active.parent_move, active.region); let px = self.px_of(active.parent_move, active.region);
AXES.into_iter() AXES.into_iter()
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis))) .any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
}); });
if (self.resized.iter().any(|&resized| resized) || box_changed) let top = match box_changed {
&& let Some(top) = self.mark_readers(id, rsc) true => self.top_reader(id),
{ false => None,
}
.or_else(|| self.derived_box_reader(id));
if let Some(top) = top {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::EagerReaderRedraws); diag::bump(Counter::EagerReaderRedraws);
self.mark_below(id, top, rsc);
self.redraw(top, rsc); self.redraw(top, rsc);
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
return; return;
@@ -746,7 +788,7 @@ impl UiRenderState {
active.parent_move, active.parent_move,
active.move_idx != active.parent_move, active.move_idx != active.parent_move,
active.mask, active.mask,
Some(active.children), Some(active),
rsc, rsc,
); );
@@ -769,24 +811,56 @@ impl UiRenderState {
} }
} }
/// The furthest ancestor that read this widget's size, directly or through /// The highest reader up the chain that gave what it read a box other
/// widgets that did the same, marking everything below it on the way. /// than the one it asked in, on an axis this widget's size reads. Above
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> { /// it every box is a constraint rather than an answer. It is the highest
/// and not the nearest because a pass-through hands a derived box down
/// unchanged.
fn derived_box_reader(&self, id: WidgetId) -> Option<WidgetId> {
let reads = self.active.get(&id)?.size_box_inputs;
let mut top = None; let mut top = None;
let mut at = id; for (active, parent) in self.reader_chain(id) {
while let Some(active) = self.active.get(&at) let px = self.px_of(active.parent_move, active.region);
&& let Some(parent) = active.parent if AXES.into_iter().zip(reads).any(|(axis, r)| {
&& self r && pixel_len_changed(active.offered_px.axis(axis), px.axis(axis))
.active }) {
.get(&parent)
.is_some_and(|p| p.size_deps.contains(&at))
{
rsc.widgets_mut().needs_redraw.insert(at);
top = Some(parent); top = Some(parent);
at = parent; }
} }
top top
} }
/// The furthest ancestor that read this widget's size, directly or through
/// widgets that did the same.
fn top_reader(&self, id: WidgetId) -> Option<WidgetId> {
self.reader_chain(id).last().map(|(_, parent)| parent)
}
/// Each widget from `id` upward whose parent read its size, with that
/// parent.
fn reader_chain(&self, id: WidgetId) -> impl Iterator<Item = (&ActiveData, WidgetId)> {
let mut at = Some(id);
std::iter::from_fn(move || {
let active = self.active.get(&at?)?;
let parent = active.parent?;
let read = self.active.get(&parent)?.size_deps.contains(&active.id);
at = read.then_some(parent);
read.then_some((active, parent))
})
}
/// Marks everything from `id` up to, and not including, `top`, so that
/// drawing `top` draws each of them rather than reusing it.
fn mark_below(&self, id: WidgetId, top: WidgetId, rsc: &mut dyn UiRsc) {
let mut at = id;
while at != top {
rsc.widgets_mut().needs_redraw.insert(at);
let Some(parent) = self.active.get(&at).and_then(|active| active.parent) else {
return;
};
at = parent;
}
}
} }
impl Default for UiRenderState { impl Default for UiRenderState {
+47 -1
View File
@@ -84,6 +84,35 @@ pub struct Tree {
pub detached: Vec<StrongWidget>, pub detached: Vec<StrongWidget>,
} }
/// Branches on a child's measured length. Comparing boxes catches a widget
/// that moved; this catches one that believed a measurement a cold start
/// would not have given it, by turning that into a different tree. Its own
/// configuration never changes, so which side draws is a property of the
/// layout alone.
pub struct Branch {
pub probe: StrongWidget,
pub wide: StrongWidget,
pub narrow: StrongWidget,
pub threshold: f32,
}
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(40.0);
let measured = painter.place(&self.probe, top).len(Axis::X);
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
let mut rest = UiRegion::FULL;
rest.y.start = rest.y.start.offset(40.0);
match px > self.threshold {
true => painter.place(&self.wide, rest),
false => painter.place(&self.narrow, rest),
};
Size::REST
}
}
pub struct Spanned { pub struct Spanned {
pub id: WeakWidget<Span>, pub id: WeakWidget<Span>,
/// Leaves grown with the span whether or not they end up in it, so both /// Leaves grown with the span whether or not they end up in it, so both
@@ -142,7 +171,7 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
fn len(&mut self) -> Option<Len> { fn len(&mut self) -> Option<Len> {
match self.rng.below(4) { match self.rng.below(4) {
0 => Some(Len::abs(20.0 + self.rng.below(180) as f32)), 0 => Some(Len::px(20.0 + self.rng.below(180) as f32)),
1 => Some(Len::REST), 1 => Some(Len::REST),
_ => None, _ => None,
} }
@@ -199,6 +228,23 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
self.tree.ids.push(id.id()); self.tree.ids.push(id.id());
return id.add_strong(self.rsc); return id.add_strong(self.rsc);
} }
if positioned == 2 {
// Both sides are grown either way, so a tree that draws one has
// the same ids as a tree that draws the other.
let probe = self.node(depth - 1);
let wide = self.node(depth - 1);
let narrow = self.node(depth - 1);
let threshold = self.rng.below(500) as f32;
let id = Branch {
probe,
wide,
narrow,
threshold,
}
.add(self.rsc);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
if positioned == 1 { if positioned == 1 {
let inner = self.node(depth - 1); let inner = self.node(depth - 1);
let inner = self.sized(inner); let inner = self.sized(inner);
+2 -2
View File
@@ -8,11 +8,11 @@ pub struct Image {
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(&self.handle); painter.primitive(&self.handle);
Size::abs(self.handle.size()) Size::px(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(Len::abs(self.handle.size().axis(axis))) Some(Len::px(self.handle.size().axis(axis)))
} }
fn on_resize(&self, _: Axis) -> OnResize { fn on_resize(&self, _: Axis) -> OnResize {
+1 -1
View File
@@ -19,7 +19,7 @@ impl Widget for MaxSize {
fn capped(len: Len, max: Option<Len>, output: f32) -> Len { fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
match max { match max {
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max, Some(max) if len.apply_rest().to_px(output) > max.apply_rest().to_px(output) => max,
_ => len, _ => len,
} }
} }
+6 -6
View File
@@ -12,11 +12,11 @@ impl Widget for Pad {
.size(); .size();
Size { Size {
x: Len { x: Len {
abs: inner.x.abs + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
..inner.x ..inner.x
}, },
y: Len { y: Len {
abs: inner.y.abs + self.padding.top + self.padding.bottom, px: inner.y.px + self.padding.top + self.padding.bottom,
..inner.y ..inner.y
}, },
} }
@@ -55,10 +55,10 @@ impl Padding {
} }
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
let mut region = UiRegion::FULL; let mut region = UiRegion::FULL;
region.x.start.abs += self.left; region.x.start.px += self.left;
region.y.start.abs += self.top; region.y.start.px += self.top;
region.x.end.abs -= self.right; region.x.end.px -= self.right;
region.y.end.abs -= self.bottom; region.y.end.px -= self.bottom;
region region
} }
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
+4 -3
View File
@@ -12,7 +12,8 @@ pub struct Scroll {
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let output_len = painter.output_len(self.axis); let output_len = painter.output_len(self.axis);
let container_len = UiScalar::abs(painter.px_len(self.axis)); // Its size is its content's, whatever box that is scrolled within.
let container_len = UiScalar::px(painter.px_len_for_draw(self.axis));
// Draw in the whole container only when its scrolling-axis length is // Draw in the whole container only when its scrolling-axis length is
// not already known, then place it at the scrolled offset. // not already known, then place it at the scrolled offset.
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL); let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
@@ -22,8 +23,8 @@ impl Widget for Scroll {
.unwrap_or_else(|| child.unwrap().axis(self.axis)) .unwrap_or_else(|| child.unwrap().axis(self.axis))
.apply_rest() .apply_rest()
.within_len(container_len) .within_len(container_len)
.to_abs(output_len); .to_px(output_len);
self.container_len = container_len.to_abs(output_len); self.container_len = container_len.to_px(output_len);
self.content_len = content_len; self.content_len = content_len;
if self.snap_end { if self.snap_end {
+14 -1
View File
@@ -8,7 +8,20 @@ pub struct SetSize {
impl Widget for SetSize { impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size(); // A declared length is what the child gets, whatever box this widget
// was offered before its parent knew that. Measuring it anywhere else
// asks about a box it will not have, and the answer on the other axis
// is taken under that: a wrapping text measured in the whole width
// reports one line, and nothing revisits it once the real width
// arrives.
let mut region = UiRegion::FULL;
for (axis, len) in [(Axis::X, self.x), (Axis::Y, self.y)] {
if let Some(len) = len {
let span = region.axis_mut(axis);
span.end = span.start + len.apply_rest();
}
}
let child = painter.widget_within(&self.inner, region).size();
Size { Size {
x: self.x.unwrap_or(child.x), x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y), y: self.y.unwrap_or(child.y),
+14 -10
View File
@@ -24,13 +24,13 @@ impl Widget for Span {
Some(len) => len, Some(len) => len,
None => painter.place(child, region).len(axis), None => painter.place(child, region).len(axis),
}; };
cursor.abs += len.abs + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
lens.push(len); lens.push(len);
} }
let gap = self.gap * self.children.len().saturating_sub(1) as f32; let gap = self.gap * self.children.len().saturating_sub(1) as f32;
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len); let total = lens.iter().fold(Len::px(gap), |sum, len| sum + *len);
let mut start = UiScalar::rel_min(); let mut start = UiScalar::rel_min();
let mut ortho = Len::ZERO; let mut ortho = Len::ZERO;
@@ -38,12 +38,12 @@ impl Widget for Span {
let mut span = UiSpan::FULL; let mut span = UiSpan::FULL;
span.start = start; span.start = start;
if len.rest > 0.0 { if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs); let offset = UiScalar::new(total.rel, total.px);
let rel_end = UiScalar::rel(len.rest / total.rest); let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + start) - offset; let end = (UiScalar::rel_max() + start) - offset;
start = rel_end.within(&start.to(end)); start = rel_end.within(&start.to(end));
} }
start.abs += len.abs; start.px += len.px;
start.rel += len.rel; start.rel += len.rel;
span.end = start; span.end = start;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
@@ -55,15 +55,19 @@ impl Widget for Span {
if used.rel > 0.0 || used.rest > 0.0 { if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST; ortho = Len::REST;
} else if ortho.rest == 0.0 { } else if ortho.rest == 0.0 {
ortho.abs = ortho.abs.max(used.abs); ortho.px = ortho.px.max(used.px);
} }
start.abs += self.gap; start.px += self.gap;
} }
let along = match total.rest == 0.0 && total.rel == 0.0 { // Carried whole rather than collapsed to one share: a span that sizes
true => total, // from its children does not resolve `rest`, it passes the weight up,
false => Len::default(), // so nesting spans divides the same space rather than re-dividing a
}; // share of it. Four `rest(1)` children under two spans under one span
// get a quarter each, which collapsing to `rest(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one.
let along = total;
Size::from_axis(axis, along, ortho) Size::from_axis(axis, along, ortho)
} }
+1 -1
View File
@@ -280,7 +280,7 @@ impl<'a> TextEditCtx<'a> {
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos - self.text.region().top_left().to_abs(size); let pos = pos - self.text.region().top_left().to_px(size);
let prev_sel = self.text.selection; let prev_sel = self.text.selection;
let prev_hit = self.text.double_hit; let prev_hit = self.text.double_hit;
+1 -1
View File
@@ -72,7 +72,7 @@ impl TextView {
let tex = self.render(painter); let tex = self.render(painter);
let region = tex.size.align(align); let region = tex.size.align(align);
let size = Size::abs(tex.size); let size = Size::px(tex.size);
let within = region.within(&painter.region()); let within = region.within(&painter.region());
painter.glyphs(tex, within); painter.glyphs(tex, within);
(region, size) (region, size)
+1 -1
View File
@@ -80,7 +80,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
slot = render.moves.push(slot, UiRegion::FULL); slot = render.moves.push(slot, UiRegion::FULL);
} }
let px = |v: f32| UiScalar { rel: 0.0, abs: v }; let px = |v: f32| UiScalar { rel: 0.0, px: v };
for i in 0..INSTANCES { for i in 0..INSTANCES {
let x = (i % (SIZE as usize / 2)) as f32 * 2.0; let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
let y = (i / (SIZE as usize / 2)) as f32; let y = (i / (SIZE as usize / 2)) as f32;
+101
View File
@@ -0,0 +1,101 @@
//! A measurement that decides control flow.
//!
//! Comparing boxes catches a widget that moved. It does not catch a widget
//! that measured a child, believed a different answer from the one a cold
//! start would give, and took the other branch -- which is the same defect
//! arriving somewhere it cannot be ignored. A widget here branches on what it
//! measured, so a disagreement shows up as a different tree.
use iris::harness::Harness;
use iris::prelude::*;
/// Measures `probe` across `axis` and draws one of two children on the
/// answer. Its own configuration never changes, so which child is drawn is a
/// property of the layout alone.
struct BranchesOnMeasurement {
probe: StrongWidget,
wide: StrongWidget,
narrow: StrongWidget,
threshold: f32,
}
impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(40.0);
let measured = painter.place(&self.probe, top).len(Axis::X);
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
let mut rest = UiRegion::FULL;
rest.y.start = rest.y.start.offset(40.0);
match px > self.threshold {
true => painter.place(&self.wide, rest),
false => painter.place(&self.narrow, rest),
};
Size::REST
}
}
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
let words = "the quick brown fox jumps over the lazy dog and keeps running";
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let wide = rect(Color::RED).add(&mut h.rsc);
let narrow = rect(Color::BLUE).add(&mut h.rsc);
let branch = BranchesOnMeasurement {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold,
}
.add(&mut h.rsc);
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
h.set_root((side, branch).span(Dir::RIGHT));
(wide.id(), narrow.id())
}
/// Which of the two branches drew, as a pair a test can compare.
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
(h.region(&wide).is_some(), h.region(&narrow).is_some())
}
#[test]
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut h = Harness::new((900, 600));
let (wide, narrow) = plant(&mut h, threshold);
let first = taken(&h, wide, narrow);
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
for frame in 0..4 {
h.rsc.widgets_mut().get_dyn_mut(wide);
h.rsc.widgets_mut().get_dyn_mut(narrow);
h.frame();
assert_eq!(
taken(&h, wide, narrow),
first,
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
);
}
}
}
#[test]
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut warm = Harness::new((900, 600));
let (wide, narrow) = plant(&mut warm, threshold);
warm.resize((640, 480));
warm.frame();
warm.rsc.widgets_mut().get_dyn_mut(wide);
warm.frame();
let mut cold = Harness::new((640, 480));
let (cwide, cnarrow) = plant(&mut cold, threshold);
assert_eq!(
taken(&warm, wide, narrow),
taken(&cold, cwide, cnarrow),
"threshold {threshold}: warm and cold took different branches"
);
}
}
+87 -27
View File
@@ -16,7 +16,20 @@ use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow}; use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
const DEPTH: usize = 4; /// 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
/// reachable by raising this -- it buys more overlap between dependency
/// paths, not more ancestry.
fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4)
}
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)
}
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98]; const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
const REGION_EPSILON_PX: f32 = 0.05; const REGION_EPSILON_PX: f32 = 0.05;
@@ -38,7 +51,7 @@ fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
} }
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree { fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits); let (root, tree) = grow(&mut h.rsc, seed, depth(), edits);
h.state.root = Some(root); h.state.root = Some(root);
h.frame(); h.frame();
tree tree
@@ -46,8 +59,8 @@ fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens { fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
let lens = [ let lens = [
Some(Len::abs(20.0 + rng.below(180) as f32)), Some(Len::px(20.0 + rng.below(180) as f32)),
Some(Len::abs(20.0 + rng.below(180) as f32)), Some(Len::px(20.0 + rng.below(180) as f32)),
]; ];
let sized = &mut h.rsc[tree.sized[idx]]; let sized = &mut h.rsc[tree.sized[idx]];
sized.x = lens[0]; sized.x = lens[0];
@@ -158,6 +171,53 @@ fn reshuffle(
(edits, detached) (edits, detached)
} }
/// What a widget was configured with, so a tree the generator found can be
/// written out by hand. A fuzz failure is a lead; the fast test that replaces
/// it has to be buildable from what the failure printed.
fn describe(id: WidgetId, h: &Harness) -> String {
let label = h.rsc.widgets().label(id).to_string();
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
return label;
};
let any: &dyn std::any::Any = widget;
let len = |l: &Option<Len>| match l {
Some(l) => format!("{l}"),
None => "-".into(),
};
if let Some(w) = any.downcast_ref::<SetSize>() {
return format!("SetSize{{x:{},y:{}}}", len(&w.x), len(&w.y));
}
if let Some(w) = any.downcast_ref::<Span>() {
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
return format!(
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
w.dir.axis,
w.gap,
w.children.len()
);
}
if let Some(w) = any.downcast_ref::<Pad>() {
let p = &w.padding;
return format!(
"Pad{{l:{},r:{},t:{},b:{}}}",
p.left, p.right, p.top, p.bottom
);
}
if let Some(w) = any.downcast_ref::<Aligned>() {
let a = |v: Option<AxisAlign>| match v {
None => "-",
Some(AxisAlign::Neg) => "neg",
Some(AxisAlign::Center) => "mid",
Some(AxisAlign::Pos) => "pos",
};
return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y));
}
if let Some(w) = any.downcast_ref::<Stack>() {
return format!("Stack{{n:{}}}", w.children.len());
}
label
}
/// Every widget in one tree against the matching widget in the other. A /// Every widget in one tree against the matching widget in the other. A
/// mismatch prints the widget's ancestry, marking the ones that own a slot, /// mismatch prints the widget's ancestry, marking the ones that own a slot,
/// since where two trees disagree is rarely where the cause is. /// since where two trees disagree is rarely where the cause is.
@@ -186,7 +246,7 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness,
true => "", true => "",
false => "*", false => "*",
}; };
chain.push(format!("{}{slot}", wh.rsc.widgets().label(id))); chain.push(format!("{}{slot}", describe(id, wh)));
at = active.parent; at = active.parent;
} }
println!( println!(
@@ -202,6 +262,10 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness,
fn changed_size(seed: u64) { fn changed_size(seed: u64) {
let mut warm = Harness::new((900, 1200)); let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default()); let grown = plant(&mut warm, seed, &Edits::default());
// Not every tree grows a declared size to change.
if grown.sized.is_empty() {
return;
}
let mut rng = Rng::new(seed ^ 0x5eed); let mut rng = Rng::new(seed ^ 0x5eed);
let sizes = edit(&mut warm, &grown, &mut rng); let sizes = edit(&mut warm, &grown, &mut rng);
@@ -224,8 +288,15 @@ fn reshuffled(seed: u64, shuffle: Shuffle) {
let mut warm = Harness::new((900, 1200)); let mut warm = Harness::new((900, 1200));
let mut grown = plant(&mut warm, seed, &Edits::default()); let mut grown = plant(&mut warm, seed, &Edits::default());
// Some seeds grow nothing but wrappers, and a shuffle with no span to // Some seeds grow nothing but wrappers, and a shuffle with no span to
// shuffle is not the same thing as one that had no effect. // shuffle is not the same thing as one that had no effect. A span behind
if grown.spans.is_empty() { // a branch nobody took is the same kind of nothing: it is not drawn, so
// shuffling it cannot move anything.
let shuffles = grown
.spans
.iter()
.step_by(3)
.any(|span| warm.region(&span.id.id()).is_some());
if !shuffles {
return; return;
} }
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect(); let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
@@ -313,6 +384,9 @@ fn resized(seed: u64) {
fn resized_then_changed(seed: u64) { fn resized_then_changed(seed: u64) {
let mut warm = Harness::new((1920, 1200)); let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &Edits::default()); let grown = plant(&mut warm, seed, &Edits::default());
if grown.sized.is_empty() {
return;
}
warm.resize((640, 900)); warm.resize((640, 900));
warm.frame(); warm.frame();
@@ -368,25 +442,11 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
} }
} }
/// Reproduces a divergence that predates the position chain: laying a tree out /// The same property over a hundred seeds and every scenario. What it has
/// again does not always land where growing it cold does. /// found so far was never where the trees disagreed: a text measured in a box
/// /// it was not going to get, and a widget re-measured in a box its own answer
/// Every one seen so far is a wrapping text on a span's *own* axis, where the /// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
/// two draws do not agree. The span measures the child in the whole box, the /// small enough to read.
/// child shapes to that width and reports the width it used, the span then
/// places it in exactly that width -- which is a length change, so the child
/// shapes again, and its longest line is shorter than the box it was just
/// given. Each pass narrows it, so where the tree ends up depends on how many
/// passes it has had, and a warm tree has had a different number from a cold
/// one. Layout is supposed to be a function of the state alone.
///
/// A span whose axis is not the wrap axis is stable, which is every real
/// column of text, and why nothing else has run into this.
///
/// 7 of these 90 diverge on `db1751f`, before the chain; 30 do with it, since
/// a placed child reaches the second shaping more often. Both numbers are the
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
/// rather than anywhere in the chain.
#[test] #[test]
#[ignore = "a hundred seeds, rather than the seven the others check"] #[ignore = "a hundred seeds, rather than the seven the others check"]
fn a_long_run_of_seeds_agrees() { fn a_long_run_of_seeds_agrees() {
@@ -394,7 +454,7 @@ fn a_long_run_of_seeds_agrees() {
.ok() .ok()
.and_then(|seed| seed.parse().ok()) .and_then(|seed| seed.parse().ok())
.map(|seed| seed..=seed) .map(|seed| seed..=seed)
.unwrap_or(1..=100); .unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
for seed in seeds { for seed in seeds {
changed_size(seed); changed_size(seed);
changed_every_size(seed); changed_every_size(seed);
+29
View File
@@ -0,0 +1,29 @@
//! Whether measuring a widget and then giving it the length it reported is a
//! fixed point, which is what a span that sizes to its children needs.
use iris::harness::Harness;
use iris::prelude::*;
#[test]
fn a_wrapping_text_in_a_span_settles_on_one_width() {
let mut h = Harness::new((900, 600));
let words = "the quick brown fox jumps over the lazy dog and keeps on running \
until it reaches the end of a rather long line of text";
let t = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((t, filler).span(Dir::RIGHT));
let mut widths = Vec::new();
for _ in 0..6 {
let r = h.region(&t.id()).unwrap();
widths.push(r.bot_right.x - r.top_left.x);
// Redrawing it changes nothing about the state, so nothing may move.
h.rsc.widgets_mut().get_dyn_mut(t.id());
h.frame();
}
println!("widths over six frames: {widths:?}");
assert!(
widths.windows(2).all(|w| w[0] == w[1]),
"a repaint that changed nothing moved it: {widths:?}"
);
}
+53 -6
View File
@@ -54,7 +54,7 @@ fn a_child_drawn_twice_moves_once() {
h.set_root((left, centered).span(Dir::RIGHT)); h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200)); assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::abs(150)); h.rsc[left].x = Some(Len::px(150));
h.frame(); h.frame();
assert_corners!(h, inner, (150, 0), (350, 200)); assert_corners!(h, inner, (150, 0), (350, 200));
@@ -104,7 +104,7 @@ fn a_fixed_box_is_drawn_again_rather_than_stretched() {
h.set_root(stack.align(Align::TOP)); h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100)); assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::abs(250)); h.rsc[leaf].y = Some(Len::px(250));
h.frame(); h.frame();
assert_corners!(h, panel, (0, 0), (400, 250)); assert_corners!(h, panel, (0, 0), (400, 250));
@@ -119,7 +119,7 @@ fn a_moved_subtree_takes_its_children_with_it() {
h.set_root((first, row).span(Dir::DOWN)); h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 50), (390, 70)); assert_corners!(h, inner, (10, 50), (390, 70));
h.rsc[first].y = Some(Len::abs(80)); h.rsc[first].y = Some(Len::px(80));
h.frame(); h.frame();
// The row is the same shape somewhere else, so one slot moved it and // The row is the same shape somewhere else, so one slot moved it and
@@ -140,7 +140,7 @@ fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
assert_corners!(h, fixed, (100, 0), (150, 200)); assert_corners!(h, fixed, (100, 0), (150, 200));
assert_corners!(h, rest, (150, 0), (400, 200)); assert_corners!(h, rest, (150, 0), (400, 200));
h.rsc[bar].x = Some(Len::abs(200)); h.rsc[bar].x = Some(Len::px(200));
h.frame(); h.frame();
// The panel's box is 100 shorter, so the fixed child is the same 50 wide // The panel's box is 100 shorter, so the fixed child is the same 50 wide
@@ -163,7 +163,7 @@ fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
h.set_root((bar, column).span(Dir::RIGHT)); h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30)); assert_corners!(h, inner, (110, 10), (390, 30));
h.rsc[bar].x = Some(Len::abs(200)); h.rsc[bar].x = Some(Len::px(200));
h.frame(); h.frame();
assert_corners!(h, inner, (210, 10), (390, 30)); assert_corners!(h, inner, (210, 10), (390, 30));
@@ -180,5 +180,52 @@ fn only_a_container_that_places_its_children_lengthens_the_chain() {
h.set_root((bar, buried).span(Dir::RIGHT)); h.set_root((bar, buried).span(Dir::RIGHT));
let slot = h.render.active[&leaf.id()].parent_move; let slot = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(slot), 1, "one span above the leaf"); assert_eq!(
h.render.moves.depth(slot),
2,
"the span above the leaf, and the root the window is held in"
);
}
/// A span that sizes from its children passes their `rest` weight up rather
/// than collapsing it to one share, so nesting divides the same space instead
/// of re-dividing a share of it.
#[test]
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
}
}
/// The same space, unevenly nested: weights carried up mean a share is a
/// share of the whole, not of whatever branch a widget happens to sit in.
#[test]
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((one, three).span(Dir::RIGHT));
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
}
} }
+1 -1
View File
@@ -216,7 +216,7 @@ fn layout_cost() {
trace_selected(&tree); trace_selected(&tree);
let sized = tree.sized[0]; let sized = tree.sized[0];
run("size", frames, &mut harness, move |harness, frame| { run("size", frames, &mut harness, move |harness, frame| {
harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0)); harness.rsc[sized].x = Some(Len::px(100.0 + (frame % 2) as f32 * 40.0));
}); });
} }
+1 -1
View File
@@ -37,7 +37,7 @@ fn replacing_rows_every_frame() {
} }
h.set_root(span); h.set_root(span);
for i in 0..FRAMES { for i in 0..FRAMES {
h.rsc[first].y = Some(Len::abs(40.0 + (i % 2) as f32)); h.rsc[first].y = Some(Len::px(40.0 + (i % 2) as f32));
h.frame(); h.frame();
} }
} }
+39 -13
View File
@@ -156,7 +156,7 @@ impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap(); let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL; let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.abs); region.y.end = region.y.start.offset(len.px);
painter.widget_within(&self.inner, region); painter.widget_within(&self.inner, region);
Size::REST Size::REST
} }
@@ -173,7 +173,7 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
h.set_root(parent); h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80)); assert_corners!(h, inner, (0, 0), (400, 80));
h.rsc[inner].y = Some(Len::abs(120)); h.rsc[inner].y = Some(Len::px(120));
h.frame(); h.frame();
assert_corners!(h, inner, (0, 0), (400, 120)); assert_corners!(h, inner, (0, 0), (400, 120));
@@ -187,10 +187,12 @@ struct ReadsOutput {
impl Widget for ReadsOutput { impl Widget for ReadsOutput {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::abs(painter.output_size() / 4.0) Size::px(painter.output_size() / 4.0)
} }
} }
/// Reads the output across one axis only, and says so: its drawing follows
/// a taller box on its own, so only a wider one is worth a draw.
struct ReadsWidth { struct ReadsWidth {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
@@ -198,14 +200,21 @@ struct ReadsWidth {
impl Widget for ReadsWidth { impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::abs((painter.output_len(Axis::X) / 4.0, 20.0).into()) Size::px((painter.output_len(Axis::X) / 4.0, 20.0).into())
}
fn on_resize(&self, axis: Axis) -> OnResize {
match axis {
Axis::X => OnResize::Redraw,
Axis::Y => OnResize::Scale,
}
} }
} }
#[test] #[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() { fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw); let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Scale);
h.set_root(leaf); h.set_root(leaf);
let settled = draws.get(); let settled = draws.get();
@@ -216,11 +225,28 @@ fn a_resize_does_not_redraw_what_the_shader_can_move() {
assert_eq!( assert_eq!(
draws.get(), draws.get(),
settled, settled,
"its box is the same fraction of a different output" "a scaling drawing follows its box, and the output is one"
); );
assert_corners!(h, leaf, (0, 0), (800, 100)); assert_corners!(h, leaf, (0, 0), (800, 100));
} }
/// The output is the root of the box chain, so a resize is a box that changed
/// length and `OnResize` answers for it -- there is not a second rule for the
/// window. A drawing that does not scale is redrawn whichever box moved.
#[test]
fn a_resize_redraws_what_does_not_scale() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1, "its box is a different length");
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test] #[test]
fn a_resize_redraws_what_read_the_output() { fn a_resize_redraws_what_read_the_output() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -287,12 +313,12 @@ fn subpixel_box_changes_accumulate_from_the_last_draw() {
let settled = draws.get(); let settled = draws.get();
for width in [100.02, 100.04, 100.05] { for width in [100.02, 100.04, 100.05] {
h.rsc[first].size.x = Len::abs(width); h.rsc[first].size.x = Len::px(width);
h.frame(); h.frame();
assert_eq!(draws.get(), settled); assert_eq!(draws.get(), settled);
} }
h.rsc[first].size.x = Len::abs(100.06); h.rsc[first].size.x = Len::px(100.06);
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 1); assert_eq!(draws.get(), settled + 1);
} }
@@ -342,13 +368,13 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// Every wrapper up to the outer pad read the size below it, so the outer // Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same // pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf. // size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw); let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), OnResize::Redraw);
let padded = leaf.pad(10).add(&mut h.rsc); let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12)); h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388)); assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::abs((100, 200).into()); h.rsc[leaf].size = Size::px((100, 200).into());
h.frame(); h.frame();
assert_corners!(h, below, (12, 232), (388, 388)); assert_corners!(h, below, (12, 232), (388, 388));
@@ -387,7 +413,7 @@ fn stretching_a_subtree_carries_the_children_in_it() {
let settled = draws.get(); let settled = draws.get();
assert_corners!(h, inner, (0, 40), (400, 400)); assert_corners!(h, inner, (0, 40), (400, 400));
h.rsc[first].y = Some(Len::abs(80)); h.rsc[first].y = Some(Len::px(80));
h.frame(); h.frame();
assert_eq!( assert_eq!(
@@ -411,7 +437,7 @@ fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
h.set_root((bar, row).span(Dir::RIGHT)); h.set_root((bar, row).span(Dir::RIGHT));
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get()); let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
h.rsc[bar].x = Some(Len::abs(200)); h.rsc[bar].x = Some(Len::px(200));
h.frame(); h.frame();
// The span reads every child's size, so redrawing one takes the span // The span reads every child's size, so redrawing one takes the span
@@ -437,7 +463,7 @@ fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
h.set_root((bar, row).span(Dir::RIGHT)); h.set_root((bar, row).span(Dir::RIGHT));
let settled = draws.get(); let settled = draws.get();
h.rsc[bar].x = Some(Len::abs(200)); h.rsc[bar].x = Some(Len::px(200));
h.frame(); h.frame();
assert_eq!(draws.get(), settled, "its own length did not change"); assert_eq!(draws.get(), settled, "its own length did not change");
+1 -5
View File
@@ -94,11 +94,7 @@ fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
let mut col = Span::empty(Dir::DOWN); let mut col = Span::empty(Dir::DOWN);
for _ in 0..rows { for _ in 0..rows {
let mut row = Span::empty(Dir::RIGHT); let mut row = Span::empty(Dir::RIGHT);
row.push( row.push(rect(Color::RED).width(Len::px(40.0)).add_strong(&mut h.rsc));
rect(Color::RED)
.width(Len::abs(40.0))
.add_strong(&mut h.rsc),
);
let mut body = Span::empty(Dir::DOWN); let mut body = Span::empty(Dir::DOWN);
let para = wtext(words(&mut rng, 12, 52)) let para = wtext(words(&mut rng, 12, 52))
.size(16) .size(16)
+525
View File
@@ -0,0 +1,525 @@
//! A property test that shrinks its own counterexample.
//!
//! `generated.rs` reproduces a failure from a seed, but a seed is not a lead
//! anybody can read: the tree is hundreds of widgets, and reconstructing the
//! part that matters by hand has failed every time it has been tried. This
//! grows trees it can take apart, so a failure is reduced to the smallest
//! tree that still shows it and printed as something to write a fast test
//! from.
//!
//! cargo test --release --test shrink -- --ignored --nocapture
//!
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow
//! them, `SHRINK_CASE` which scenario. 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.
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Branch, Rng};
/// The same two leaves `iris::random` grows, since only one of them reads the
/// width it is given and that is the difference that matters.
const WORDS: &[&str] = &[
"Wrapping",
"shapes",
"one",
"source",
"into",
"as",
"many",
"lines",
"as",
"the",
"box",
"leaves",
"room",
"for,",
"so",
"a",
"paragraph's",
"height",
"is",
"an",
"answer",
"and",
"not",
"a",
"setting.",
];
const ONE_LINE: &str = "one line, overflowing whatever it is given";
const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0);
#[derive(Clone, Debug, PartialEq)]
enum Node {
/// Words taken from [`WORDS`], and whether it wraps.
Text(usize, bool),
/// The leaf that overflows whatever box it is given rather than wrapping.
OneLine,
Rect,
/// Direction, gap, children in creation order, and the order they are
/// attached in -- separate so a tree that reorders its children
/// still makes the same widgets in the same order, and two
/// builds line up index for index.
Span(bool, f32, Vec<Node>, Vec<usize>),
Stack(Vec<Node>),
Pad(f32, Box<Node>),
Aligned(u8, u8, Box<Node>),
Sized(Option<Len>, Option<Len>, Box<Node>),
Scroll(bool, Box<Node>),
Branch(Box<Node>, Box<Node>, Box<Node>, f32),
}
fn axis_align(v: u8) -> Option<AxisAlign> {
match v % 4 {
0 => None,
1 => Some(AxisAlign::Neg),
2 => Some(AxisAlign::Center),
_ => Some(AxisAlign::Pos),
}
}
fn dir(down: bool) -> Dir {
if down { Dir::DOWN } else { Dir::RIGHT }
}
impl Node {
/// Builds into `h`, pushing every id in tree order, so two builds of one
/// node line up index for index and their boxes can be compared.
fn build(
&self,
h: &mut Harness,
out: &mut Vec<WidgetId>,
spans: &mut Vec<WeakWidget<Span>>,
) -> StrongWidget {
let id: StrongWidget = match self {
Node::Text(words, wrap) => {
let n = (*words).clamp(1, WORDS.len());
wtext(WORDS[..n].join(" "))
.size(16)
.wrap(*wrap)
.add_strong(&mut h.rsc)
}
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
Node::Span(down, gap, kids, order) => {
let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect();
// `order` is a permutation, so each is taken exactly once.
let children = order
.iter()
.map(|&i| built[i].take().expect("order repeats an index"))
.collect();
let handle = Span {
children,
dir: dir(*down),
gap: *gap,
}
.add(&mut h.rsc);
spans.push(handle);
handle.add_strong(&mut h.rsc)
}
Node::Stack(kids) => {
let children = kids.iter().map(|k| k.build(h, out, spans)).collect();
Stack {
children,
size: StackSize::Child(0),
}
.add_strong(&mut h.rsc)
}
Node::Pad(p, kid) => {
let inner = kid.build(h, out, spans);
Pad {
padding: Padding {
left: *p,
right: *p,
top: *p,
bottom: *p,
},
inner,
}
.add_strong(&mut h.rsc)
}
Node::Aligned(x, y, kid) => {
let inner = kid.build(h, out, spans);
Aligned {
inner,
align: Align {
x: axis_align(*x),
y: axis_align(*y),
},
}
.add_strong(&mut h.rsc)
}
Node::Sized(x, y, kid) => {
let inner = kid.build(h, out, spans);
SetSize {
inner,
x: *x,
y: *y,
}
.add_strong(&mut h.rsc)
}
Node::Scroll(down, kid) => {
let inner = kid.build(h, out, spans);
let axis = if *down { Axis::Y } else { Axis::X };
Scroll::new(inner, axis).add_strong(&mut h.rsc)
}
Node::Branch(probe, a, b, at) => {
let probe = probe.build(h, out, spans);
let wide = a.build(h, out, spans);
let narrow = b.build(h, out, spans);
Branch {
probe,
wide,
narrow,
threshold: *at,
}
.add_strong(&mut h.rsc)
}
};
out.push(id.id());
id
}
fn size(&self) -> usize {
1 + match self {
Node::Text(..) | Node::OneLine | Node::Rect => 0,
Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
Node::Pad(_, k)
| Node::Aligned(_, _, k)
| Node::Sized(_, _, k)
| Node::Scroll(_, k) => k.size(),
Node::Branch(p, a, b, _) => p.size() + a.size() + b.size(),
}
}
/// Every one-step simplification: a wrapper replaced by what it wrapped, a
/// child dropped, a length or a word count reduced. Ordered cheapest-first
/// so the greedy walk takes the biggest bites early.
fn smaller(&self) -> Vec<Node> {
let mut out = Vec::new();
let leaf = Node::Rect;
match self {
Node::Text(words, wrap) => {
if *words > 1 {
out.push(Node::Text(words / 2, *wrap));
out.push(Node::Text(words - 1, *wrap));
}
if *wrap {
out.push(Node::Text(*words, false));
}
out.push(leaf);
}
Node::OneLine => out.push(Node::Rect),
Node::Rect => {}
Node::Span(down, gap, kids, order) => {
out.extend(order.iter().map(|&i| kids[i].clone()));
for i in 0..kids.len() {
if kids.len() > 1 {
let mut less = kids.clone();
less.remove(i);
let order = (0..less.len()).collect();
out.push(Node::Span(*down, *gap, less, order));
}
}
if *gap != 0.0 {
out.push(Node::Span(*down, 0.0, kids.clone(), order.clone()));
}
for (i, kid) in kids.iter().enumerate() {
for small in kid.smaller() {
let mut next = kids.clone();
next[i] = small;
out.push(Node::Span(*down, *gap, next, order.clone()));
}
}
}
Node::Stack(kids) => {
out.extend(kids.iter().cloned());
for i in 0..kids.len() {
if kids.len() > 1 {
let mut less = kids.clone();
less.remove(i);
out.push(Node::Stack(less));
}
}
for (i, kid) in kids.iter().enumerate() {
for small in kid.smaller() {
let mut next = kids.clone();
next[i] = small;
out.push(Node::Stack(next));
}
}
}
Node::Pad(p, kid) => {
out.push((**kid).clone());
if *p != 0.0 {
out.push(Node::Pad(0.0, kid.clone()));
}
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Pad(*p, Box::new(k))),
);
}
Node::Aligned(x, y, kid) => {
out.push((**kid).clone());
for (nx, ny) in [(0, *y), (*x, 0)] {
if (nx, ny) != (*x, *y) {
out.push(Node::Aligned(nx, ny, kid.clone()));
}
}
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Aligned(*x, *y, Box::new(k))),
);
}
Node::Sized(x, y, kid) => {
out.push((**kid).clone());
if x.is_some() {
out.push(Node::Sized(None, *y, kid.clone()));
}
if y.is_some() {
out.push(Node::Sized(*x, None, kid.clone()));
}
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Sized(*x, *y, Box::new(k))),
);
}
Node::Scroll(down, kid) => {
out.push((**kid).clone());
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Scroll(*down, Box::new(k))),
);
}
Node::Branch(p, a, b, at) => {
out.push((**p).clone());
out.push((**a).clone());
out.push((**b).clone());
for small in p.smaller() {
out.push(Node::Branch(Box::new(small), a.clone(), b.clone(), *at));
}
for small in a.smaller() {
out.push(Node::Branch(p.clone(), Box::new(small), b.clone(), *at));
}
for small in b.smaller() {
out.push(Node::Branch(p.clone(), a.clone(), Box::new(small), *at));
}
}
}
out
}
}
/// A declared size over about half the tree, the way `iris::random` puts them
/// in: on the way into every child rather than as a node kind of its own, so
/// readers of a size are dense rather than occasional.
fn sized(rng: &mut Rng, inner: Node) -> Node {
if !rng.chance() {
return inner;
}
let len = |rng: &mut Rng| match rng.below(4) {
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
1 => Some(Len::REST),
_ => None,
};
Node::Sized(len(rng), len(rng), Box::new(inner))
}
fn grow(rng: &mut Rng, depth: usize) -> Node {
if depth == 0 {
return match rng.below(4) {
0 => Node::Text(1 + rng.below(WORDS.len()), true),
1 => Node::OneLine,
_ => Node::Rect,
};
}
let len = |rng: &mut Rng| match rng.below(4) {
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
1 => Some(Len::REST),
2 => Some(Len::rel(0.25 + rng.below(3) as f32 * 0.25)),
_ => None,
};
let kid = |rng: &mut Rng| {
let inner = grow(rng, depth - 1);
sized(rng, inner)
};
match rng.below(8) {
0 => Node::Scroll(rng.chance(), Box::new(kid(rng))),
1 => Node::Aligned(rng.below(4) as u8, rng.below(4) as u8, Box::new(kid(rng))),
2 => Node::Pad(rng.below(24) as f32, Box::new(kid(rng))),
3 => Node::Sized(len(rng), len(rng), Box::new(kid(rng))),
4 => Node::Branch(
Box::new(kid(rng)),
Box::new(kid(rng)),
Box::new(kid(rng)),
rng.below(500) as f32,
),
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
_ => {
let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect();
let order = (0..kids.len()).collect();
Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order)
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum Case {
Resize,
Repaint,
ResizeRepaint,
Reorder,
}
/// Every span's children rotated by one, as a tree rather than as a change:
/// what a warm frame reaches by moving them has to be where growing them that
/// way lands.
fn reordered(node: &Node) -> Node {
match node {
Node::Span(down, gap, kids, order) => {
let kids = kids.iter().map(reordered).collect::<Vec<_>>();
let mut order = order.clone();
order.rotate_left(1);
Node::Span(*down, *gap, kids, order)
}
Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()),
Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))),
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))),
Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))),
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))),
Node::Branch(p, a, b, at) => Node::Branch(
Box::new(reordered(p)),
Box::new(reordered(a)),
Box::new(reordered(b)),
*at,
),
leaf => leaf.clone(),
}
}
/// Runs one scenario warm and cold and says where they disagree.
fn diverges(node: &Node, case: Case) -> Option<String> {
let resizes = matches!(case, Case::Resize | Case::ResizeRepaint);
let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint);
let start = if resizes { OUTER } else { INNER };
let mut warm = Harness::new(start);
let mut warm_ids = Vec::new();
let mut warm_spans = Vec::new();
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans);
warm.state.root = Some(root);
// The frame that makes it warm: without it there is nothing retained and
// the comparison is two cold starts agreeing with each other.
warm.frame();
if resizes {
warm.resize(INNER);
warm.frame();
}
if repaints {
for &id in &warm_ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
}
if case == Case::Reorder {
for span in &warm_spans {
warm.rsc[*span].children.rotate_left(1);
}
warm.frame();
}
// What the warm tree was moved into, grown that way from the start.
let want = match case {
Case::Reorder => reordered(node),
_ => node.clone(),
};
let mut cold = Harness::new(INNER);
let mut cold_ids = Vec::new();
let mut cold_spans = Vec::new();
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans);
cold.state.root = Some(root);
cold.frame();
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
let same = match (got, want) {
(Some(g), Some(c)) => {
let d = |a: f32, b: f32| (a - b).abs() <= 0.05;
d(g.top_left.x, c.top_left.x)
&& d(g.top_left.y, c.top_left.y)
&& d(g.bot_right.x, c.bot_right.x)
&& d(g.bot_right.y, c.bot_right.y)
}
(None, None) => true,
_ => false,
};
if !same {
return Some(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
None
}
/// Takes the first simplification that still fails, until none does.
fn shrink(mut node: Node, case: Case) -> Node {
loop {
let Some(next) = node
.smaller()
.into_iter()
.find(|small| diverges(small, case).is_some())
else {
return node;
};
node = next;
}
}
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)
}
#[test]
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
fn no_grown_tree_lays_out_differently_warm_than_cold() {
let seeds: u64 = env("SHRINK_SEEDS", 400);
let depth: usize = env("SHRINK_DEPTH", 5);
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
"repaint" => Case::Repaint,
"resize-repaint" => Case::ResizeRepaint,
"reorder" => Case::Reorder,
_ => Case::Resize,
};
for seed in 1..=seeds {
let node = grow(&mut Rng::new(seed), depth);
let Some(how) = diverges(&node, case) else {
continue;
};
let small = shrink(node.clone(), case);
println!(
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
node.size(),
small.size()
);
panic!("seed {seed} lays out differently warm than cold");
}
let sizes: Vec<usize> = (1..=seeds)
.map(|seed| grow(&mut Rng::new(seed), depth).size())
.collect();
let total: usize = sizes.iter().sum();
println!(
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
total,
sizes.iter().max().copied().unwrap_or(0)
);
}
+155
View File
@@ -0,0 +1,155 @@
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is
//! actually drawn in on a first frame against a settled one.
#![cfg(feature = "layout-diagnostics")]
use iris::core::layout_diagnostics::{self as diag, TraceEvent};
use iris::harness::Harness;
use iris::prelude::*;
fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
let sized = SetSize {
inner: wrapped.add_strong(&mut h.rsc),
x: Some(Len::px(76.0)),
y: None,
}
.add(&mut h.rsc);
let aligned = Aligned {
inner: sized.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Pos),
y: Some(AxisAlign::Pos),
},
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
}
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
println!("--- {label} ---");
for event in report.traces() {
match event {
TraceEvent::DrawRequest {
id,
region,
pixel_size,
..
} if *id == text => {
println!(
" draw in {:.2}x{:.2} region {region:?}",
pixel_size.x, pixel_size.y
)
}
TraceEvent::SizeReported { id, size } if *id == text => {
println!(" reported {size}")
}
TraceEvent::SizeRead { id, reader, size } if *id == text => {
println!(" size read by {reader:?}: {size}")
}
TraceEvent::Placed { id, parent, region } if *id == text => {
println!(" placed by {parent:?} at {region:?}")
}
TraceEvent::Reuse { id, outcome } if *id == text => println!(" reuse: {outcome:?}"),
_ => {}
}
}
}
#[test]
#[ignore = "a diagnostic, not a check"]
fn what_box_the_text_is_drawn_in() {
diag::clear_traced_widgets();
let _ = diag::take();
let mut h = Harness::new((640, 900));
let ids = plant(&mut h);
let text = ids[1];
diag::trace_widget(text);
let _ = diag::take();
h.frame();
dump("first frame", &diag::take(), text);
for _ in 0..2 {
for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id);
}
let _ = diag::take();
h.frame();
dump("repaint", &diag::take(), text);
}
diag::clear_traced_widgets();
}
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = Aligned {
inner: text.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Neg),
y: None,
},
}
.add(&mut h.rsc);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
let sized = SetSize {
inner: inner.add_strong(&mut h.rsc),
x: Some(Len::px(189.0)),
y: Some(Len::px(176.0)),
}
.add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
}
#[test]
#[ignore = "a diagnostic, not a check"]
fn what_box_the_fixed_text_is_drawn_in() {
diag::clear_traced_widgets();
let _ = diag::take();
let mut h = Harness::new((1920, 1200));
let ids = plant_fixed(&mut h);
let text = ids[0];
diag::trace_widget(text);
let _ = diag::take();
h.frame();
dump("first frame at 1920", &diag::take(), text);
h.resize((640, 900));
h.frame();
dump("after resize to 640", &diag::take(), text);
let mut cold = Harness::new((640, 900));
let cids = plant_fixed(&mut cold);
diag::clear_traced_widgets();
diag::trace_widget(cids[0]);
let _ = diag::take();
cold.frame();
dump("cold at 640", &diag::take(), cids[0]);
diag::clear_traced_widgets();
}
+298
View File
@@ -0,0 +1,298 @@
//! The smallest trees that laid out differently warm than cold, each shrunk
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
//! frame that had not settled: a wrapping text shaped at a width it was
//! measured in rather than the one it was given. The rest are a widget
//! measured again in a box its own answer had decided, where the old answer
//! is a fixed point whatever the content now says.
use iris::harness::Harness;
use iris::prelude::*;
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a
/// cold one does.
fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
let sized = SetSize {
inner: wrapped.add_strong(&mut h.rsc),
x: Some(Len::px(76.0)),
y: None,
}
.add(&mut h.rsc);
let aligned = Aligned {
inner: sized.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Pos),
y: Some(AxisAlign::Pos),
},
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(root);
vec![
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
}
/// The first frame does not reach the layout a second one does, so "cold" is
/// not a fixed point and comparing against it compares against a tree that
/// has not settled.
#[test]
fn one_frame_is_enough() {
let mut h = Harness::new((640, 900));
let ids = plant(&mut h);
let first = h.region(&ids[1]).unwrap();
for _ in 0..3 {
for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id);
}
h.frame();
}
let settled = h.region(&ids[1]).unwrap();
println!(
"first frame {} tall, settled {} tall",
first.bot_right.y - first.top_left.y,
settled.bot_right.y - settled.top_left.y
);
assert_eq!(
first.bot_right.y - first.top_left.y,
settled.bot_right.y - settled.top_left.y,
"the first frame had not finished laying out"
);
}
#[test]
fn repainting_everything_moves_nothing() {
let mut warm = Harness::new((640, 900));
let ids = plant(&mut warm);
for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant(&mut cold);
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
/// is the same size whatever the output is, so a resize may not change any of
/// it -- but the text comes out 3.92px narrower warm than cold.
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = Aligned {
inner: text.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Neg),
y: None,
},
}
.add(&mut h.rsc);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
let sized = SetSize {
inner: inner.add_strong(&mut h.rsc),
x: Some(Len::px(189.0)),
y: Some(Len::px(176.0)),
}
.add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
}
#[test]
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
let mut warm = Harness::new((1920, 1200));
let ids = plant_fixed(&mut warm);
warm.frame();
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant_fixed(&mut cold);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
/// moving them, cold by growing them that way. Same widgets, same sizes, one
/// ends up 29.9px from where the other does.
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
let wrapped = wtext("Wrapping shapes one source into as many lines")
.size(16)
.wrap(true)
.add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
let second: StrongWidget = plain.add_strong(&mut h.rsc);
let children = match swapped {
true => vec![second, first],
false => vec![first, second],
};
let span = Span {
children,
dir: Dir::RIGHT,
gap: 0.0,
}
.add(&mut h.rsc);
let span_handle = span;
let aligned = Aligned {
inner: span.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Center),
y: None,
},
}
.add(&mut h.rsc);
h.state.root = Some(aligned.add_strong(&mut h.rsc));
(
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
span_handle,
)
}
#[test]
fn swapping_two_children_lands_where_growing_them_that_way_does() {
let mut warm = Harness::new((640, 900));
let (ids, span) = plant_pair(&mut warm, false);
warm.frame();
warm.rsc[span].children.rotate_left(1);
warm.frame();
let mut cold = Harness::new((640, 900));
let (cold_ids, _) = plant_pair(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
/// content from what the content says, and hands that box down through a
/// pass-through; the span under it was placed once, in that box, so nothing
/// at its own edge says the box was its own answer.
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let mut inner_children: Vec<StrongWidget> =
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
if swapped {
inner_children.rotate_left(1);
}
let inner = Span {
children: inner_children,
dir: Dir::RIGHT,
gap: 0.0,
}
.add(&mut h.rsc);
let block = rect(Color::RED).add(&mut h.rsc);
let fixed = SetSize {
inner: block.add_strong(&mut h.rsc),
x: Some(Len::px(87.0)),
y: None,
}
.add(&mut h.rsc);
let mut outer_children: Vec<StrongWidget> =
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
if swapped {
outer_children.rotate_left(1);
}
let outer = Span {
children: outer_children,
dir: Dir::RIGHT,
gap: 0.0,
}
.add(&mut h.rsc);
let through = SetSize {
inner: outer.add_strong(&mut h.rsc),
x: None,
y: None,
}
.add(&mut h.rsc);
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.state.root = Some(scroll.add_strong(&mut h.rsc));
(
vec![
text.id(),
filler.id(),
inner.id(),
block.id(),
fixed.id(),
outer.id(),
through.id(),
scroll.id(),
],
[inner, outer],
)
}
#[test]
fn a_span_placed_once_in_a_box_its_answer_decided() {
let mut warm = Harness::new((640, 900));
let (ids, spans) = plant_scrolled(&mut warm, false);
warm.frame();
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((640, 900));
let (cold_ids, _) = plant_scrolled(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}