Compare commits

..
Author SHA1 Message Date
iris-aiandClaude Fable 5.1 62a16b5608 Re-ask a dirty widget at its offer locally instead of deferring to its parent
A local redraw refused any widget whose given box was not as long as its
offer and marked its parent instead. Under the frame/extent protocol that
is nearly every widget beneath a self-sized container: a span hands its
children its own placement across itself, which is `FULL` while the span
is measured and its answer once it is placed, so the children's offer and
given frames differ on every such axis. A `many` frame at seed 13, depth
8 escalated 43 marks along chains up to seven levels and redrew 508 of
583 active widgets where e44dea3 redraws 159.

Retain the offer's frame beside the given one and ask the offer question
locally: the offer frame composed where the given one is, at the offer's
lengths and placement, then place at the given box where the two differ.
Seed 13 `many` goes from 4.73 ms to 1.28 ms against e44dea3's 0.90, and
294 distinct widgets a frame; size, scroll and repaint are unchanged.

Not sound yet: the suite, the debug oracle and the shrinker at 400 trees
of depth 5 pass, but the oracle at 1000 seeds of depth 6 diverges on seed
532 under reorder and seed 398 under every-size. Both reduce to a
self-sized container whose answer changes under a local redraw; the
reduced plans are in docs/HANDOFF.md of ai-app-2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 20:51:36 -04:00
iris-aiandClaude Opus 5 34cafb6edc Read the marks rather than the queue to decide the walk is done
Review of the two commits above. The queue was the walk's only record of
what was left, so a mark that reached `needs_redraw` without going through
`mark` -- an `on_undraw` handler is the reachable one -- would have waited
for the next frame. The set is read again once the queue drains, which is
what the scan it replaced did for free. `pop_last` takes the deepest entry
in one step rather than reading and then removing it.

The rest is comments: nine lines shorter, and the arm that takes an
ordinary ask said only what it does for a declared length.

Unchanged by all of it: 109 suite and 20 core tests, the four fuzzer runs
(100 seeds, 400 trees at depth 5, 1000 at depth 6, 2000 at depth 4), the
five reference renders and the resized `tabs`, and every counter on the
diagnostics rig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:31:29 -04:00
iris-aiandClaude Opus 5 3bf22935ce Take the deepest dirty widget from an ordered queue, not by scanning
The walk found the next widget to settle with `max_by_key` over the whole
`needs_redraw` set, and `depth` is a hash lookup, so a frame did a lookup
per marked widget per pop -- 131 depth reads for nine marks at seed 1
depth 8, 1,314 for 34, and 14,611 for 145. The set is scanned once now and
kept in a `BTreeSet` keyed by depth, and every mark made while the walk
runs goes through `mark`, which puts itself in place. The same three
counts become 57, 160 and 436.

Two things the scan gave for free are paid for explicitly: a widget that
was settled inside an ancestor's draw, or deferred to one, is dropped when
its entry comes up, and an entry whose widget has since changed depth --
a subtree that moved under a new parent -- is re-queued at the depth it now
has. What is drawn does not change: widget draws are identical at every
load measured.

Median frame at seed 1, depth 8: 0.955 -> 0.843 ms with 145 marks, 0.668 ->
0.666 with 34, and seed 13's default load 5.19 -> 4.86 ms. Ties between
equal depths now break by widget id rather than by hash order, which makes
the walk deterministic; nothing in the order within one depth was ever
relied on, since a widget at the same depth as another cannot contain it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:16:40 -04:00
iris-aiandClaude Opus 5 e6ba570d07 Give a child a part of the container's extent rather than its raw box
`Pad` and `Stack` read `Painter::placement` to put their children inside
their own drawing, and reading it is what says the drawing holds for that
placement alone. So a pad or a stack anywhere in a row was drawn again --
with its whole subtree -- the moment an earlier sibling changed length,
however little else had moved.

`widget_within` now takes a `DrawRegion`, and `DrawRegion::Extent(part)`
gives the child a part of the extent without reading it. What is retained
is the part rather than the box it resolved to, so moving the extent
re-places the child through the same rule instead of redrawing the parent:
`inherited_children` becomes `extent_children`, carrying `Inherit` for the
wrapper case `Painter::widget` already had and `Within(part)` for the new
one.

The dependency that goes up is a range on the container's extent rather
than on its frame, since only the part's *length* reaches the child and
where the part sits is re-placed. A declared length is unchanged: it is a
length of the frame wherever the box it sits in came from. What still pins
the placement is a report with a fraction in it -- the same fraction of a
different extent is a different length -- and that pin is on the answer,
which `extent_frames_keep_fractional_reports_and_numeric_dependencies_valid`
fails without.

Three tests from the first attempt at this come with it, and the
diagnostics rig now says which of the three contracts refused a reuse,
which is what found the above.

Measured, seed 1 at depth 8, median frame: `many` 0.667 -> 0.613 ms and
`resize` 48 -> 32 us; seed 13's `many` 6.35 -> 5.15 ms. Green: fmt, clippy,
109 suite and 20 core tests, the oracle at 100 seeds, the shrinker at 400
trees of depth 5, 1000 seeds at depth 6, and 2000 seeds at depth 4 over all
fifteen cases. The five reference renders are byte-identical to `0e107f0`
on Venus, as are `tabs` resized to 900x1200 and `random` to 1280x800
against cold renders there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:15:17 -04:00
iris-ai 0e107f0e89 Keep valid layout guarantees when a redraw widens their range 2026-09-17 17:24:53 -04:00
iris-ai f860f716e6 Separate measured-answer dependencies from retained drawing validity 2026-09-17 17:10:55 -04:00
iris-ai c44bd198ee Retain child frames relative to the container extent 2026-09-17 16:40:54 -04:00
iris-ai a7307d95fd Resolve a text draw's glyph origin once 2026-09-17 16:17:47 -04:00
iris-ai 7601aa2a5d Measure container children without intermediate placement 2026-09-17 15:53:24 -04:00
iris-ai c330ecec2b Skip inverse arithmetic for unrestricted layout validity 2026-09-17 15:53:24 -04:00
iris-ai 39f7b08c6c Honor fixed child alignment inside parent-selected slots 2026-09-17 15:53:24 -04:00
iris-ai 2ed5503717 Recompose retained frames exactly and preserve text width validity
Keep each widget's original local frame and replay the same composition
order on reuse. Remove inverse region remapping, including its fixed-frame
fallback that forced otherwise valid subtrees to draw again.

Require exact pixel-region equality in the shared generated oracle. Check
primitive and mask geometry as well as draw reuse when fixed frames resize.
Publish text's retained line-break range, with no upper bound when there
are no soft breaks, and cover widening, explicit newlines, and empty text.

Compared with efb416b, the depth-8 diagnostic rig performs 7-9% fewer widget
evaluations in the affected phases. Uninstrumented release runs use 3.5%
fewer instructions for size changes and 5.0% fewer for resize. Repaint and
scroll use 0.7% and 0.6% more instructions. Container updates remain substantially more expensive than the e44dea3 baseline;
this is still an experimental continuation, not a production replacement.
2026-09-17 15:11:03 -04:00
iris-ai efb416bbc3 Retain frame and extent dependencies independently
Keep the original measurement placement separate from the assigned slot.
Validate frame and extent lengths before reusing an answer or drawing, and
represent hint-only records as having no measured answer.

Retain primitive and mask coordinates with their frame/extent reference.
Forwarded children follow a reused wrapper's placement without rerunning
valid draw bodies. Keep the single Widget::draw API.

Restore the eight failing suite cases from the region/placement prototype,
with regressions for mixed coordinate references, a changed inherited
extent, the sizing-stack fraction, and an undrawn share becoming visible.

This remains experimental: nested container updates do substantially more
work than e44dea3 despite restoring the leaf and wrapper reuse guarantees.
Do not merge it as a performance improvement.
2026-09-17 14:50:09 -04:00
iris-ai 5fcace1bfa WIP: a widget's region stays put and its placement moves in it
The protocol split: `region` is the box a parent gives a widget -- what a
fraction it declares or reports is a fraction of, and the coordinates
every region it writes composes within -- and it is the same box on the
ask that measures and the ask that places. `placement` is what of that
region the drawing takes, chosen by the parent per axis or by the
widget's own answer and alignment.

That is what stops a fraction being resolved twice: the placing ask no
longer hands the widget its own answer as its box, so nothing under it
re-resolves against a box that came from its own report. `reports_of`
and `decided` are gone, folded into the two regions; `box_of` is gone;
`declared_box` becomes `ask_box`, which gives a rule the region's length
and takes the position from the placement.

84 of 92 suite tests pass. Five text and region-node cases still diverge
warm against cold, and three count a second widget draw where a span's
measuring ask and its placing ask give different placements.
2026-09-17 13:53:14 -04:00
iris-ai e44dea34b4 Record which widget is drawing a subtree that changed hands
A subtree can be reused whole under a different parent -- same box, same
layer, same region node, clean -- and nothing in the drawing says it
moved. Two things read who its parent is, and both were wrong after one
of these.

The old parent still listed it as a child, and a parent's next draw
undraws whatever is missing from that list: two spans under one root,
with the root swapping which of them it holds, drew the subtree under
the new span and then erased it when the old one drew. The move is
recorded on both sides where `draw_inner` already writes what the ask
decided, rather than guarded at each reader.

Its depth was also the one it had under the old parent, which is what
the settling walk orders by, so a change made under it afterwards
settled at the wrong point in the frame. `try_reuse` re-walks the
subtree's depths, and only where the top of it moved, which is what
makes that free in the ordinary case.

Two tests: one shape where the subtree's box does not move and the span
it left erases it, one where it changes depth and the change made under
it has to reach the span it moved to. Each fails without one half.
2026-09-17 13:05:15 -04:00
iris-ai a0693acc56 Let a resize settle through the walk, and drop the stale-answer guard
A resize drew the root outside `redraw_updates`, top-down over a tree
with dirty widgets still in it, which is the one entry point
`dirty_size_under` was guarding: since `a92c6ac` settles a frame strictly
bottom-up, no fuzzer could tell whether that guard still did anything
anywhere else. Closing the entry point retires the guard rather than
keeping a check for a hole reasoned rather than measured.

The root is marked instead, and only where the new output falls outside
what its answer holds for. That range is the intersection of everything
under it, so admitting the new output says the whole tree still stands,
and nothing above the root moved -- the window is no entry to rewrite.
Marking it unconditionally would have cost the root its own `Holds`: a
leaf root that scales with its box was drawn again on every resize.

`dirty_size_under` goes at both call sites. `resize` takes `Widgets`
because a mark is what it now leaves behind.
2026-09-17 13:00:32 -04:00
iris-ai 25e456e0b5 Say what the fuzzers can no longer tell about the stale-answer guard
Dropping `dirty_size_under` from it now passes every run there is. It stays
for the one entry the bottom-up ordering does not reach -- `update` draws
the root for a resize before `redraw_updates` runs -- which is a hole
reasoned rather than measured, and the note says which.
2026-09-17 05:12:44 -04:00
iris-ai 53b00c68e9 Find a span's leftover boundary through the inverse it already has
The decision used a rounded division, `total.px.div(fixed)`, where the room
the children get is a floored multiply, so the boundary and the drawing it
guards were two expressions for one length and disagreed at the edge of it.
`room` is that length as a `Len`, `room.to_px` is the multiply, and
`Holds::through` is its exact preimage -- so ask `room` whether anything is
left and hand the answer back through the same expression.

The three branches go with the division. They were the sign of `1 - rel`:
the fixed parts growing slower than the box, faster, or exactly with it, and
`through` reads that sign already. Forty lines become twelve, one `div`
leaves layout, and the boundary is the drawing's own.

Green on the suite, the shrinker at 400 seeds of depth 5, the oracle at 1000
seeds of depth 6 and 120 in debug, and 2000 seeds at depth 4 over all
fifteen cases. `tabs`, `view`, `minimal` and `random` byte-identical.
2026-09-17 05:02:45 -04:00
iris-ai a92c6acdbf Settle a frame strictly bottom-up rather than escalating into a parent
The queue was already deepest-first, but a widget that could not settle
where it was called `redraw` on its parent from inside itself. That drew a
shallow widget while dirty widgets deeper in other subtrees were still
pending, and a parent drawing over a subtree that has not settled reads
answers about to move: the one that settles does so inside the parent's
draw, where its mark comes off and nothing compares what it now answers.
Seed 564 was exactly that, and it is the second time this shape has been
found.

So a widget that cannot settle defers instead. It marks its parent, stays
marked itself, and waits in `deferred` until the walk down the depths
reaches the parent -- which cannot be before everything deeper has settled,
because the walk always takes the deepest widget that is not waiting. The
category stops being something to check for. (Bryan, 2026-09-17.)

`dirty_size_under` stays in `draw_inner` for now: `update` draws the root
for a resize before `redraw_updates` runs at all, so the ordering does not
cover that entry.

Green on the suite, the shrinker at 400 seeds of depth 5, the oracle at 1000
seeds of depth 6, and 2000 seeds at depth 4 over all fifteen cases. Drawn
widgets, widget draws and primitive writes are unchanged on every rig phase;
`many` pays 51 queue pops for 27 and 1059 depth reads for 410, which is the
deferring and nothing else.
2026-09-17 04:29:41 -04:00
iris-ai c8beca5753 Give the text example's aligned labels the width to align in
All three sat in the middle of a box the width of the widest of them, so
left, centred and right were the same picture. `text_align` puts the glyphs
somewhere in the box the text is given, and a text that reports the width of
its own glyphs is given exactly that -- there is nowhere for it to sit.
Declaring `rel(1.0)` on each hands it the row instead. (Bryan, 2026-09-17.)
2026-09-17 03:21:17 -04:00
iris-ai 4bd8607968 Report the step at or above a text's longest line
A wrapping text reported the width it used rounded to the nearest step,
which is under the line it measured half the time. A parent that sizes
itself from that report then hands the text back a box its own longest line
does not fit in, and breaking there is a different break -- one line more.

Two tolerances were hiding it and both go. `TextBuffer::shape` answered a
width up to 0.05 px under the longest line from the break in hand, which is
a structural decision taken on a hair's breadth: it kept a warm tree
self-consistent while a cold tree at the same width broke differently, and
0.05 px is fifty steps of the grid. The `Holds` range the text declares
started at the nearest step to its longest line for the same reason, so it
admitted boxes the line does not fit in. Both are the line itself now,
exactly, because the report no longer lands under it.

Found by seeds 1121 and 1839 at depth 4, which fail on `ea6dbae` and every
commit before it: a defect older than anything on this branch, reached by
running 2000 seeds at a depth the long runs do not use. Shrunk to the eight
widgets `a_text_is_given_back_a_box_the_line_it_measured_fits_in` builds.
2000 seeds at depth 4 over all fifteen cases are clean now, as are the
three long runs.

`text` is the one reference render that moves: its lower paragraph shifts a
pixel, the box being a step wider and its left edge crossing a snap
boundary. Same words, same lines, same breaks; `tabs`, `view`, `minimal`
and `random` are byte-identical.
2026-09-17 03:18:53 -04:00
iris-ai ffd79f32d3 Read a child's report as a fraction of the containing widget
`rel(0.5)` is half the span whatever else is in it and wherever the child
sits among them (Bryan, 2026-09-17). It was half of what the span had left
at the point it asked, because a report came back composed through the box
it was offered and a span offers each child the room from its cursor -- so
a nested span taking half of what it was given took a quarter of a row
whose first half was already spoken for, where the same half written as a
rule on the child took half the row.

The offer stays the remainder: a text has to wrap at the width actually
there, and `a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row`
pins that. What separates from it is the base a report's fractions are of,
which the ask now carries. It is the box the child was given wherever that
box is the child's whole area -- a pad's inset, a stack child, a scroll's
content -- and a span passes its own extent along the row.

`widget_decided` becomes `widget_at`, which says both things about an ask
rather than one of them; `widget_within` is still the sugar for neither.

Two spans asking for half each now take the whole row between them and a
third overflows, which the rewritten
`a_span_reads_a_child_report_as_a_fraction_of_the_row` states outright.
The five reference renders are byte-identical at 1920x1200 and `random`
live-resized still matches a cold render, so nothing that exists reports a
fraction to a span today.
2026-09-17 02:56:51 -04:00
iris-ai 0e0d4af326 Refuse a retained answer while something the widget measured is dirty
`draw_inner` took an answer from `try_reuse`, which checks only whether the
widget itself is marked, where `retained_answer` beside it also refused one
while anything the widget read a size from was dirty. A widget whose drawing
happened to be reusable therefore handed back the answer it gave before that
descendant changed.

Nothing puts that right afterwards. The comparison that tells a reader its
child's answer moved is in `redraw`, and a widget settled inside its parent's
own draw never goes through it -- the placing ask redraws the subtree, the
descendant's mark is cleared there, and the parent keeps a number the tree no
longer agrees with. So the check is not the optimization its comment claimed;
it is what makes the answer an answer, and both retained routes are answers,
so it is asked once in `draw_inner` rather than by one of them.

Found by the generated oracle at seed 564, depth 6, `shuffle-every-other`,
while reading a child's report as a fraction of the containing widget: that
reading lets a span overflow itself, which makes the two asks' boxes differ
far enough for the placing one to redraw.

Twenty-five rig work counters are unchanged on `cold`, `repaint`, `scroll`,
`resize` and `size`; `many` makes 18 fewer reuse attempts, 17 of which
already reported "dirty". Both long fuzzers green.
2026-09-17 02:46:25 -04:00
iris-aiandClaude Opus 5 ea6dbae0dc Hand a redrawn widget the mask it inherited, not its own
`ActiveData::mask` is the mask a widget's drawing is clipped to, which is
either one it set itself or the one it inherited. `redraw` passed it back as
the *inherited* mask, so a `Masked` widget settled on its own was handed its
own mask and `set_mask` asserted -- a panic on any local redraw of one, for
as long as there has been a local-redraw path. The two are separate facts, so
`parent_mask` keeps the second.

That also states the question `remap_subtree` was asking. It compared a
widget's mask with the one threaded down from its parent to find out whether
the widget owned it; the comparison is now between the two fields on the
widget, which is the same question asked where the answer lives, and the
parameter goes.

Checked: fmt, clippy, 87 suite tests including the new one, which panics
without this; 18 core unit tests; the release oracle at 100 seeds; the
fifteen shrinker cases at 400 seeds of depth 5; and `tabs` renders
byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:18:39 -04:00
iris-aiandClaude Opus 5 32542d0c0b Thread a box in pixels down the draw, one multiply from its parent's
A box in pixels was composed back up the move chain, on a grid fine enough
that the walk rounded once, while a widget's offer was threaded down through
its ancestors' offers. Two routes to one length, which is what
`Holds::through` allowed for -- and the offer's route broke at a region node.
`offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved
that against the node's slot entry, which holds the box its parent *placed*
the node in. Under a `Scroll` that is as long as the content rather than the
viewport, so everything below was re-asked at a width its own answer had
produced and the old answer confirmed itself: shrinker seed 220 on `reorder`
left a widget 290px out.

`ActiveData` now keeps a widget's box as lengths of its parent's box --
`given_len`, and `offer_len` for the box it was first asked about -- and
`DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a
time: the box its parent gave it, then the part of that box its own answer
placed the drawing in, which `placed_lens` states once for both `placed_box`
and the walk. `Painter::px_size` and `px_len` read that value, and
`UiRenderState::asked_px` takes the same steps back up the parent chain where
a local redraw starts part-way down the tree. Neither chain has a coordinate
frame in it, so neither can break at a region node, and warm and cold reach
every length by the same expression.

Three things follow. `Holds::through` is the exact preimage of
`px + floor(rel * box)` -- two divisions, no allowance, the whole of a box
mapping back to itself. A local redraw asks in the box its parent gave it and
only where that box is as long as the offer, which retires `redraw`'s third
ask and the region-node exception beside it; `draw_inner` places the answer
inside that box itself. And symbolic regions are left to the GPU, hit testing
and remaps, where `Moves::resolve` is the only walk: `wide.rs`,
`Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region`
and `slot_wide` are gone, 252 lines of `core/` net.

`px` is deliberately not stored beside those lengths. A resize every widget's
`Holds` admits redraws nothing, so a stored pixel length would be stale on
every widget in the tree with nothing on it to say so, and refreshing it costs
a walk down every reused subtree on the resize path.

Instructions:u, medians of 21 runs, seed 1 at depth 8:

| phase | before | after | |
| --- | ---: | ---: | ---: |
| `cold`, 200 frames | 313.1M | 312.9M | -0.04% |
| `resize` | 408.1M | 405.6M | -0.61% |
| `many` | 1,924M | 1,756M | -8.75% |
| `scroll` | 357.3M | 323.4M | -9.49% |
| `repaint` | 363.3M | 315.4M | -13.18% |

`cold` and `resize` have all twenty-five work counters identical, so those
two rows say the draw path costs the same threaded as composed. The other
three do less work: `repaint` goes from 23 draw requests and 13 widget draws
a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and
186 to 207 and 157. Primitive writes are unmoved in every phase.

Verified: `view`, `minimal`, `random`, `tabs` and `text` render
byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch
replay before and after the gesture, and a live resize of `random` to
1280x800 is identical both to the old head's and to a cold render at that
size. The oracle passes 100 seeds in release and 120 in debug -- the debug
run is the one that exercises the `Holds` assertion -- and the fifteen
shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`,
which needs both halves of this to fail: the old chain with the old allowance
passes it, and the old chain with the exact preimage does not.

`AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the
400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the
resize path re-expressing a part as a fraction of a box that changed length,
not a length reached two ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:12:56 -04:00
iris-aiandClaude Fable 5.1 5b7800264d Read a child's answer in the asker's frame, and drop the root move entry
A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.

That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.

The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.

Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:00:15 -04:00
iris-aiandClaude Opus 5 5f16617511 Carry the composed box down the draw, rather than walking back up for it
Every widget that reads its box in pixels was making `Moves` compose its
slot's chain again, a mean of 2.8 levels, about eight hundred times a frame.
A draw already descends past every one of those entries on its way in, so
`DrawInfo` carries what the slot composes to and `draw_at` steps it one box
further -- which is a select where it was a walk. `Moves::size_of` and
`compose` are left for `redraw`, which starts mid-tree with nothing above it
in flight.

Measured on the fixed-shape fixture, seed 1 depth 8, 500 frames of `many`,
medians of 25 runs, twenty-five work counters identical throughout:

| | instructions | cycles |
| --- | ---: | ---: |
| `d21a215`, before exact composition | 1,908M | 760M |
| `45a7176`, composing on the fine grid | 1,880M | 755M |
| this | **1,840M** | **735M** |

So exact composition ends up 3.6% fewer instructions and 3.3% fewer cycles
than the rounding-per-level walk it replaced, and the widening it needed was
paid for twice over by not doing the walk.

`Holds::through`'s allowance does not move: two half steps is where shrinker
seed 220 pins it, not where the arithmetic does. `Painter` still composes a
child's region into its own on the grid before asking for it in pixels, which
is the last narrow step in that path; taking it out needs the child's region
as its parent stated it, which `draw_inner` is not handed.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400
seeds of depth 5, and `tabs`, `view`, `minimal`, `text`, `random` and the tab
replay byte-identical at 1920x1200 against `45a7176`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 21:49:25 -04:00
iris-aiandClaude Opus 5 45a717695b Compose a box down its chain once, not once a level
Bryan's call, 2026-09-16, for correctness. `Moves` walked the move chain in
`Len`, so every level's four multiplies landed back on the grid before the
next started and the residue grew with the depth of the tree. `WideLen`
carries a length through the walk on a grid twenty-four bits of a box and
twenty-two of a pixel finer, and rounds once at the end.

What it buys, measured rather than argued: `Holds::through`'s allowance for
the two routes to a length drops from three half steps to two, and the whole
of a box now maps back to a range one step wide rather than one step per
level of nesting. One half step further is arithmetically available -- the
`Holds` assertion is quiet there and the whole-box case becomes an exact
identity -- and it is **not taken**, because shrinker seed 220 then lays out
differently warm than cold. Too narrow is meant to cost a redraw and no more;
there it re-breaks a wrapping text, whose reported width moves a `Branch`
onto its other subtree. That is the unsettled-text family, and closing it is
what would let this go lower. The note is in `through`.

`Moves` now answers three questions instead of one, and they are different
questions: `size_of` for how long a box is, which is what reads a box in
pixels; `compose` for where both of its ends are, which is what compares two
boxes; and `resolve`, unchanged, for the `Len` walk the vertex shader does
again in floats. A length composes on its own in two multiplies a level
rather than four, since where the parent sits falls out of the difference --
which is most of why this is not slower.

Measured on the fixed-shape fixture, seed 1 depth 8, 500 frames of `many`,
medians of 25 runs with all twenty-five work counters identical between the
two: 1,880M instructions and 755M cycles against 1,908M and 760M. So it is
free, and a little better on instructions. Three things were tried on the way
and two kept: composing the length alone rather than both ends (-111M
instructions), taking the pixel term's fraction on the ordinary grid so it
stays in an `i64` (-2M instructions, -8M cycles), and skipping a parent that
spans its own box, which **cost** 18M instructions and is not here -- the
same verdict a short-circuit got in `UiSpan::within`.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400
seeds of depth 5, and `tabs`, `view`, `minimal`, `text` and `random`
byte-identical at 1920x1200 against `d21a215`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 21:30:45 -04:00
iris-aiandClaude Opus 5 d21a21524f Rename WidgetPtr to Wrapper and give it a builder
Bryan's call, 2026-09-16: a length and an alignment are properties of one
widget, so a widget cannot both be 100 wide and take two shares of a row --
that needs two widgets, and the second one should do as little as possible.
`WidgetPtr` already was that widget: it draws its child in the whole of its
box and reports what the child said. It only lacked a name that says so and
a way to make one around an existing widget.

`Wrapper` rather than `Wrap` so it cannot be read as the text setting, and
`.wrapper()` rather than `.wrapped()` for the same reason. Its child stays
optional, since being a swappable slot is what it was written for and what
the tab bar still uses it as.

`set_ptr` is deleted rather than renamed. It had no caller, and putting a
widget into an existing wrapper is what `Wrapper::set` already does.

`tabs` draws its centred square again: `.sized((100, 100)).center()
.wrapper().width(leftover(2))` is two widgets where the chain without
`.wrapper()` was one, and `.width` was overwriting what `.sized` set. That
was the last of the three ways `tabs` had drifted from canonical `main`
unnoticed; what is left between them is the truncated multiply's antialiased
edges and the widget count itself.

`widget_trait!` takes no attributes, so `.wrapper()` carries an ordinary
comment and the explanation lives on `Wrapper`.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle
at 100 seeds, and `tabs` rendered at 1920x1200 against `main`'s own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:48:07 -04:00
iris-aiandClaude Opus 5 e166e005dc Pin that a length in pixels is that many pixels
Asked of the `tabs` render: does a gap come out the same number of pixels
wherever it appears? For a length in pixels it does, and structurally rather
than by luck -- `Len::within` adds a part's own pixels rather than scaling
them, and both ends of a gap carry the same fraction, so the multiply that
rounds is the same on each and cancels. The test buries a row of five under
three containers that are each a fraction of their parent, so nothing
reaches the window without being composed and rounded, and checks every gap
and every declared width at five box widths. Swept over 2,100 widths when it
was written and exact at every one.

For a share it does not, and the second test pins by how much rather than
pretending otherwise: one or two steps between children that asked for the
same fraction, 0.001 to 0.002 px. A position is the quantity that gets
rounded so the row fills exactly and no two children leave a seam, and that
is what costs it. Exact composition would shrink the spread, not remove it:
five equal lengths cannot fill a row whose step count is not a multiple of
five.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:16:18 -04:00
iris-aiandClaude Opus 5 38eba543f6 Tighten a validity range to what the arithmetic needs
`Holds::through`'s allowance for the two routes to a length was four half
steps either side, from a derivation that said each rounding now drops a
whole step where it used to drop half of one. That overshot: three is the
floor, two fires the `Holds` assertion in `draw_at` on eleven generated
cases, and four was never measured as necessary. Tightening both ends did
not move one of the rig's twenty-five work counters, so the extra half step
was not buying any reuse either.

It cannot go to zero. The range has to contain the box a drawing was made
in, which the assertion checks, and it must not contain a box the drawing
does not hold for, which the warm-against-cold oracle checks -- and those
two only coincide where a length reached two ways is the same number. It is
not, yet; composing in `i64` and narrowing once is the queued change that
would make it so, and shrinking this allowance is how to tell whether that
worked.

Checked: fmt, clippy, 81 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400
seeds of depth 5, and `tabs`, `view`, `minimal`, `text`, `random` and the tab
replay byte-identical at 1920x1200 against `2bc6bdf`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:15:44 -04:00
iris-aiandClaude Opus 5 2bc6bdfc77 Let a child with room to move use its own alignment
`Stack` and `Pad` forced the near edge on every child. That override exists
so a container that reports a child's size and then hands it the box derived
from that report does not place its content twice -- and it is owed only
where the box really is the child's own answer.

`Stack` gives every child the box its sizing child defines. That box is
`box_of(child.size())`, so the sizing child has no room in it and needs the
override; every other child is handed a box that owes nothing to it, and
where it sits in one bigger than itself is its own business. With the
override it could not be aligned at all.

`Pad` reports its inner's size plus the padding, so where its box is that
answer the inset box is exactly the inner and alignment has nowhere to move
it. Where the box is bigger -- a share of a row, a rule over the pad -- the
slack belongs to the inner, and the override pinned it to a corner.

The `tabs` example is the visible case both ways: its counters asked for
`Align::RIGHT` inside a stack and sat at the left, and `text`'s narrow panel
filled a row it had asked to sit at the top of. Both match canonical `main`
again. Neither was noticed when `d3b0ebf` made alignment a property, and the
handoff's claim that `tabs` then "differs only in the widget count it prints
about itself" was wrong -- it was checked at `8220a78` and not re-checked
after the next commit.

Checked: fmt, clippy, 81 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, and all fifteen shrinker cases at
400 seeds of depth 5. `tabs`, `text` and `random` change exactly where a
child now honours its own alignment; `view` and `minimal` are unchanged.

`tabs` is still not `main`'s render: `.sized((100, 100)).center().width(
leftover(2))` on one widget no longer means a square centred in a two-share
box, because one widget carries one length per axis and `.width` overwrites
what `.sized` set. That one is an API question, not a bug, and is open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:03:09 -04:00
iris-aiandClaude Opus 5 d8ae9c3bdd Place a locally redrawn widget once, in the box already chosen for it
`redraw` asks a dirty widget at its offer, and then again in the final box
its parent chose from that answer. The second ask handed that box over as if
it were an offer, so `draw_inner` ran `placed_box` on it and applied the
widget's own alignment to a box that had already been placed -- a second
placement on every local redraw of a widget that is not near-aligned. It
only showed where the widget's alignment was its own to apply: a container
override makes `draw_inner` take the box as given, and `Stack`, `Pad` and
`Scroll` override every child they hand a box to.

It is the fix for both of the handoff's standing warm-against-cold failures.
Shrinker seed 288 on `region-node` was an 8.8px inset at each end of a `Text`
under a `Span(Y-)` under two `Stack`s; oracle seed 326 at depth 6 was 88px on
a `Text` under two `Branch`es. Neither reduced below 11 and 43 widgets, and
both are this.

Checked: fmt, clippy, 80 suite tests, 17 core unit tests, the release oracle
at 100 seeds, **all fifteen shrinker cases at 400 seeds of depth 5**, and
**1000 seeds of depth 6** -- the last two for the first time. `tabs`, `text`,
`random` and the tab replay render byte-identical at 1920x1200 against
`08c9d5a`, since nothing about a cold layout changes.

Generated seed 20 at depth 4 catches it and joins the ordinary set, so
`cargo test` fails without this rather than only the ignored long run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:58:07 -04:00
iris-aiandClaude Opus 5 08c9d5aa32 Drop a multiply to the step below rather than rounding it
Bryan's call, 2026-09-16, taken for the cycles: a share now lands a
thousandth of a pixel short of its row instead of on it, which is less than
an even number of pixels draws.

`Fixed::mul` is a widening multiply and a shift, with the sign branch and the
half-step add gone. The two short-circuits priced against the old multiply go
with it: `UiSpan::within`'s test for a span that is the whole of its parent,
and `Fixed::scaled`'s test for nothing scaled by something, which was the
whole of `scaled` -- both cases come out of the truncating multiply unchanged,
and the bodies the comparisons cost were what kept the inliner from taking
`within` at all. `nm` is the check: `<UiSpan>::within` is a symbol in the
rounding head and in neither the float head nor this one.

`Holds::through` inverts the multiply, so its widening is re-derived: each
rounding now drops a whole step where it dropped half of one, which doubles
the allowance for the two routes to a length, and the multiply on the way in
drops only downward, so its own step goes at the top of the range alone. The
derived allowance for one truncation either side is measurably too narrow --
it excludes boxes drawings were made in, in eleven generated cases -- because
each route is a chain of multiplies rather than one.

Measured on the fixed-shape fixture (`Edits::fixed_branches`), seed 1 depth 8,
500 frames of `many`, medians of 25 runs of uninstrumented release binaries
with this VM's garbage `perf` readings dropped:

| | instructions | cycles | IPC |
| --- | ---: | ---: | ---: |
| `5ed9e87`, the float head | 1,761M | 688M | 2.561 |
| `60367d8`, rounding | 1,915M | 777M | 2.465 |
| this | 1,800M | 715M | 2.516 |

-6.0% instructions and -8.0% cycles against `60367d8`, whose twenty-five work
counters are identical to this one's, so that pair is the same work at a
different speed. It leaves +2.2% and +3.9% against the float head, from
+8.7% and +12.9% -- but the float head draws 100 widgets to this one's 97 and
writes 4,272 primitives to 3,951, so that pair is not, and the remainder is
not all arithmetic.

Checked: fmt, clippy, 80 suite tests and 18 core unit tests, the release
oracle at 100 seeds, all fifteen shrinker cases at 400 seeds of depth 5 (seed
288 on `region-node` still failing, unchanged), and depth-6 oracle seeds 18
and 190 passing with 326 still failing. `view`, `minimal`, `text`, `random`
and the tab replay render byte-identical at 1920x1200; `tabs` differs on
4,664 of 2,304,000 pixels, single-pixel-wide runs along 80 columns of one
band of rounded rects, which is an antialiased edge moved less than a pixel.

Three tests say what changed rather than being relaxed: a multiply drops on
both sides of zero, a division cannot put back what it dropped, and an
unevenly nested row's shares stay contiguous and end at its edge with each
edge on the even division or one step below.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:03:45 -04:00
iris-aiandClaude Opus 5 60367d806e Do not widen a validity range where nothing rounded
`Holds::through` inverts `px + rel * box`, and allowed three half steps
either side: one for that multiply's rounding and two for the difference
between a length composed down the chain and the same length measured
against the window. The whole of a box has no multiply in it -- `rel` is one
and taking the pixels off again is exact -- so the first half step was being
allowed for a rounding that did not happen, and it compounded: a chain of
widgets each taking the whole of its parent grew the interval half a step a
level. Traced while making the multiply truncate, where the same compounding
moved the interval off the box the drawing was made in and fired the
`Holds` assertion in eleven generated cases.

A range wider than what a drawing holds for is one that admits reusing it
where it does not hold, so this is the unsound direction to be loose in.

Checked: fmt, clippy, 80 suite tests and 16 core unit tests, the release
oracle at 100 seeds, all fifteen shrinker cases at 400 seeds of depth 5
(seed 288 on `region-node` still failing and unchanged by this), and `tabs`,
`view`, `minimal`, `text`, `random` plus the tab replay byte-identical at
1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:46:44 -04:00
iris-aiandClaude Opus 5 aea878d141 Place a locally redrawn widget in its box, not just at its length
A dirty widget is asked again in the box its parent asked it in, and then
again in the box its parent chose from that answer. The second ask was
skipped whenever the two boxes were the same *length*, which is not the same
question: an offer as long as the final box but somewhere else is a different
box. `d3b0ebf` already compared whole boxes for `parent_must_place` and left
this one a length comparison, so the two halves of one decision disagreed.

It shows on a region node, which draws the box it drew in into its own move
entry. A scroll inside a scrolled span is offered the outer scroll's whole
viewport and placed 24px above it, the height of the sized child the outer
scroll snaps to the end of; redrawing only its text left it at the offer and
24px too low. `tests/cases/unsettled.rs` had that five-widget tree ignored as
a known defect and now runs it.

`px_region` names the walk both comparisons and `window_region` were writing
out.

Checked: fmt, clippy, 80 tests, the release oracle at 100 seeds, all fifteen
shrinker cases at 400 seeds of depth 5, and `tabs`, `view`, `minimal`,
`text`, `random` plus the tab replay byte-identical at 1920x1200 against
`98d4e98`. The `many` fixture's twenty-five counters are unchanged.

Fixed with it, from the handoff's unreduced leads: shrinker seeds 174 and 175
on `repaint-some` and seed 2 on `region-node`, and oracle seeds 18 and 190 at
depth 6. Still failing: shrinker seed 288 on `region-node`, and oracle seed
326 at depth 6, which reduces to 43 widgets around two `Branch`es and is not
this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:25:26 -04:00
iris-aiandClaude Opus 5 98d4e98a29 Describe a tree before building it, so a failing seed can be reduced
The oracle grew its trees from a seed and the shrinker grew its own, with
every scenario written out on each side. So a failure the oracle found could
not be handed to the shrinker: there was no tree to pass it, only a seed, and
a seed cannot be made smaller. The shrinker could only grow its own trees and
hope to meet the same shape, which it does not -- 20,000 of its trees never
reproduced what the oracle's seed 18 shows at depth 6.

`iris::random` now answers with a `Plan`: `plan(seed, depth, &edits)` draws one
out of the random stream and `build(rsc, &plan)` makes the widgets, where
`grow` did both at once. Every draw happens in the order it always has, so a
seed still means the tree it meant -- checked by running the oracle at 1000
seeds of depth 6 before and after and getting the same three failures with the
same boxes. `Plan::smaller` reduces one, `Plan::edited` applies an `Edits` to a
tree that already exists, and `tests/scenario/` holds the fifteen cases both
rigs now run over the same trees.

A span keeps the order it holds its children in apart from the children
themselves, so detaching, attaching and reordering leave the widgets made in
the same order and two builds still line up index for index. `Tree::detached`
is gone: `Spanned::spares` is everything made for a span that it does not
hold, which is what both of those were.

`tests/cases/plan.rs` pins the three properties the rest rests on: editing a
plan is growing one with those edits, every simplification is smaller than
what it came from, and reducing ends. The second caught this change's own
defect, where dropping a side of a `Branch` duplicated another and grew the
tree by four widgets.

What it found, on its first run: `SHRINK_SEED=18 SHRINK_DEPTH=6
SHRINK_CASE=repaint-some` reduces 277 widgets to 5. A scroll inside a scroll,
the inner one owning a movable region, and only the text at the bottom marked
for redraw -- and the span lands 24px out, which is exactly the sized child's
height. `git bisect` names `95fb4f9`, where `Masked` began reporting its box
rather than its inner's size, so what the outer scroll is told its content
measures now depends on whether the inner subtree was redrawn this frame.
`tests/cases/unsettled.rs` has it written out, ignored until it is fixed.

Checked: fmt, clippy over all targets with -D warnings, the workspace tests
(79 + 11 + 15, one ignored for the defect above), and the 100-seed oracle over
all fifteen cases at depth 4. The shrinker at 400 seeds of depth 5 now fails,
which it did not before running the oracle's trees and cases: seeds 2 and 288
on region-node and 174 and 175 on repaint-some are unreduced leads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:58:31 -04:00
iris-aiandClaude Fable 5.1 4febabfd2e Wrap rather than saturate: nothing draws two million pixels out
A saturating add is five instructions where a wrapping one is one, and
it has no i32 vector form. Measured on the fixed-shape fixture, seed 1,
depth 8, 500 frames of `many`: 2,098M instructions and ~826M cycles down
to 1,918M and ~771M, with `random`, `tabs` and `text` byte-identical at
1920x1200 and the 100-seed oracle passing.

What saturating bought was ordering past the end of the range, where a
layout is already a defect; wrapping makes that defect obvious instead
of plausible. `from_f32` still clamps, since a float has the range to
come from anywhere, and `narrow` stays for `Holds`, whose range past
i32 really does mean unbounded. MIN and MAX remain unbounded ends only
where they are compared and never added to, which is every use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:22:48 -04:00
iris-aiandClaude Opus 5 394d5149a5 Measure a cost on a tree that does not move when layout does
`Branch` picks which of two subtrees to draw by comparing a measured pixel
length with a threshold. That is exactly what the oracle wants -- it is how a
widget believing a measurement a cold start would not have given it becomes a
different tree -- and exactly what a rig measuring cost must not have: the
fixture's shape moves with the thing being measured.

It has been moving. Seed 1 at depth 8 draws 88 widgets and writes 2,298
primitives a frame at `5ed9e87`, and 115 and 8,209 at `bd6de71` -- three and
a half times the work -- so the handoff's "fixed point cost 3x" compared two
different workloads and is withdrawn. Measured on one tree instead, with
`Edits::fixed_branches`, `5ed9e87` is 1,761M instructions and ~699M cycles
against this head's 2,093M and ~819M, while drawing 100 widgets against 97
and writing 4,272 primitives against 3,951. Fixed point costs something like
a fifth to a quarter, not three times.

The oracle keeps measured branches: `fixed_branches` is false by default and
only the rig sets it. A branch consumes its randomness either way, so both
grow the same ids.

**Check the work counters before comparing two commits' times.** The rig
prints drawn widgets, widget draws and primitive writes for this reason;
an undrawn `leftover` child still moves them, which no flag can remove.

Checked: fmt, clippy, 105 tests, the 100-seed generated oracle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:06:44 -04:00
iris-aiandClaude Opus 5 4cbb242a5d Do not multiply by a part of nothing
`lerp` is `a + (b - a) * f`, and `b - a` is nothing often enough to be worth
asking: a box with the same pixels at both ends of an axis, a span with no
fraction of one, a part of a subtree whose box did not move on that axis.
`Fixed::scaled` is `mul` that answers a zero receiver without widening to
`i64`, rounding and narrowing back, and `lerp` uses it -- so every lerp in
layout gets it rather than the two places that were about to grow their own
comparison.

`many` over 500 frames: 1,705,786,553 instructions to 1,657,571,216, and
638.9M cycles against 657.9M, averaged over four runs each.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, and
`tabs`, `text`, `random`, `minimal` and `view` byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:01:52 -04:00
iris-aiandClaude Opus 5 d75a1e2129 Do not multiply a box through the whole of its parent
Composing a box within another is four multiplies an axis, and two of the
shapes it is asked for compose to nothing: a part that is the whole box is
the box, and a box composed through the whole of its parent is itself. Both
are exact -- multiplying by one on the grid rounds to what it started as --
so four comparisons answer what four multiplies would have.

`many` over 500 frames: 1,742,553,104 instructions to 1,705,786,553, 2.1%
fewer, and 660M cycles to 658M. The cycles are the honest number and they
say this is worth little here; it is kept because instructions are what a
phone pays for and the check is four comparisons.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, the
100-seed generated oracle, and `tabs`, `text` and `random` byte-identical at
1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:55:30 -04:00
iris-aiandClaude Opus 5 1940e85c70 Move a whole box at once, since that is what a move does
Profiling a move by cycles rather than by instructions says the cost is not
where the last session recorded it. In `apply_scalar` the `i64` division is
**0.00%** of cycles and the multiply 1.5%: the time is in `saturating_add`,
which is five instructions and no vector form for an `i32`, and a box that
only moved does eight of them. Asking for them one scalar at a time, each
behind a match on which kind of move this is, gives the compiler four short
sequences where it had four adds in a row to pair up.

So a translation is now asked for once for the whole region -- which is what
a translation is -- and the match happens once above it rather than per
scalar. `many` over 500 frames: 684M cycles to 660M, and 1,815,666,327
instructions to 1,742,553,104.

Cycle counts are worth trusting here, which is the other thing to keep: three
runs of one binary varied 0.23%. It is wall time that varies 2x on this
machine, not the counters, and instructions alone cannot see a stall.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, and
`tabs`, `text` and `random` byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:50:58 -04:00
iris-aiandClaude Opus 5 cb1bba4682 Work a move out once for the subtree, not once for each part
`RegionRemap` re-derived the same things for every scalar of every part of a
moving subtree: the extent it divides by, whether the box only moved, whether
it spans the whole of its parent's, and the two ends of each `lerp`. All of
them are the same for the whole walk, because the walk is one box moving into
one other box. They are worked out once in `RegionRemap::new` now, as an
`AxisRemap` per axis that is either a translation or a scale.

Identical arithmetic in the same order, so the answers are unchanged: 500
frames of the `many` phase went from 1,886,328,855 instructions to
1,815,666,327, 3.8% fewer, and `tabs`, `text` and `random` are byte-identical
at 1920x1200.

Cycles moved 0.8%, which is the finding worth keeping: the surrounding
arithmetic was never the cost. The `i64` division is, and it is still there.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:46:23 -04:00
iris-aiandClaude Opus 5 490918b789 Ask whether a rule gives the length, not whether there is one
`Painter::ruled` answered "is there a rule beside me on this axis", which is
the same question as "is my report moot" only while `Exact` is the only rule
there is. `Min`, `Max` and `Clamp` are queued, and under one of those the
answer is still the widget's to give and a span across itself still has to
read its children -- so the name would have been true and the meaning wrong,
which is the worst way for a predicate to age.

It is `has_exact_size` now, over `SizeRule::exact` rather than `known`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:40:51 -04:00
iris-aiandClaude Opus 5 a8898aaa54 Give a length with no share in it its own type again
`UiScalar` was `Len` without the `leftover` weight, which is the separation
canonical `main` already had as `Len` beside `LayoutLen` and this branch
collapsed. It is needed back for the queued clamp: a cap may not contain a
share, because a cap has to read the report a rule otherwise makes moot, and
a share puts the container's division into the same equation -- two
self-consistent assignments, which is the multiple-fixed-point failure
generated seed 13 punished for orthogonal sizing. `min(report, cap)` is not
a `LayoutLen` either: it is a sum of parts, and the smaller of two of them
is not one.

So `UiScalar` is `Len`, what was `Len` is `LayoutLen`, and the two say in
their docs which is which: a `Len` is pixels plus a fraction of a box -- a
position being the length from the box's start, which is why a span is two
of them -- and a `LayoutLen` is a `Len` plus a claim only a container
dividing its room can answer. `From<Len> for LayoutLen` is the one-way step
between them.

Names only; the shader's `UiScalar` is renamed with them. Checked: fmt,
clippy, 105 tests, and `tabs`, `minimal`, `view`, `text` and `random`
byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:40:15 -04:00
46 changed files with 4440 additions and 2226 deletions

No files matched your search

+98 -36
View File
@@ -10,23 +10,30 @@ use std::{
/// chain, and the same box summed from what its children asked for -- and has /// chain, and the same box summed from what its children asked for -- and has
/// to decide whether the two are the same place. In floats they land a few /// to decide whether the two are the same place. In floats they land a few
/// bits apart, which is a defect wherever the answer changes what is drawn /// bits apart, which is a defect wherever the answer changes what is drawn
/// rather than where. Here adding and subtracting are exact and only a /// rather than where. Here adding and subtracting are exact, a multiply
/// multiply or a conversion rounds, back onto the same steps, so two routes /// drops to the step below, and a conversion between grids takes the nearest
/// that come within half a step land on one number and everything downstream /// one, so two routes to one place land on one number and everything
/// compares for equality instead of for nearness. /// downstream compares for equality instead of for nearness.
/// ///
/// `SHIFT` is the number of fractional bits, which is what makes the steps /// `SHIFT` is the number of fractional bits, which is what makes the steps
/// divide a whole number: a power of two also converts to `f32` without /// divide a whole number: a power of two also converts to `f32` without
/// rounding while the value fits in its mantissa. /// rounding while the value fits in its mantissa.
///
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
/// does. Saturating instead was measured at a twelfth of layout's
/// instructions -- five per add against one -- to keep the ordering of
/// coordinates two million pixels out, where nothing draws anyway. A value
/// off the end is a defect either way; wrapping makes it an obvious one.
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
#[repr(transparent)] #[repr(transparent)]
#[derive( #[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
)] )]
pub struct Fixed<const SHIFT: u32>(i32); pub struct Fixed<const SHIFT: u32>(i32);
/// A length or a coordinate in pixels, to a sixty-fourth. Finer than anything /// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
/// a display can show, and exact in `f32` up to 262,144 px, which is what lets /// anything a display can show, and exact in `f32` up to 16,384 px, which is
/// the same number reach the GPU. /// what lets the same number reach the GPU.
pub type Px = Fixed<PX_SHIFT>; pub type Px = Fixed<PX_SHIFT>;
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`] /// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
@@ -56,8 +63,8 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// The gap between neighbouring values, which is also how far apart two /// The gap between neighbouring values, which is also how far apart two
/// numbers can be and still mean the same place. /// numbers can be and still mean the same place.
pub const STEP: Self = Self(1); pub const STEP: Self = Self(1);
/// Also what stands in for an unbounded end, since arithmetic saturates /// Also what stands in for an unbounded end: compared against, never
/// here rather than wrapping past it. /// added to, since arithmetic wraps past it.
pub const MIN: Self = Self(i32::MIN); pub const MIN: Self = Self(i32::MIN);
pub const MAX: Self = Self(i32::MAX); pub const MAX: Self = Self(i32::MAX);
@@ -77,12 +84,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
} }
pub const fn from_int(v: i32) -> Self { pub const fn from_int(v: i32) -> Self {
Self(v.saturating_mul(Self::one().0)) Self(v.wrapping_mul(Self::one().0))
} }
/// Rounds to the nearest step, and saturates rather than wrapping. A NaN /// Rounds to the nearest step, and clamps to the ends of the grid rather
/// has no nearest step and becomes zero, which is a caller's mistake /// than wrapping: this is where a number from outside arrives, and a float
/// rather than a value worth carrying. /// has the range to be anywhere. A NaN has no nearest step and becomes
/// zero, which is a caller's mistake rather than a value worth carrying.
/// ///
/// Half-away is written out rather than called through `f32::round`, /// Half-away is written out rather than called through `f32::round`,
/// which is not `const`: a layout constant has to stay a constant. /// which is not `const`: a layout constant has to stay a constant.
@@ -101,6 +109,18 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
}) })
} }
/// The first step at or above `v`, where [`Self::from_f32`] takes the
/// nearest one and is below it half the time. For a bound that has to
/// admit the value it came from: a measurement rounded down is a bound
/// that leaves out the thing it was measured from.
pub const fn ceil_from_f32(v: f32) -> Self {
let nearest = Self::from_f32(v);
match nearest.to_f32() < v {
true => nearest.next_up(),
false => nearest,
}
}
/// From a number as it is written in source -- `16`, `1.5` -- which is /// From a number as it is written in source -- `16`, `1.5` -- which is
/// the other place a value enters the grid. /// the other place a value enters the grid.
pub fn from_num(v: impl UiNum) -> Self { pub fn from_num(v: impl UiNum) -> Self {
@@ -114,33 +134,41 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// The same value on another grid, rounded where the new one is coarser. /// The same value on another grid, rounded where the new one is coarser.
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> { pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
Fixed(match TO >= SHIFT { Fixed(match TO >= SHIFT {
true => narrow((self.0 as i64) << (TO - SHIFT)), true => self.0 << (TO - SHIFT),
false => narrow(shift_round(self.0 as i64, SHIFT - TO)), false => shift_round(self.0 as i64, SHIFT - TO) as i32,
}) })
} }
pub const fn add(self, rhs: Self) -> Self { pub const fn add(self, rhs: Self) -> Self {
Self(self.0.saturating_add(rhs.0)) Self(self.0.wrapping_add(rhs.0))
} }
pub const fn sub(self, rhs: Self) -> Self { pub const fn sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0)) Self(self.0.wrapping_sub(rhs.0))
} }
pub const fn neg(self) -> Self { pub const fn neg(self) -> Self {
Self(self.0.saturating_neg()) Self(self.0.wrapping_neg())
} }
/// Scaled by a number on any grid, which is how a length takes a fraction /// Scaled by a number on any grid, which is how a length takes a fraction
/// of itself and keeps being a length: the product is measured in the /// of itself and keeps being a length: the product is measured in the
/// receiver's steps. /// receiver's steps.
///
/// Dropped to the step below rather than taken to the nearest one
/// (Bryan, 2026-09-16), which costs a share a thousandth of a pixel of
/// its row -- less than an even number of pixels draws. Toward negative
/// infinity on both sides of zero, since that is a shift and nothing
/// else: a value and its negation therefore land different distances
/// from where they came, so a flipped span can sit a step from its
/// mirror image.
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self { pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
Self(narrow(shift_round(self.0 as i64 * by.0 as i64, BY))) Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
} }
/// Repeated a whole number of times, which no grid rounds. /// Repeated a whole number of times, which no grid rounds.
pub const fn mul_int(self, by: i32) -> Self { pub const fn mul_int(self, by: i32) -> Self {
Self(narrow(self.0 as i64 * by as i64)) Self(self.0.wrapping_mul(by))
} }
/// Divided into a whole number of parts, rounded to the nearest step. /// Divided into a whole number of parts, rounded to the nearest step.
@@ -149,12 +177,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
if by == 0 { if by == 0 {
return Self::ZERO; return Self::ZERO;
} }
Self(narrow(div_round(self.0 as i64, by as i64))) Self(div_round(self.0 as i64, by as i64) as i32)
} }
/// Divided by a number on any grid. A zero divisor is a caller bug -- a /// Divided by a number on any grid. A zero divisor is a caller bug -- a
/// box of no length has no fraction of itself -- and saturates so that a /// box of no length has no fraction of itself -- and answers with the end
/// release build lays out something absurd rather than dying. /// of the range so that a release build lays out something absurd rather
/// than dying.
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self { pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
debug_assert!(by.0 != 0, "dividing by a length of zero"); debug_assert!(by.0 != 0, "dividing by a length of zero");
if by.0 == 0 { if by.0 == 0 {
@@ -163,7 +192,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
false => Self::MAX, false => Self::MAX,
}; };
} }
Self(narrow(div_round((self.0 as i64) << BY, by.0 as i64))) Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
} }
/// `num / den` on *this* grid rather than on theirs, for weights coarser /// `num / den` on *this* grid rather than on theirs, for weights coarser
@@ -173,7 +202,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
if den.0 == 0 { if den.0 == 0 {
return Self::ZERO; return Self::ZERO;
} }
Self(narrow(div_round((num.0 as i64) << SHIFT, den.0 as i64))) Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
} }
/// `from` and `to` a fraction of the way apart, the fraction being the /// `from` and `to` a fraction of the way apart, the fraction being the
@@ -197,7 +226,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
} }
pub const fn abs(self) -> Self { pub const fn abs(self) -> Self {
Self(self.0.saturating_abs()) Self(self.0.wrapping_abs())
} }
pub const fn clamp(self, lo: Self, hi: Self) -> Self { pub const fn clamp(self, lo: Self, hi: Self) -> Self {
@@ -209,11 +238,11 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// boundary. The step is the whole gap, so there is nothing to exclude /// boundary. The step is the whole gap, so there is nothing to exclude
/// between this and the boundary itself. /// between this and the boundary itself.
pub const fn next_up(self) -> Self { pub const fn next_up(self) -> Self {
Self(self.0.saturating_add(1)) Self(self.0.wrapping_add(1))
} }
pub const fn next_down(self) -> Self { pub const fn next_down(self) -> Self {
Self(self.0.saturating_sub(1)) Self(self.0.wrapping_sub(1))
} }
} }
@@ -250,6 +279,8 @@ pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
} }
} }
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
/// lengths that runs past `i32` really is unbounded.
pub(crate) const fn narrow(v: i64) -> i32 { pub(crate) const fn narrow(v: i64) -> i32 {
if v > i32::MAX as i64 { if v > i32::MAX as i64 {
return i32::MAX; return i32::MAX;
@@ -353,6 +384,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y)) Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
} }
/// The first step at or above each part, for a measurement reported as a
/// box: what it occupies is not less than what was measured.
pub fn ceil_from_f32(v: Vec2) -> Self {
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
}
pub fn to_f32(self) -> Vec2 { pub fn to_f32(self) -> Vec2 {
Vec2::new(self.x.to_f32(), self.y.to_f32()) Vec2::new(self.x.to_f32(), self.y.to_f32())
} }
@@ -444,29 +481,54 @@ mod tests {
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO); assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
} }
/// Toward negative infinity on both sides of zero, which is what makes
/// it a shift rather than a shift and a sign branch -- and what makes a
/// value and its negation land different distances from where they came,
/// so a flipped span can sit a step from its mirror image.
#[test] #[test]
fn halves_round_away_from_zero_either_side() { fn a_multiply_drops_to_the_step_below_on_both_sides_of_zero() {
// A step and a half of one, which has no step of its own. // A step and a half of one, which has no step of its own.
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw()); let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(2)); assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(1));
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2)); assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
} }
/// A division rounds to the nearest step, so it cannot put back the
/// steps a truncating multiply dropped: a round trip comes back short,
/// never long, and by the few steps the two operations gave up.
#[test] #[test]
fn dividing_by_a_fraction_undoes_multiplying_by_it() { fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
let third = Rel::ONE / Rel::from_int(3); let third = Rel::ONE / Rel::from_int(3);
let len = Px::from_int(300); let len = Px::from_int(300);
assert_eq!(len * third / third, len); let back = len * third / third;
assert!(back <= len, "{back:?} is longer than {len:?}");
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200)); assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
} }
/// The bound a greedy line break needs: the width it was measured at is
/// not on the grid, and the narrowest box the break still holds for is
/// the step at or above it, never the one below.
#[test] #[test]
fn arithmetic_saturates_rather_than_wrapping() { fn a_ceiling_never_lands_below_the_number_it_came_from() {
assert_eq!(Px::MAX + Px::ONE, Px::MAX); let step = 1.0 / (1 << PX_SHIFT) as f32;
assert_eq!(Px::MIN - Px::ONE, Px::MIN); for n in 0..64 {
let v = 189.0 + n as f32 * step / 3.0;
let up = Px::ceil_from_f32(v);
assert!(up.to_f32() >= v, "{up:?} is below {v}");
assert!(
up.to_f32() - v < step,
"{up:?} is more than a step above {v}"
);
}
// An exact step is its own ceiling.
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
}
#[test]
fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::from_f32(1e12), Px::MAX); assert_eq!(Px::from_f32(1e12), Px::MAX);
assert_eq!(Px::from_f32(-1e12), Px::MIN); assert_eq!(Px::from_f32(-1e12), Px::MIN);
assert_eq!(Px::from_int(i32::MAX), Px::MAX);
} }
#[test] #[test]
+10 -4
View File
@@ -15,7 +15,7 @@
//! reuse, size, placement, and text events for one suspicious widget. The //! reuse, size, placement, and text events for one suspicious widget. The
//! selection is a set and survives [`take`] until cleared. //! selection is a set and survives [`take`] until cleared.
use crate::{Axis, Len, PxVec2, Size, UiRegion, WidgetId}; use crate::{Axis, LayoutLen, PxVec2, Size, UiRegion, WidgetId};
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
@@ -54,10 +54,13 @@ pub(crate) enum Counter {
TextShapes, TextShapes,
TextBreaks, TextBreaks,
GlyphPlacements, GlyphPlacements,
OutsidePlacement,
OutsideFrame,
OutsideExtent,
} }
impl Counter { impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1; const COUNT: usize = Self::OutsideExtent as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"updates", "updates",
@@ -89,6 +92,9 @@ impl Counter {
"text shapes", "text shapes",
"text line breaks", "text line breaks",
"glyph placements", "glyph placements",
"reuse outside: the placement it was pinned to",
"reuse outside: a frame length",
"reuse outside: an extent length",
]; ];
} }
@@ -287,7 +293,7 @@ pub enum TraceEvent {
id: WidgetId, id: WidgetId,
reader: WidgetId, reader: WidgetId,
axis: Axis, axis: Axis,
hint: Option<Len>, hint: Option<LayoutLen>,
}, },
TextRendered { TextRendered {
id: WidgetId, id: WidgetId,
@@ -389,7 +395,7 @@ pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
trace(id, TraceEvent::SizeRead { id, reader, size }); trace(id, TraceEvent::SizeRead { id, reader, size });
} }
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<Len>) { pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<LayoutLen>) {
trace( trace(
id, id,
TraceEvent::HintRead { TraceEvent::HintRead {
+7 -8
View File
@@ -84,8 +84,7 @@ pub struct RegionAlign {
} }
impl RegionAlign { impl RegionAlign {
/// Both axes at the near edge. What a container passes as an override for /// Both axes at the near edge: the start of a box in its own orientation.
/// a child it is going to position itself.
pub const NEAR: Self = Self { pub const NEAR: Self = Self {
x: AxisAlign::NEG, x: AxisAlign::NEG,
y: AxisAlign::NEG, y: AxisAlign::NEG,
@@ -172,14 +171,14 @@ impl Vec2 {
} }
} }
impl UiScalar { impl Len {
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 rest = Rel::ONE.sub(rel); let rest = Rel::ONE.sub(rel);
let at = UiScalar::from_parts(rel, Px::ZERO); let at = Len::from_parts(rel, Px::ZERO);
UiSpan { UiSpan {
start: UiScalar::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))), start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))),
end: UiScalar::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))), end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))),
} }
} }
} }
@@ -221,8 +220,8 @@ impl From<CardinalAlign> for Align {
const impl From<RegionAlign> for UiVec2 { const impl From<RegionAlign> for UiVec2 {
fn from(align: RegionAlign) -> Self { fn from(align: RegionAlign) -> Self {
Self::new( Self::new(
UiScalar::from_parts(align.x.rel(), Px::ZERO), Len::from_parts(align.x.rel(), Px::ZERO),
UiScalar::from_parts(align.y.rel(), Px::ZERO), Len::from_parts(align.y.rel(), Px::ZERO),
) )
} }
} }
+11
View File
@@ -7,6 +7,17 @@ pub enum Axis {
Y, Y,
} }
impl Axis {
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
match self {
Self::X => [aligned, ortho],
Self::Y => [ortho, aligned],
}
}
}
impl std::ops::Not for Axis { impl std::ops::Not for Axis {
type Output = Self; type Output = Self;
+73 -37
View File
@@ -3,23 +3,28 @@ use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Size { pub struct Size {
pub x: Len, pub x: LayoutLen,
pub y: Len, pub y: LayoutLen,
} }
/// What a widget asks for along one axis: pixels, a fraction of the box it /// What a widget asks for along one axis: a [`Len`] -- pixels and a fraction
/// is given, and a share of whatever is left over once everything fixed has /// of the box it is given -- plus a share of whatever is left over once
/// been taken. The three add up rather than choosing between one another. /// everything fixed has been taken. The parts add up rather than choosing
/// between one another.
///
/// Only a container dividing its room can answer a share, so a length nobody
/// divides is a `Len`: a position, a padding, a cap, anything already
/// resolved.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Len { pub struct LayoutLen {
pub px: Px, pub px: Px,
pub rel: Rel, pub rel: Rel,
pub leftover: Weight, pub leftover: Weight,
} }
impl<N: UiNum> From<N> for Len { impl<N: UiNum> From<N> for LayoutLen {
fn from(value: N) -> Self { fn from(value: N) -> Self {
Len::px(value.to_f32()) LayoutLen::px(value.to_f32())
} }
} }
@@ -32,21 +37,33 @@ impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
} }
} }
impl From<Len> for Size { /// A length with no share in it is a length a container does not have to
fn from(value: Len) -> Self { /// divide, which is one it can always give.
impl From<Len> for LayoutLen {
fn from(len: Len) -> Self {
Self {
px: len.px,
rel: len.rel,
leftover: Weight::ZERO,
}
}
}
impl From<LayoutLen> for Size {
fn from(value: LayoutLen) -> Self {
Self { x: value, y: value } Self { x: value, y: value }
} }
} }
impl Size { impl Size {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
x: Len::ZERO, x: LayoutLen::ZERO,
y: Len::ZERO, y: LayoutLen::ZERO,
}; };
pub const LEFTOVER: Self = Self { pub const LEFTOVER: Self = Self {
x: Len::LEFTOVER, x: LayoutLen::LEFTOVER,
y: Len::LEFTOVER, y: LayoutLen::LEFTOVER,
}; };
/// From something measured outside layout -- a texture, a shaped line -- /// From something measured outside layout -- a texture, a shaped line --
@@ -57,28 +74,28 @@ impl Size {
pub const fn from_px(v: PxVec2) -> Self { pub const fn from_px(v: PxVec2) -> Self {
Self { Self {
x: Len { x: LayoutLen {
px: v.x, px: v.x,
..Len::ZERO ..LayoutLen::ZERO
}, },
y: Len { y: LayoutLen {
px: v.y, px: v.y,
..Len::ZERO ..LayoutLen::ZERO
}, },
} }
} }
pub fn rel(v: Vec2) -> Self { pub fn rel(v: Vec2) -> Self {
Self { Self {
x: Len::rel(v.x), x: LayoutLen::rel(v.x),
y: Len::rel(v.y), y: LayoutLen::rel(v.y),
} }
} }
pub fn leftover(v: Vec2) -> Self { pub fn leftover(v: Vec2) -> Self {
Self { Self {
x: Len::leftover(v.x), x: LayoutLen::leftover(v.x),
y: Len::leftover(v.y), y: LayoutLen::leftover(v.y),
} }
} }
@@ -89,7 +106,7 @@ impl Size {
} }
} }
pub fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self { pub fn from_axis(axis: Axis, aligned: LayoutLen, ortho: LayoutLen) -> Self {
match axis { match axis {
Axis::X => Self { Axis::X => Self {
x: aligned, x: aligned,
@@ -102,15 +119,22 @@ impl Size {
} }
} }
pub fn axis(&self, axis: Axis) -> Len { pub fn axis(&self, axis: Axis) -> LayoutLen {
match axis { match axis {
Axis::X => self.x, Axis::X => self.x,
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl Len { impl LayoutLen {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
px: Px::ZERO, px: Px::ZERO,
rel: Rel::ZERO, rel: Rel::ZERO,
@@ -126,12 +150,24 @@ impl Len {
/// The whole of what is left over counts as the whole box, which is what /// The whole of what is left over counts as the whole box, which is what
/// a length means to something that is not dividing a box between /// a length means to something that is not dividing a box between
/// siblings -- a scroll asking how long its content is. /// siblings -- a scroll asking how long its content is.
pub fn apply_leftover(&self) -> UiScalar { pub fn apply_leftover(&self) -> Len {
let share = match self.leftover > Weight::ZERO { let share = match self.leftover > Weight::ZERO {
true => Rel::ONE, true => Rel::ONE,
false => Rel::ZERO, false => Rel::ZERO,
}; };
UiScalar::from_parts(self.rel.add(share), self.px) Len::from_parts(self.rel.add(share), self.px)
}
/// This length, given as a part of a box `len` long, as a part of the
/// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len);
Self {
px: part.px,
rel: part.rel,
leftover: self.leftover,
}
} }
pub fn px(px: impl UiNum) -> Self { pub fn px(px: impl UiNum) -> Self {
@@ -157,24 +193,24 @@ impl Len {
pub mod len_fns { pub mod len_fns {
use super::*; use super::*;
pub fn px(px: impl UiNum) -> Len { pub fn px(px: impl UiNum) -> LayoutLen {
Len::px(px) LayoutLen::px(px)
} }
pub fn rel(rel: impl UiNum) -> Len { pub fn rel(rel: impl UiNum) -> LayoutLen {
Len::rel(rel) LayoutLen::rel(rel)
} }
pub fn leftover(ratio: impl UiNum) -> Len { pub fn leftover(ratio: impl UiNum) -> LayoutLen {
Len::leftover(ratio) LayoutLen::leftover(ratio)
} }
} }
impl_op!(same Len Add add; px rel leftover); impl_op!(same LayoutLen Add add; px rel leftover);
impl_op!(same Len Sub sub; px rel leftover); impl_op!(same LayoutLen Sub sub; px rel leftover);
impl_op!(same Size Add add; x y); impl_op!(same Size Add add; x y);
impl_op!(same Size Sub sub; x y); impl_op!(same Size Sub sub; x y);
impl Default for Len { impl Default for LayoutLen {
fn default() -> Self { fn default() -> Self {
Self::leftover(1.0) Self::leftover(1.0)
} }
@@ -186,7 +222,7 @@ impl std::fmt::Display for Size {
} }
} }
impl std::fmt::Display for Len { impl std::fmt::Display for LayoutLen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.px != Px::ZERO { if self.px != Px::ZERO {
write!(f, "{} px;", self.px)?; write!(f, "{} px;", self.px)?;
+67 -38
View File
@@ -6,41 +6,41 @@ use crate::{Px, PxVec2, Rel, UiNum, util::impl_op};
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct UiVec2 { pub struct UiVec2 {
pub x: UiScalar, pub x: Len,
pub y: UiScalar, pub y: Len,
} }
impl UiVec2 { impl UiVec2 {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
x: UiScalar::ZERO, x: Len::ZERO,
y: UiScalar::ZERO, y: Len::ZERO,
}; };
pub const fn new(x: UiScalar, y: UiScalar) -> Self { pub const fn new(x: Len, y: Len) -> Self {
Self { x, y } Self { x, y }
} }
pub const fn px(px: impl const Into<Vec2>) -> Self { pub const fn px(px: impl const Into<Vec2>) -> Self {
let px = px.into(); let px = px.into();
Self { Self {
x: UiScalar::px(px.x), x: Len::px(px.x),
y: UiScalar::px(px.y), y: Len::px(px.y),
} }
} }
/// From lengths already on the grid, with no fraction of a box. /// From lengths already on the grid, with no fraction of a box.
pub const fn from_px(px: PxVec2) -> Self { pub const fn from_px(px: PxVec2) -> Self {
Self { Self {
x: UiScalar::from_parts(Rel::ZERO, px.x), x: Len::from_parts(Rel::ZERO, px.x),
y: UiScalar::from_parts(Rel::ZERO, px.y), y: Len::from_parts(Rel::ZERO, px.y),
} }
} }
pub const fn rel(rel: impl const Into<Vec2>) -> Self { pub const fn rel(rel: impl const Into<Vec2>) -> Self {
let rel = rel.into(); let rel = rel.into();
Self { Self {
x: UiScalar::rel(rel.x), x: Len::rel(rel.x),
y: UiScalar::rel(rel.y), y: Len::rel(rel.y),
} }
} }
@@ -61,14 +61,14 @@ impl UiVec2 {
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar { pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
match axis { match axis {
Axis::X => &mut self.x, Axis::X => &mut self.x,
Axis::Y => &mut self.y, Axis::Y => &mut self.y,
} }
} }
pub fn axis(&self, axis: Axis) -> UiScalar { pub fn axis(&self, axis: Axis) -> Len {
match axis { match axis {
Axis::X => self.x, Axis::X => self.x,
Axis::Y => self.y, Axis::Y => self.y,
@@ -83,7 +83,7 @@ impl UiVec2 {
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE); pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self { pub const fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
match axis { match axis {
Axis::X => Self { Axis::X => Self {
x: aligned, x: aligned,
@@ -129,21 +129,27 @@ where
} }
} }
/// A position along one axis, as a fraction of the box it sits in plus an /// A length along one axis: a fraction of the box it is measured in plus an
/// offset: `rel * len + px`. Both parts are fixed point, so composing one /// offset, `rel * box + px`. A position is the same number -- the length from
/// through a chain of boxes rounds only where it multiplies and lands on the /// the start of the box to the point -- which is why a [`UiSpan`] is two of
/// same number as any other route to the same place. /// these. Both parts are fixed point, so composing one through a chain of
/// boxes rounds only where it multiplies, and lands on the same number as any
/// other route to the same place.
///
/// It carries no claim on what a container has left over. That is
/// [`crate::LayoutLen`], which is this plus a weight, and which means nothing
/// to anyone but whoever divides the room.
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)]
pub struct UiScalar { pub struct Len {
pub rel: Rel, pub rel: Rel,
pub px: Px, pub px: Px,
} }
impl_op!(same UiScalar Add add; rel px); impl_op!(same Len Add add; rel px);
impl_op!(same UiScalar Sub sub; rel px); impl_op!(same Len Sub sub; rel px);
impl UiScalar { impl Len {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
rel: Rel::ZERO, rel: Rel::ZERO,
px: Px::ZERO, px: Px::ZERO,
@@ -192,10 +198,8 @@ impl UiScalar {
} }
} }
/// Both channels by the same factor, which is what a fraction of a /// Both parts by the same fraction, which is what a part of a length
/// length means when the length is part pixels and part a share. /// means when the length is part pixels and part a fraction of a box.
/// Both channels by the same fraction, which is what a part of a length
/// means when the length is part pixels and part a share.
pub const fn scale(&self, by: Rel) -> Self { pub const fn scale(&self, by: Rel) -> Self {
Self { Self {
rel: self.rel.mul(by), rel: self.rel.mul(by),
@@ -215,14 +219,14 @@ impl UiScalar {
} }
} }
pub fn within_len(&self, len: UiScalar) -> Self { pub const fn within_len(&self, len: Len) -> Self {
self.within(&UiSpan { self.within(&UiSpan {
start: UiScalar::ZERO, start: Len::ZERO,
end: len, end: len,
}) })
} }
pub fn select_len(&self, len: UiScalar) -> Self { pub fn select_len(&self, len: Len) -> Self {
len.within_len(*self) len.within_len(*self)
} }
@@ -245,24 +249,24 @@ impl UiScalar {
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UiSpan { pub struct UiSpan {
pub start: UiScalar, pub start: Len,
pub end: UiScalar, pub end: Len,
} }
impl UiSpan { impl UiSpan {
pub const FULL: Self = Self { pub const FULL: Self = Self {
start: UiScalar::ZERO, start: Len::ZERO,
end: UiScalar::FULL, end: Len::FULL,
}; };
pub const fn rel(rel: f32) -> Self { pub const fn rel(rel: f32) -> Self {
Self { Self {
start: UiScalar::rel(rel), start: Len::rel(rel),
end: UiScalar::rel(rel), end: Len::rel(rel),
} }
} }
pub const fn new(start: UiScalar, end: UiScalar) -> Self { pub const fn new(start: Len, end: Len) -> Self {
Self { start, end } Self { start, end }
} }
@@ -273,11 +277,16 @@ impl UiSpan {
std::mem::swap(&mut self.start.px, &mut self.end.px); 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: Len) {
self.start += offset; self.start += offset;
self.end += offset; self.end += offset;
} }
/// Composing a box through the one it sits in, and the hottest line in
/// layout. It used to skip the multiplies where a span was the whole of
/// its parent or the parent the whole of its own; both come out of the
/// multiply unchanged anyway, and the body those comparisons cost was
/// what kept the inliner from taking this at all.
pub const fn within(&self, parent: &Self) -> Self { pub const fn within(&self, parent: &Self) -> Self {
Self { Self {
start: self.start.within(parent), start: self.start.within(parent),
@@ -285,9 +294,18 @@ impl UiSpan {
} }
} }
pub const fn len(&self) -> UiScalar { pub const fn len(&self) -> Len {
self.end - self.start self.end - self.start
} }
/// Both ends by the same amount, which is what moving a box without
/// changing its length does to every part of it.
pub const fn translated(self, by: Len) -> Self {
Self {
start: self.start + by,
end: self.end + by,
}
}
} }
#[repr(C)] #[repr(C)]
@@ -298,6 +316,17 @@ pub struct UiRegion {
} }
impl UiRegion { impl UiRegion {
/// Every part of the box by the same amount on each axis. Done to the
/// whole region rather than an end at a time, because that is what it is
/// -- and because four adds in a row are four adds, where four asked for
/// separately are four sequences.
pub const fn translated(self, x: Len, y: Len) -> Self {
Self {
x: self.x.translated(x),
y: self.y.translated(y),
}
}
pub const FULL: Self = Self { pub const FULL: Self = Self {
x: UiSpan::FULL, x: UiSpan::FULL,
y: UiSpan::FULL, y: UiSpan::FULL,
+26 -12
View File
@@ -107,13 +107,6 @@ 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,
@@ -183,6 +176,23 @@ impl TextBuffer {
self.layout_key.as_ref()?.max_width self.layout_key.as_ref()?.max_width
} }
/// Widths covered by the current line breaks, including a wider shaping
/// retained when a later draw requested a narrower box.
pub fn width_holds(&self) -> crate::Holds {
let Some(width) = self.wrap_width() else {
return crate::Holds::ANY;
};
let width = Px::from_f32(width);
let soft_wrapped = self.layout.lines().any(|line| {
matches!(
line.break_reason(),
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
)
});
let upper = if soft_wrapped { width } else { Px::MAX };
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
}
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height()) Vec2::new(self.layout.width(), self.layout.height())
} }
@@ -200,15 +210,19 @@ impl TextBuffer {
// A greedy break at one width is the same break at every width down // 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 // 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 // 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 // hand already answers, and re-breaking would only be work.
// disagree with itself -- which is what happens when a parent offers //
// a child the length that child just reported, and the two land // At the longest line exactly, with no margin below it. A narrower
// either side of a float. // width really does break differently, so answering one from the
// break in hand is how a warm tree keeps lines a cold tree would
// never produce. The margin was here because a text reports the
// width it used and a parent hands that back; the report is the step
// at or above its longest line now, so what comes back fits.
if let Some(key) = &self.layout_key if let Some(key) = &self.layout_key
&& key.attrs == *attrs && key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width) && let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at && want <= broke_at
&& want + BREAK_EPSILON_PX >= self.layout.width() && want >= self.layout.width()
{ {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits); diag::bump(Counter::TextShapeHits);
+2 -1
View File
@@ -106,7 +106,8 @@ impl UiRenderNode {
self.active.push(i); self.active.push(i);
for change in draws.apply_free() { for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives { for primitive in &mut inst.primitives {
let h = &mut primitive.handle;
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
+18 -14
View File
@@ -35,6 +35,10 @@ struct MoveOffset {
// belongs to the pixel above it. Flooring the product instead drops a pixel // belongs to the pixel above it. Flooring the product instead drops a pixel
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of // wherever a fraction divides a window exactly: a fifth of 1920 comes out of
// `REL_STEP` as 383.99998, and five tabs each lose their last column. // `REL_STEP` as 383.99998, and five tabs each lose their last column.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
fn snap_floor(v: vec2<f32>) -> vec2<f32> { fn snap_floor(v: vec2<f32>) -> vec2<f32> {
return floor(v + PX_STEP * 0.5); return floor(v + PX_STEP * 0.5);
} }
@@ -49,16 +53,16 @@ struct RawSpan {
end: RawScalar, end: RawScalar,
} }
fn scalar_of(raw: RawScalar) -> UiScalar { fn scalar_of(raw: RawScalar) -> Len {
return UiScalar(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP); return Len(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP);
} }
fn span_of(raw: RawSpan) -> UiSpan { fn span_of(raw: RawSpan) -> UiSpan {
return UiSpan(scalar_of(raw.start), scalar_of(raw.end)); return UiSpan(scalar_of(raw.start), scalar_of(raw.end));
} }
fn scalar_of_pair(raw: vec2<i32>) -> UiScalar { fn scalar_of_pair(raw: vec2<i32>) -> Len {
return UiScalar(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP); return Len(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP);
} }
struct Region { struct Region {
@@ -72,12 +76,12 @@ const MOVE_NONE: u32 = 4294967295u;
// resolve a deep one the same way. // resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u; const CHAIN_LIMIT: u32 = 64u;
// The same expression `UiScalar::within` uses, in floats rather than on the // The same expression `Len::within` uses, in floats rather than on the
// CPU's grid: a move is resolved here so that scrolling a subtree writes one // CPU's grid: a move is resolved here so that scrolling a subtree writes one
// entry instead of walking it. What has to hold is that this agrees with // entry instead of walking it. What has to hold is that this agrees with
// itself frame to frame, not that it matches the CPU to the last bit. // itself frame to frame, not that it matches the CPU to the last bit.
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar { fn scalar_within(s: Len, p: UiSpan) -> Len {
return UiScalar( return Len(
p.start.rel + (p.end.rel - p.start.rel) * s.rel, p.start.rel + (p.end.rel - p.start.rel) * s.rel,
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel), s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
); );
@@ -102,11 +106,11 @@ fn resolve_move(idx: u32, local: Region) -> Region {
} }
struct UiSpan { struct UiSpan {
start: UiScalar, start: Len,
end: UiScalar, end: Len,
} }
struct UiScalar { struct Len {
rel: f32, rel: f32,
px: f32, px: f32,
} }
@@ -147,8 +151,8 @@ fn vs_main(
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_px = vec2(r.x.end.px, r.y.end.px); let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px); let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px); let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
let size = bot_right - top_left; let size = bot_right - top_left;
let uv = vec2<f32>( let uv = vec2<f32>(
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
let br = vec2(m.x.end.rel, m.y.end.rel); let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px); let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px); let top_left = snap_floor(tl * window.dim + tl_px);
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px); let bot_right = snap_floor(br * window.dim + 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;
+77 -26
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
UiRegion, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,20 +9,32 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// The box its drawing is in, in `parent_move`'s coordinates. /// The box its parent gave it, in `parent_move`'s coordinates: what it
/// was asked about, and what a fraction under it is a fraction of. A
/// local redraw asks here.
pub region: UiRegion, pub region: UiRegion,
/// The box its parent first asked about it in, as a part of the box the /// Where its drawing sits inside that box, in the box's own coordinates.
/// parent was itself asked in. Any later box it was given was decided pub placement: UiRegion,
/// knowing its answer, so this is where a question about it is asked /// The original frame in its parent widget's coordinates. Recomposition
/// again -- and it is kept relative so that it follows the parent's. /// and pixel-length evaluation both follow this chain.
pub offer: UiRegion, pub given_region: UiRegion,
/// What it answered there: the size and what that held for. /// The frame it was first asked in, in the same coordinates: the offer's
pub answer: (Size, [Holds; 2]), /// frame, which its parent's placing draw may since have narrowed.
pub offer_region: UiRegion,
/// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
/// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it.
pub offer_len: UiVec2,
pub offer_placement: [Option<crate::UiSpan>; 2],
/// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// What the widget said it used of its box, the last time it drew. /// What the widget said it used of its box, the last time it drew.
pub size: Size, pub size: Size,
/// The pixel lengths of `region`, per axis, that its drawing and `size` /// The frame, extent and explicit placement reads that this drawing holds for.
/// hold for. pub holds: LayoutHolds,
pub holds: [Holds; 2],
pub drawn: bool, pub drawn: bool,
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
@@ -30,7 +42,13 @@ pub struct ActiveData {
/// widget a frame visits and cannot drift while one is being drawn. /// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize, pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>,
/// The children whose box is a part of this widget's extent rather than
/// of its frame, and which part each was given. Moving the extent
/// re-places them through that part, so the drawing need not depend on
/// where it sits.
pub(crate) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>, pub size_deps: Vec<WidgetId>,
@@ -40,25 +58,58 @@ pub struct ActiveData {
/// The declared lengths whoever drew this widget resolved into its box. /// The declared lengths whoever drew this widget resolved into its box.
/// A change to one moves a box this widget cannot fix by drawing again, /// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so. /// and comparing them is what says so.
pub declared: [Option<Len>; 2], pub declared: [Option<LayoutLen>; 2],
/// The alignment its parent asked it with. A local redraw repeats that /// The axes along which its parent chose its box from its own answer,
/// question, including an override chosen by a container. /// so a local redraw asks the question its parent asked.
pub align: RegionAlign, pub decided: [bool; 2],
/// Whether that alignment was the parent's override rather than the /// Its alignment when it was last drawn, which a change to the property
/// widget's own property. /// is found against.
pub align_override: bool,
/// Its own alignment when it was last drawn. A change to the property is
/// found against this even when its parent overrode the alignment.
pub own_align: RegionAlign, pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses. /// The movable region whose coordinates `region` uses.
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
pub mask: MaskIdx, pub mask: MaskIdx,
/// That inherited one. The two differ exactly where the widget set a
/// mask of its own, which is the one it owns and the one a move rewrites
/// -- and the one a redraw of it must not be handed back, since setting
/// a mask asserts there is none.
pub parent_mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
impl ActiveData { impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths. /// Whether what it answered still stands for a box of these pixel
pub fn holds_at(&self, px: crate::PxVec2) -> bool { /// lengths -- the box it was asked in, where `holds` is about the box its
self.holds[0].contains(px.x) && self.holds[1].contains(px.y) /// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
self.answer.is_some_and(|(_, holds)| {
holds.contains(
px,
UiRegion {
x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL),
y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL),
},
)
})
}
}
/// What of a container's extent a child was given: the whole of it, for a
/// wrapper whose box is its child's, or a part of it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum ExtentPlacement {
Inherit,
Within(UiRegion),
}
impl ExtentPlacement {
/// The child's frame in the container's frame coordinates, and the slot
/// the container chose within it.
pub fn resolve(self, extent: UiRegion) -> (UiRegion, [Option<crate::UiSpan>; 2]) {
match self {
Self::Inherit => (UiRegion::FULL, [Some(extent.x), Some(extent.y)]),
Self::Within(part) => (part.within(&extent), [None; 2]),
}
} }
} }
+36
View File
@@ -0,0 +1,36 @@
use crate::{PrimitiveHandle, UiRegion};
/// Retains which box geometry follows when only the extent changes.
#[derive(Clone, Copy, Debug)]
pub enum DrawRegion {
Frame(UiRegion),
Extent(UiRegion),
}
impl DrawRegion {
pub(crate) fn resolve(self, frame: UiRegion, extent: UiRegion) -> UiRegion {
match self {
Self::Frame(local) => local.within(&frame),
Self::Extent(local) => local.within(&extent).within(&frame),
}
}
pub(crate) fn map(self, f: impl FnOnce(UiRegion) -> UiRegion) -> Self {
match self {
Self::Frame(local) => Self::Frame(f(local)),
Self::Extent(local) => Self::Extent(f(local)),
}
}
}
impl From<UiRegion> for DrawRegion {
fn from(region: UiRegion) -> Self {
Self::Frame(region)
}
}
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: DrawRegion,
}
+78 -26
View File
@@ -1,4 +1,4 @@
use crate::{Px, REL_SHIFT, UiScalar, fixed::div_toward, fixed::narrow}; use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for: /// The lengths of a box, in pixels, that one drawing of a widget holds for:
@@ -10,9 +10,9 @@ use std::ops::RangeInclusive;
/// ///
/// The ends are lengths on the grid rather than floats with a tolerance /// The ends are lengths on the grid rather than floats with a tolerance
/// around them: a box offered back at the length a widget reported comes back /// around them: a box offered back at the length a widget reported comes back
/// as the same number, so a range means what it says. What widening there is /// as the same number, so a range means what it says. The one place a range
/// belongs to [`Self::through`], which has a rounding to undo, and is derived /// is wider than the length it came from is [`Self::through`], and what it is
/// from that rounding rather than chosen. /// wider by is the floor that inverting a fraction undoes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holds { pub struct Holds {
pub lo: Px, pub lo: Px,
@@ -41,32 +41,32 @@ impl Holds {
} }
/// What a box has to be for a part of it, `len` of the box long, to stay /// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range. A part with no relative extent is a fixed length: it /// in this range: the exact preimage of `px + floor(rel * box)`, which is
/// was drawn at that length and any box keeps it there. /// the one way a box in pixels is reached. A part with no relative extent
/// is a fixed length -- it was drawn at that length and any box keeps it
/// there.
/// ///
/// The way in is `px + rel * box` taken to the nearest step, so a part /// The answer is an interval even where this range is a single length,
/// of exactly `lo` came from anything within half a step of it and the /// because the multiply on the way in drops to the step below and many
/// answer is an interval even where this range is one length. Inverting /// boxes therefore give one length. That is a floor rather than an
/// the length alone instead gives a point that need not even contain the /// allowance: inverting it is two divisions and nothing else, and the
/// box the part was drawn in, which is a range excluding the drawing it /// whole of a box maps back to itself.
/// was made for. pub const fn through(self, len: Len) -> Self {
pub const fn through(self, len: UiScalar) -> Self { if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
return Self::ANY;
}
let rel = len.rel.raw() as i64; let rel = len.rel.raw() as i64;
if rel == 0 { if rel == 0 {
return Self::ANY; return Self::ANY;
} }
// Three half steps either side -- one for the rounding on the way
// in, two for the difference between a length composed down the
// chain and the same length measured against the window -- and half
// of what a `Rel` counts in, to divide by the fraction. Exact until
// the division takes it back to the grid.
let px = len.px.raw() as i64; let px = len.px.raw() as i64;
let half_rel = REL_SHIFT - 1; // `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
let lo = ((self.lo.raw() as i64 - px) * 2 - 3) << half_rel; // `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
let hi = ((self.hi.raw() as i64 - px) * 2 + 3) << half_rel; let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
// Dividing by a negative turns the ends around, so which end each let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
// bound comes from is decided before dividing rather than by taking // Dividing by a negative fraction turns the ends around, so which
// the min and max of four divisions. // bound each comes from is decided before dividing rather than by
// taking the min and max of four divisions.
match rel > 0 { match rel > 0 {
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)), true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)), false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
@@ -95,10 +95,20 @@ mod tests {
use super::*; use super::*;
use crate::Rel; use crate::Rel;
#[test]
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
for px in [-8, 0, 8] {
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
assert_eq!(Holds::ANY.through(len), Holds::ANY);
}
}
}
#[test] #[test]
fn through_reverses_a_range_for_a_negative_fraction() { fn through_reverses_a_range_for_a_negative_fraction() {
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20. // `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
let part = UiScalar::from_parts(Rel::from_f32(-0.5), Px::from_int(10)); let part = Len::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part); let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20))); assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19))); assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
@@ -109,13 +119,55 @@ mod tests {
/// box is not a whole number of steps. /// box is not a whole number of steps.
#[test] #[test]
fn a_part_maps_back_onto_the_box_it_was_measured_in() { fn a_part_maps_back_onto_the_box_it_was_measured_in() {
let part = UiScalar::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146)); let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
for box_len in (440..460).map(Px::from_int) { for box_len in (440..460).map(Px::from_int) {
let holds = Holds::at(part.to_px(box_len)).through(part); let holds = Holds::at(part.to_px(box_len)).through(part);
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}"); assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
} }
} }
/// A widget handed the whole of its parent's box, with or without pixels
/// taken off it, has no fraction to invert: multiplying by one is exact
/// and taking the pixels off again is too, so the box maps back to
/// itself. Allowing for anything here compounded a step a level down a
/// chain of widgets each taking the whole of its parent.
#[test]
fn the_whole_of_a_box_maps_back_to_itself() {
let at = Px::from_int(956);
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
assert_eq!(
Holds::at(at).through(less_eight),
Holds::at(at + Px::from_int(8))
);
}
/// The range is the exact preimage at both ends, so a box one step
/// outside it really does give a length outside this range. What a wider
/// range costs is a drawing reused where it does not hold.
#[test]
fn a_box_one_step_outside_the_range_is_outside_it() {
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
let at = Px::from_int(300);
let holds = Holds::at(at).through(part);
for inside in [holds.lo, holds.hi] {
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
}
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
}
}
/// A truncating multiply only ever drops, so the step it needs allowing
/// for on the way in belongs at the top of the range and not the bottom.
#[test]
fn a_fraction_widens_further_up_than_down() {
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
let holds = Holds::at(Px::from_int(100)).through(half);
let box_len = Px::from_int(200);
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
}
#[test] #[test]
fn a_boundary_the_next_step_along_does_not_admit_it() { fn a_boundary_the_next_step_along_does_not_admit_it() {
let boundary = Px::from_int(10); let boundary = Px::from_int(10);
+63
View File
@@ -0,0 +1,63 @@
use crate::{Axis, Holds, PxVec2, UiRegion};
/// Dependencies of one evaluation, before the frame and extent are composed.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub frame: [Holds; 2],
pub extent: [Holds; 2],
pub placement: Option<UiRegion>,
}
impl LayoutHolds {
pub const ANY: Self = Self {
frame: [Holds::ANY; 2],
extent: [Holds::ANY; 2],
placement: None,
};
pub fn and(self, other: Self) -> Self {
debug_assert!(
self.placement.is_none()
|| other.placement.is_none()
|| self.placement == other.placement
);
Self {
frame: [
self.frame[0].and(other.frame[0]),
self.frame[1].and(other.frame[1]),
],
extent: [
self.extent[0].and(other.extent[0]),
self.extent[1].and(other.extent[1]),
],
placement: self.placement.or(other.placement),
}
}
pub fn covers(self, other: Self) -> bool {
self.placement
.is_none_or(|placement| other.placement == Some(placement))
&& [0, 1].into_iter().all(|n| {
self.frame[n].lo <= other.frame[n].lo
&& self.frame[n].hi >= other.frame[n].hi
&& self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi
})
}
pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool {
self.placement.is_none_or(|old| old == placement)
&& [Axis::X, Axis::Y].into_iter().all(|axis| {
self.frame[axis as usize].contains(px.axis(axis))
&& self.extent[axis as usize]
.contains(placement.axis(axis).len().to_px(px.axis(axis)))
})
}
pub fn in_frame(self, placement: UiRegion) -> [Holds; 2] {
[Axis::X, Axis::Y].map(|axis| {
self.frame[axis as usize]
.and(self.extent[axis as usize].through(placement.axis(axis).len()))
})
}
}
+16 -6
View File
@@ -10,12 +10,16 @@ use crate::{
pub const CHAIN_LIMIT: u32 = 64; pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod draw_region;
mod holds; mod holds;
mod layout_holds;
mod painter; mod painter;
mod render_state; mod render_state;
pub use active::*; pub use active::*;
pub use draw_region::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
@@ -68,17 +72,24 @@ impl Moves {
} }
} }
/// Composes a region held in `idx`'s coordinates down the chain, which is /// The same walk the vertex shader does, in the same `Len` the shader is
/// the same walk the vertex shader does. /// handed, for asking where a drawing will actually land -- hit testing,
/// and nothing layout decides on. Layout threads its lengths down the
/// draw instead, so no box it compares is composed back up this chain.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion { pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local; let mut region = local;
self.walk(idx, |entry| region = region.within(entry));
region
}
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
let mut at = idx; let mut at = idx;
for _ in 0..CHAIN_LIMIT { for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE { if at == MoveIdx::NONE {
return region; return;
} }
let entry = self.arena[at.idx()]; let entry = &self.arena[at.idx()];
region = region.within(&entry.region); step(&entry.region);
at = entry.parent; at = entry.parent;
} }
debug_assert!( debug_assert!(
@@ -86,7 +97,6 @@ impl Moves {
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \ "a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth" and the shader stops at the same depth"
); );
region
} }
/// How many slots a region in `idx` is composed through, which is what /// How many slots a region in `idx` is composed through, which is what
+407 -154
View File
@@ -1,12 +1,12 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget, TextAttrs, Axis, DrawRegion, ExtentPlacement, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign,
TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Weight, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
WidgetId, Widgets, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::render_state::DrawInfo,
}; };
@@ -17,27 +17,51 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's box, in the coordinates of `move_idx`. /// The box its parent gave it, in the coordinates of `move_idx`: what a
/// fraction of this widget's area is a fraction of, and what every region
/// it writes composes within. The same box on the ask that measures and
/// the ask that places, which is what keeps a fraction under it from
/// being resolved twice.
pub(super) region: UiRegion, pub(super) region: UiRegion,
/// Where this widget's drawing sits inside that box, in the box's own
/// coordinates: `FULL` while its answer is not yet known, and the box
/// its answer or its parent chose once one of them has.
pub(super) placement: UiRegion,
/// Whether this draw read its placement, which makes the drawing one
/// that holds for that placement alone -- the way reading a length in
/// pixels makes it hold for that length.
pub(super) reads_placement: bool,
/// That box in pixels, which its children's are a length of: threaded
/// down from the box this widget was given rather than composed back up
/// the chain, so every length in layout is one multiply from its
/// parent's and [`Holds::through`] inverts exactly that.
pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<DrawRegion>,
pub(super) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub(super) extent_own: [Holds; 2],
/// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked in /// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer. /// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>, pub(super) offered: Vec<WidgetId>,
/// The box this widget was first asked about in, in pixels. /// The lengths of the box this widget was first asked about in, in
/// pixels. Its children's offers are a fraction of it.
pub(super) offered_px: PxVec2, pub(super) offered_px: PxVec2,
/// Whether this draw is in that box, which makes the questions it asks /// Whether this draw is in a box of those lengths, which makes the
/// the ones a cold layout asks and their answers the ones to keep. /// questions it asks the ones a cold layout asks and their answers the
/// ones to keep.
pub(super) at_offer: bool, pub(super) at_offer: bool,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of its box in pixels, per axis: every /// What this draw itself read of its box in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise. /// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2], pub(super) own: [Holds; 2],
/// What the children it asked about and drew keep it to. /// Dependencies of every child drawing, including unmeasured overlays.
pub(super) under: [Holds; 2], pub(super) under: LayoutHolds,
/// The movable region this widget's primitives are positioned through: /// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's. /// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx, pub(super) move_idx: MoveIdx,
@@ -50,13 +74,28 @@ pub struct Painter<'a> {
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: DrawRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>(); let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region); self.write(kind, primitive, region);
} }
/// Takes the kind, for a caller writing many of one primitive. /// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) { fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: DrawRegion) {
self.write_resolved(
kind,
primitive,
region,
region.resolve(self.region, self.placement),
);
}
fn write_resolved<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: DrawRegion,
resolved: UiRegion,
) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites); diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
@@ -65,15 +104,15 @@ impl<'a> Painter<'a> {
kind, kind,
id: self.id, id: self.id,
primitive, primitive,
region, region: resolved,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx, move_idx: self.move_idx,
}, },
); );
self.push_primitive(h); self.push_primitive(RetainedPrimitive { handle: h, region });
} }
fn push_primitive(&mut self, h: PrimitiveHandle) { fn push_primitive(&mut self, h: RetainedPrimitive) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -81,35 +120,55 @@ impl<'a> Painter<'a> {
self.primitives.push(h); self.primitives.push(h);
} }
/// Writes a primitive to be rendered /// Writes a primitive over the whole of this widget's own box.
pub fn primitive(&mut self, primitive: impl PrimitiveLike) { pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let at = DrawRegion::Extent(UiRegion::FULL);
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region) self.primitive_at(primitive, at)
} }
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { /// Writes in the frame by default. `DrawRegion::Extent` keeps the local
/// geometry attached to this widget's box without reading its placement.
pub fn primitive_within(
&mut self,
primitive: impl PrimitiveLike,
region: impl Into<DrawRegion>,
) {
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region)); self.primitive_at(primitive, region.into());
} }
pub fn set_mask(&mut self, region: UiRegion) { /// Sets a mask in the selected frame or extent coordinates.
pub fn set_mask(&mut self, region: impl Into<DrawRegion>) {
let region = region.into();
self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { self.mask = self.rsc.ui_mut().masks.push(Mask {
region, region: region.resolve(self.region, self.placement),
move_idx: self.move_idx, move_idx: self.move_idx,
}); });
} }
/// Draws a widget within this widget's region. /// Draws a widget in the whole of this widget's own box: it gets the
/// same region -- the same area for its fractions to be of -- and is put
/// where this widget was put. What a container that is only a wrapper
/// around one child wants, since its box is the child's.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_within(id, UiRegion::FULL) let own = self.placement;
self.widget_at_inner(
id,
UiRegion::FULL,
[Some(own.x), Some(own.y)],
Some(ExtentPlacement::Inherit),
false,
)
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// What a widget's rules declare its lengths to be, which whoever draws
/// it resolves into its box. Reading them depends on nothing -- the box /// it resolves into its box. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it /// that comes of them is kept on the child, and `redraw` compares it
/// there. /// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<Len>; 2] { fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
declared_lens(self.rsc.widgets(), id.id()) declared_lens(self.rsc.widgets(), id.id())
} }
@@ -118,50 +177,77 @@ impl<'a> Painter<'a> {
/// this frame; what it answered is still something this widget asked. /// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) { pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id()); self.children.retain(|child| *child != id.id());
self.extent_children.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc); self.state.undraw_rec(id.id(), self.rsc);
} }
/// Draws a widget somewhere within this one. `region` is in this widget's /// Draws a child in `region`, relative to this widget's frame. The child
/// own coordinates, and the child's declared lengths are still to be /// resolves declared lengths and reports against that frame, then places
/// taken from it. Where the child's drawing sits inside what it is given /// its drawing by its own alignment.
/// is the child's alignment, applied where the child is drawn, so a ///
/// container positions a child either by handing it a box of exactly its /// `DrawRegion::Extent` gives a part of where this widget's drawing sits
/// length or by leaving it room and letting its alignment decide. /// instead, for a container whose children belong inside that rather than
/// inside the box it was offered. The part is what is kept, so moving the
/// extent re-places the child rather than drawing this widget again.
pub fn widget_within<'s, W: ?Sized>( pub fn widget_within<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: impl Into<DrawRegion>,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, None) match region.into() {
DrawRegion::Frame(region) => self.widget_at(id, region, [None; 2]),
DrawRegion::Extent(part) => {
let within = part.within(&self.placement);
self.widget_at_inner(
id,
within,
[None; 2],
Some(ExtentPlacement::Within(part)),
false,
)
}
}
} }
/// Draws a widget with an alignment chosen by its container rather than /// Draws a widget in `region`, saying where in it the drawing goes.
/// the widget's property. Containers use this when the box they hand down ///
/// already expresses the size they report around the child. /// `region` is the child's own area: what a fraction it declares or
pub fn widget_aligned<'s, W: ?Sized>( /// reports is a fraction of, and the coordinates the regions it writes
/// compose within. It is the same box on the ask that measures and the
/// ask that places, which is what stops a fraction under it being
/// resolved twice.
///
/// `placement` is what of that region the child's drawing takes, per
/// axis, wherever this widget is choosing. `None` leaves the axis to the
/// child's own answer and alignment, which is what
/// [`Self::widget_within`] passes. A span passes the whole row as the
/// region, so `rel(0.5)` is half the row wherever the child sits in it,
/// and places the child by passing the slot along its axis.
pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
align: RegionAlign, placement: [Option<UiSpan>; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, Some(align)) self.widget_at_inner(id, region, placement, None, false)
} }
fn widget_at<'s, W: ?Sized>( fn widget_at_inner<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
align_override: Option<RegionAlign>, placement: [Option<UiSpan>; 2],
extent: Option<ExtentPlacement>,
measuring: bool,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.extent_children.retain(|(child, _)| *child != id.id());
if let Some(extent) = extent {
self.extent_children.push((id.id(), extent));
}
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id())); let align = self.rsc.widgets().alignment(id.id());
// Composing `FULL` through a box is not quite the identity in f32, let (local, placement) = ask_box(region, declared, align, placement);
// so a child with nothing declared keeps the box it would have had.
let local = match declared.iter().any(Option::is_some) {
true => declared_box(region, declared, align),
false => region,
};
let within = match local == UiRegion::FULL { let within = match local == UiRegion::FULL {
true => self.region, true => self.region,
false => local.within(&self.region), false => local.within(&self.region),
@@ -176,15 +262,37 @@ impl<'a> Painter<'a> {
self.children.push(id.id()); self.children.push(id.id());
} }
let first_ask = self.offer(id.id()); let first_ask = self.offer(id.id());
let offer = match first_ask { let given_len = local.size();
true => local, let offer_len = match first_ask {
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer), true => given_len,
false => self
.state
.active
.get(&id.id())
.map_or(given_len, |a| a.offer_len),
}; };
let answers_offer = self.at_offer && local == offer; let offer_region = match first_ask {
true => local,
false => self
.state
.active
.get(&id.id())
.map_or(local, |a| a.offer_region),
};
let offer_placement = if first_ask {
placement
} else {
self.state
.active
.get(&id.id())
.map_or(placement, |a| a.offer_placement)
};
let px = given_len.to_px(self.px);
let offered_px = offer_len.to_px(self.offered_px);
// The answer and what it holds for, both about the box asked in. The // The answer and what it holds for, both about the box asked in. The
// child's record may say something else once its drawing has been // child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box. // placed: a drawing made again in its placed box holds for that box.
let (size, holds) = self.state.draw_inner( let (size, answer_holds, holds) = self.state.draw_inner(
id.id(), id.id(),
within, within,
DrawInfo { DrawInfo {
@@ -194,35 +302,84 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
offer, given_region: local,
offered_px: self.px_within_offer(offer), offer_region,
align: align_override, offer_len,
offer_placement,
px,
offered_px,
placement,
}, },
None, None,
measuring,
self.rsc, self.rsc,
); );
if answers_offer { let in_parent = |holds: LayoutHolds| {
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds); let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
let chosen = placement[n].unwrap_or(UiSpan::FULL).len();
match extent {
// Its box is this widget's own, so what its drawing holds
// for is what this widget's extent holds for.
Some(ExtentPlacement::Inherit) if declared[n].is_none() => {
result.frame[n] = holds.frame[n].through(local.axis(axis).len());
result.extent[n] = holds.extent[n];
if holds.placement.is_some() {
result.placement = Some(self.placement);
} }
// Whatever the child's answer holds for keeps this one to the boxes }
// that give the child a length inside it. // Its box is a part of this widget's extent, so what it
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { // holds for is a range on that extent and none of it a
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); // range on the frame. Only the part's length reaches it,
// which is what lets the extent move without a redraw.
Some(ExtentPlacement::Within(part)) if declared[n].is_none() => {
result.extent[n] = holds.frame[n]
.and(holds.extent[n].through(chosen))
.through(part.axis(axis).len());
}
// Its box is a length of this widget's frame: an
// ordinary ask, or a declared length, which is that
// length wherever the box it sits in came from.
_ => {
result.frame[n] = holds.frame[n].through(local.axis(axis).len()).and(
holds.extent[n]
.through(chosen)
.through(local.axis(axis).len()),
);
}
}
}
result
};
self.under = self.under.and(in_parent(holds));
let mut answer_holds = in_parent(answer_holds);
// What it reports is a fraction of the box it was given, which is a
// part of this widget's extent -- so the same fraction is a different
// length once that extent is, and pixels are not. The answer only:
// the drawing this holds is re-placed rather than made again.
if matches!(extent, Some(ExtentPlacement::Within(_)))
&& AXES.into_iter().any(|axis| {
declared[axis as usize].is_none() && size.axis(axis).rel != crate::Rel::ZERO
})
{
answer_holds.placement = Some(self.placement);
} }
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size, size: in_parent_frame(size, local.size(), declared),
answer_holds,
} }
} }
/// What a child says its length is without being drawn, if it can say. /// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size. /// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let widgets = self.rsc.widgets(); let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the // A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too. // widget would draw, so it has to win over what the widget says too.
let hint = widgets.size_rules(id.id()).axis(axis).known().or_else(|| { let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
widgets widgets
.get_dyn(id.id()) .get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis)) .and_then(|widget| widget.size_hint(axis))
@@ -244,41 +401,53 @@ impl<'a> Painter<'a> {
} }
} }
/// A child's length in the box it is about to be offered, if it can be /// Measures a child's length from its hint, a retained answer, or `draw`.
/// had without drawing it: from its hint, or from a drawing it already /// A fresh draw evaluates the offer without placing its answer. The caller
/// has that holds for that box. /// must later place or undraw the child.
pub fn known_len<W: ?Sized>( pub fn measure_len<W: ?Sized>(
&mut self, &mut self,
child: &StrongWidget<W>, child: &StrongWidget<W>,
axis: Axis, axis: Axis,
region: UiRegion, region: UiRegion,
) -> Option<Len> { placement: [Option<UiSpan>; 2],
) -> LayoutLen {
let offered = placement;
let declared = self.declared_lens(child); let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id()); let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align); let (local, placement) = ask_box(region, declared, align, placement);
let within = local.within(&self.region); let first_ask = self.at_offer && !self.offered.contains(&child.id());
let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer = local;
}
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return hint;
} }
let px = self.state.px_of(self.move_idx, within); let px = local.size().to_px(self.px);
let (size, holds) = let retained =
self.state self.state
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?; .retained_size(child.id(), px, placement, self.move_idx, self.rsc.widgets());
let Some((size, holds)) = retained else {
return self
.widget_at_inner(child, region, offered, None, true)
.len(axis);
};
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits); diag::bump(Counter::RetainedSizeHits);
self.depend_on(child); self.depend_on(child);
if first_ask { if first_ask {
self.offered.push(child.id());
let active = self.state.active.get_mut(&child.id()).unwrap(); let active = self.state.active.get_mut(&child.id()).unwrap();
active.answer = (size, holds); active.offer_len = local.size();
active.offer_region = local;
active.offer_placement = placement;
} }
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { let placement = UiRegion {
x: placement[0].unwrap_or(UiSpan::FULL),
y: placement[1].unwrap_or(UiSpan::FULL),
};
let holds = holds.in_frame(placement);
for (axis, under) in AXES.into_iter().zip(self.answer_under.frame.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); *under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
Some(size.axis(axis)) in_parent_frame(size, local.size(), declared).axis(axis)
} }
/// Whether this is the first box a child is asked about in during a draw /// Whether this is the first box a child is asked about in during a draw
@@ -292,15 +461,6 @@ impl<'a> Painter<'a> {
true true
} }
/// The pixel size of a part of the box this widget was asked in.
fn px_within_offer(&self, local: UiRegion) -> PxVec2 {
let size = local.size();
PxVec2::new(
size.x.to_px(self.offered_px.x),
size.y.to_px(self.offered_px.y),
)
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) { if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id()); self.size_deps.push(child.id());
@@ -319,11 +479,16 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// Writes glyphs in the selected frame or extent coordinates.
// TODO: merge the text methods into the primitive ones. // TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into<DrawRegion>) {
let origin = origin.into();
// Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs the frame/extent composition.
let resolved = origin.resolve(self.region, self.placement);
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let mut region = origin; let place = |mut region: UiRegion| {
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::from_px(glyph.offset)); let mut region = region.offset(UiVec2::from_px(glyph.offset));
@@ -333,7 +498,9 @@ impl<'a> Painter<'a> {
); );
region.x.end = region.x.start.offset(size.x); region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y); region.y.end = region.y.start.offset(size.y);
self.write( region
};
self.write_resolved(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
uv_min: glyph.entry.uv_min, uv_min: glyph.entry.uv_min,
@@ -342,17 +509,32 @@ impl<'a> Painter<'a> {
color: text.color, color: text.color,
flags: glyph.entry.flags(), flags: glyph.entry.flags(),
}, },
region, origin.map(place),
place(resolved),
); );
} }
} }
/// This widget's box, in the coordinates its own primitives are written /// The box this widget's parent gave it, in the coordinates its own
/// in -- so a region composed `within` it may be drawn directly. /// primitives are written in -- so a region composed `within` it may be
/// drawn directly. Its own box is [`Self::placement`] of this one.
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
self.region self.region
} }
/// Where this widget's drawing goes inside the box it was given, in that
/// box's coordinates: what its own answer took of it, or what its parent
/// chose for it. `FULL` on the ask that measures, since nothing has been
/// placed yet.
///
/// Reading it is what says the drawing depends on it, so a widget that
/// positions its own content reads it and is drawn again once its box is
/// known, and one that fills whatever it is given never is.
pub fn placement(&mut self) -> UiRegion {
self.reads_placement = true;
self.placement
}
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
/// widget that positions its own content reads it to place that content /// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget. /// the way the box around it would have placed the widget.
@@ -360,17 +542,21 @@ impl<'a> Painter<'a> {
self.rsc.widgets().alignment(self.id) self.rsc.widgets().alignment(self.id)
} }
/// Whether a rule beside this widget settles its length on `axis`, which /// Whether a rule beside this widget gives its length on `axis` outright,
/// makes whatever it reports for that axis moot. The widget under a rule /// which makes whatever it reports for that axis moot. A rule that only
/// does not otherwise learn of it -- this is for a container deciding /// bounds the length is not one of these: the answer is still the
/// whether reading its children across an axis is worth anything, since /// widget's to give, and something still has to work it out.
/// reading one is also what makes its own size depend on it. ///
pub fn ruled(&self, axis: Axis) -> bool { /// The widget under a rule does not otherwise learn of it -- this is for
/// a container deciding whether reading its children across an axis is
/// worth anything, since reading one is also what makes its own size
/// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc self.rsc
.widgets() .widgets()
.size_rules(self.id) .size_rules(self.id)
.axis(axis) .axis(axis)
.known() .exact()
.is_some() .is_some()
} }
@@ -378,25 +564,49 @@ impl<'a> Painter<'a> {
/// near edge. A container that reports one child's size gives every child /// near edge. A container that reports one child's size gives every child
/// this, so what it draws is inside what it says it occupies. /// this, so what it draws is inside what it says it occupies.
pub fn box_of(&self, size: Size) -> UiRegion { pub fn box_of(&self, size: Size) -> UiRegion {
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2]) let lens = placed_lens(size, [None; 2], [false; 2]);
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
} }
/// This widget's box in pixels. Reading it makes the drawing one that /// This widget's own box in pixels. Reading it makes the drawing one
/// holds for this box only, until `holds` says how far it goes. /// that holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 { pub fn px_size(&mut self) -> PxVec2 {
let px = self.state.px_of(self.move_idx, self.region); PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) { }
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.placement.axis(axis).len();
let len = part.to_px(self.px.axis(axis));
let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
} }
} len
px
} }
/// One axis of this widget's box in pixels. Prefer this to /// The lengths of this widget's own box on `axis` that what it is drawing
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// holds for -- the same primitives, in the same fractions and offsets
pub fn px_len(&mut self, axis: Axis) -> Px { /// of the box, and the same reported size. A widget that read its length
let len = self.state.px_of(self.move_idx, self.region).axis(axis); /// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let part = self.placement.axis(axis).len();
let holds = holds.into();
debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.extent_own[axis as usize] = holds;
}
/// One axis of the box this widget's parent gave it, in pixels -- what a
/// fraction of its area resolves against, and so what a container divides
/// among its children. Its own box is a part of this one.
pub fn region_px_len(&mut self, axis: Axis) -> Px {
let len = self.px.axis(axis);
let own = &mut self.own[axis as usize]; let own = &mut self.own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
@@ -404,15 +614,14 @@ impl<'a> Painter<'a> {
len len
} }
/// The lengths of this widget's box on `axis` that what it is drawing /// [`Self::holds`] stated about the region rather than about this
/// holds for -- the same primitives, in the same fractions and offsets /// widget's own box, for a container whose drawing turns on the box it
/// of the box, and the same reported size. A widget that read its /// was given rather than on the part of it it took.
/// length in pixels holds for that one alone until it says otherwise. pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)), holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box", "'{}' ({:?}) says its drawing holds for lengths that leave out its region",
self.label(), self.label(),
self.id self.id
); );
@@ -459,6 +668,7 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>, painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>, child: &'p StrongWidget<W>,
size: Size, size: Size,
answer_holds: LayoutHolds,
} }
impl<W: ?Sized> DrawResult<'_, '_, W> { impl<W: ?Sized> DrawResult<'_, '_, W> {
@@ -469,10 +679,11 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::size_read(self.child.id(), self.painter.id, self.size); diag::size_read(self.child.id(), self.painter.id, self.size);
} }
self.painter.depend_on(self.child); self.painter.depend_on(self.child);
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
self.size self.size
} }
pub fn len(self, axis: Axis) -> Len { pub fn len(self, axis: Axis) -> LayoutLen {
self.size().axis(axis) self.size().axis(axis)
} }
} }
@@ -502,10 +713,25 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// A child's answer as lengths of the parent's own region. A widget reports
/// a fraction of its own region, and `of` is that region as a length of this
/// one. Pixels come through untouched, being that many pixels wherever they
/// end up. A declared axis is already the parent's: it resolved the rule in
/// its own region, and the rule is what the report says.
fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis));
}
}
size
}
/// What a widget declares a length of its box to be. `leftover` is not one: a /// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one, /// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead. /// so it passes up in the size instead.
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<Len>; 2] { pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] {
let rules = widgets.size_rules(id); let rules = widgets.size_rules(id);
let widget = widgets.get_dyn(id); let widget = widgets.get_dyn(id);
AXES.map(|axis| { AXES.map(|axis| {
@@ -522,52 +748,79 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<Len>; 2]
}) })
} }
/// The box a drawing occupies: the size the widget reported, on the side of /// Whether what a widget reported along an axis is the whole of the box it
/// the box it was asked in that its alignment says. An axis reported as a /// is in rather than a part to be placed inside it. A share fills, because a
/// share fills, because a share is a length only to whoever divides one, and /// share is a length only to whoever divides one, and whoever did is the one
/// whoever did is the one that handed down this box. A declared axis is /// that handed down this box. A declared axis does too: the rule already gave
/// left alone too: `declared_box` already placed it, in the parent's box, /// the region its length, and the rule's length is what the widget reports
/// and the rule's length is what the widget reports there. /// there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// What of the box it was given a widget's drawing occupies, as lengths of
/// that box: the size it reported wherever that is a part to be placed, and
/// the whole of the box wherever the answer fills it.
/// ///
/// A reported fraction is a fraction of the box the widget drew in, where a /// A reported fraction is a fraction of the box the widget drew in, where a
/// declared one is a fraction of the box its parent handed down -- a span /// declared one is a fraction of the box its parent handed down -- a span
/// reporting `rel(1.0)` means all of what it was given, whatever that was a /// reporting `rel(1.0)` means all of what it was given, whatever that was a
/// fraction of. So this scales by the box rather than composing into it. /// fraction of. So this is a length of the box rather than a length composed
pub(crate) fn placed_box( /// into it, and a box in pixels is this step from the given box's pixels.
region: UiRegion, pub(crate) fn placed_lens(
size: Size, size: Size,
align: RegionAlign, declared: [Option<LayoutLen>; 2],
declared: [Option<Len>; 2], decided: [bool; 2],
) -> UiRegion { ) -> UiVec2 {
let mut placed = region; let mut lens = UiVec2::FULL_SIZE;
for (axis, declared) in AXES.into_iter().zip(declared) { for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
let reported = size.axis(axis); let reported = size.axis(axis);
if reported.leftover != Weight::ZERO || declared.is_some() { if !fills(reported, declared, decided) {
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
}
}
lens
}
/// Where that drawing sits: those lengths taken of the box the widget was
/// asked in, on the side of it that the widget's alignment says.
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
let mut placed = region;
for axis in AXES {
// The whole of the box is already where it sits, and the arithmetic
// below is the identity for it.
if lens.axis(axis) == Len::FULL {
continue; continue;
} }
let span = placed.axis_mut(axis); let span = placed.axis_mut(axis);
let len = span.len().scale(reported.rel) + UiScalar::from_parts(Rel::ZERO, reported.px); let len = lens.axis(axis).within_len(span.len());
span.start += (span.len() - len).scale(align.axis(axis).rel()); span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len; span.end = span.start + len;
} }
placed placed
} }
/// Takes a widget's declared lengths in the box `region` is given in, since a /// A declared axis gets a frame of that length, aligned within the parent's
/// fraction of a length means a fraction of that one, and puts what is left /// slot (or the offer). Undeclared axes keep the offered frame and chosen
/// over on the side its alignment says. A caller that already reserved the /// placement, so their reported fractions retain that reference.
/// space hands back the same length, so this is the identity for it. pub(crate) fn ask_box(
pub(crate) fn declared_box(
mut region: UiRegion, mut region: UiRegion,
declared: [Option<Len>; 2], declared: [Option<LayoutLen>; 2],
align: RegionAlign, align: RegionAlign,
) -> UiRegion { placement: [Option<UiSpan>; 2],
for (axis, len) in AXES.into_iter().zip(declared) { ) -> (UiRegion, [Option<UiSpan>; 2]) {
let Some(len) = len else { continue }; let mut placed = [None; 2];
for (axis, (len, chosen)) in AXES.into_iter().zip(declared.into_iter().zip(placement)) {
let Some(len) = len else {
placed[axis as usize] = chosen;
continue;
};
let span = region.axis_mut(axis); let span = region.axis_mut(axis);
let len = UiScalar::from_parts(len.rel, len.px); let len = Len::from_parts(len.rel, len.px);
span.start += (span.len() - len).scale(align.axis(axis).rel()); let slot = chosen.unwrap_or(*span);
span.start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len; span.end = span.start + len;
} }
region (region, placed)
} }
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,4 +1,4 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SlotId { pub struct SlotId {
idx: u32, idx: u32,
genr: u32, genr: u32,
+4 -4
View File
@@ -1,4 +1,4 @@
use crate::{Axis, Len, Painter, Size}; use crate::{Axis, LayoutLen, Painter, Size};
use std::any::Any; use std::any::Any;
mod data; mod data;
@@ -24,7 +24,7 @@ pub trait Widget: Any {
/// An exact length the widget can give without a painter or its children. /// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that /// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion. /// disagrees with the eventual draw fails a debug assertion.
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
None None
} }
} }
@@ -35,8 +35,8 @@ impl Widget for () {
Size::default() Size::default()
} }
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
Some(Len::default()) Some(LayoutLen::default())
} }
} }
+14 -13
View File
@@ -1,4 +1,4 @@
use crate::{Axis, Len, Weight}; use crate::{Axis, LayoutLen, Weight};
/// What a widget's length on one axis is, as a rule its parent applies where /// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself. /// it draws it rather than an answer the widget gives about itself.
@@ -14,7 +14,7 @@ pub enum SizeRule {
#[default] #[default]
Free, Free,
/// This length, whatever the widget reports. /// This length, whatever the widget reports.
Exact(Len), Exact(LayoutLen),
} }
impl SizeRule { impl SizeRule {
@@ -22,18 +22,19 @@ impl SizeRule {
/// give one. `leftover` is never among them: a share is a length only to /// give one. `leftover` is never among them: a share is a length only to
/// whoever divides one, so it passes up in the reported size instead and /// whoever divides one, so it passes up in the reported size instead and
/// is resolved there. /// is resolved there.
pub fn declared(&self) -> Option<Len> { pub fn declared(&self) -> Option<LayoutLen> {
match self { match self {
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len), Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
_ => None, _ => None,
} }
} }
/// The length this rule fixes, whether or not it can narrow a box. A /// The length this rule gives outright, whatever the widget reports --
/// share is a length the widget's parent still has to divide, so it is /// which makes the widget's answer on that axis moot. A share counts: it
/// known here and resolved there -- unlike `declared`, which is only the /// is a length the widget's parent still has to divide, so it is exact
/// ones that give a box directly. /// here and resolved there, unlike `declared`, which is only the ones
pub fn known(&self) -> Option<Len> { /// that give a box directly.
pub fn exact(&self) -> Option<LayoutLen> {
match self { match self {
Self::Free => None, Self::Free => None,
Self::Exact(len) => Some(*len), Self::Exact(len) => Some(*len),
@@ -41,7 +42,7 @@ impl SizeRule {
} }
/// The length a widget reporting `reported` ends up with. /// The length a widget reporting `reported` ends up with.
pub fn apply(&self, reported: Len) -> Len { pub fn apply(&self, reported: LayoutLen) -> LayoutLen {
match self { match self {
Self::Free => reported, Self::Free => reported,
Self::Exact(len) => *len, Self::Exact(len) => *len,
@@ -49,14 +50,14 @@ impl SizeRule {
} }
} }
impl From<Len> for SizeRule { impl From<LayoutLen> for SizeRule {
fn from(len: Len) -> Self { fn from(len: LayoutLen) -> Self {
Self::Exact(len) Self::Exact(len)
} }
} }
impl From<Option<Len>> for SizeRule { impl From<Option<LayoutLen>> for SizeRule {
fn from(len: Option<Len>) -> Self { fn from(len: Option<LayoutLen>) -> Self {
len.map_or(Self::Free, Self::Exact) len.map_or(Self::Free, Self::Exact)
} }
} }
+5 -1
View File
@@ -20,10 +20,14 @@ impl DefaultAppState for Client {
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
( (
// The square is one widget and the two shares of the row it
// sits centred in are another: a length is a property of a
// widget, so `.width` here would overwrite the `.sized`.
rrect rrect
.color(Color::RED) .color(Color::RED)
.sized((100, 100)) .sized((100, 100))
.center() .center()
.wrapper()
.width(leftover(2)), .width(leftover(2)),
( (
rrect.color(Color::ORANGE), rrect.color(Color::ORANGE),
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(rsc);
let main = WidgetPtr::new().add(rsc); let main = Wrapper::new().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new()))); let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| { let mut switch_button = |color, to: WeakWidget, label| {
+7 -3
View File
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
.pad(16) .pad(16)
.background(panel()); .background(panel());
// Each one takes the whole width, because `text_align` puts the
// glyphs somewhere in the box the text is given and a text that
// reports the width of its own glyphs is given exactly that.
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
let aligned = ( let aligned = (
wtext("left").size(24).text_align(Align::LEFT), label("left", Align::LEFT),
wtext("centred").size(24).text_align(Align::CENTER), label("centred", Align::H_CENTER),
wtext("right").size(24).text_align(Align::RIGHT), label("right", Align::RIGHT),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.gap(8) .gap(8)
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height)); render.resize((size.width, size.height), rsc.widgets_mut());
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
+4 -4
View File
@@ -144,9 +144,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test // bound that comes with `SyncSender` is far past anything a test
// leaves unread. // leaves unread.
let (send, updates) = sync_channel(1024); let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send))); let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize(size); render.resize(size, rsc.widgets_mut());
Self { Self {
rsc, rsc,
render, render,
@@ -161,11 +161,11 @@ impl Harness {
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size); self.render.resize(size, self.rsc.widgets_mut());
} }
/// Changes a length rule after the fact, the way `.width()` sets one. /// Changes a length rule after the fact, the way `.width()` sets one.
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<Len>) { pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
self.rsc self.rsc
.widgets_mut() .widgets_mut()
.set_size_rule(id, axis, SizeRule::Exact(len.into())); .set_size_rule(id, axis, SizeRule::Exact(len.into()));
+698 -159
View File
@@ -9,7 +9,7 @@ use crate::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
/// The declared lengths of one widget carrying a size rule, by axis. /// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<Len>; 2]; pub type Lens = [Option<LayoutLen>; 2];
/// Where one widget carrying an alignment sits, by axis. `None` uses the /// Where one widget carrying an alignment sits, by axis. `None` uses the
/// centered default. /// centered default.
@@ -29,6 +29,17 @@ pub struct Edits {
/// one. Region nodes change what a move writes and how deep a primitive's /// one. Region nodes change what a move writes and how deep a primitive's
/// chain is, so a tree that never grows one leaves both untested. /// chain is, so a tree that never grows one leaves both untested.
pub nodes: HashMap<usize, bool>, pub nodes: HashMap<usize, bool>,
/// Whether a [`Branch`] takes the side it would take at any measurement,
/// rather than the side the one it made says. The oracle wants the
/// measured side -- that is the whole point of a branch, and how a widget
/// believing a measurement a cold start would not have given it becomes a
/// different tree. A rig measuring cost wants this instead: a fixture
/// whose shape moves with the thing being measured cannot be compared
/// with itself across a change to it, and seed 1 at depth 8 went from 88
/// drawn widgets and 2,298 primitive writes a frame to 115 and 8,209
/// across fixed point, which is three and a half times the work behind a
/// number read as three and a half times the cost.
pub fixed_branches: bool,
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
@@ -90,10 +101,6 @@ pub struct Tree {
pub nodes: Vec<WidgetId>, pub nodes: Vec<WidgetId>,
pub spans: Vec<Spanned>, pub spans: Vec<Spanned>,
pub scrolls: Vec<WeakWidget<Scroll>>, pub scrolls: Vec<WeakWidget<Scroll>>,
/// Children a `SpanEdit` took out, held so that dropping the last share
/// of one does not free its id for the next widget to be given -- which
/// would put the two trees' `ids` out of step.
pub detached: Vec<StrongWidget>,
} }
/// Branches on a child's measured length. Comparing boxes catches a widget /// Branches on a child's measured length. Comparing boxes catches a widget
@@ -127,15 +134,463 @@ impl Widget for Branch {
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 /// Everything made for this span that it does not hold -- spares never
/// trees make the same widgets in the same order either way. Attaching /// attached and children detached alike. A widget belongs to one parent,
/// one moves it out of here: a widget belongs to one parent, and one that /// and one that belongs to nobody still has to be held here: dropping
/// belongs to nobody still has to be held or it reads as a leak. /// the last share of it frees its id for the next widget to be given,
/// which puts two trees out of step.
pub spares: Vec<StrongWidget>, pub spares: Vec<StrongWidget>,
/// How many children it was grown with, before any edit. /// How many children it was grown with, before any edit.
pub grown: usize, pub grown: usize,
} }
/// A tree described rather than built: [`plan`] turns a seed into one of
/// these and [`build`] turns it into widgets, where growing did both at once.
///
/// The split is what makes a counterexample readable. A failing seed used to
/// be the entire record of one, because a grower that makes widgets as it
/// draws leaves nothing to take apart -- a shrinker could only grow its own
/// trees and hope to meet the same shape, which in practice it does not. A
/// plan is reduced by [`Plan::smaller`] and built again, so any seed that
/// fails can be cut down until what is left is small enough to read.
#[derive(Clone, Debug, PartialEq)]
pub struct Plan {
pub kind: Kind,
/// The declared size this widget carries. Whoever grows a widget offers
/// it one and the offer is taken or declined; a second offer to the same
/// widget is dropped, because two rules on one widget would settle in the
/// order they were applied rather than in grow order.
pub size: Option<Lens>,
/// The alignment it carries, under the same one-offer rule.
pub align: Option<Aligns>,
/// Whether it was offered a movable region of its own and what it
/// answered. `Some(false)` is an offer declined, which still uses up the
/// one offer, where `None` is an offer never made.
pub region_node: Option<bool>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Kind {
/// Wrapped and unwrapped text, because only one of them reads the width
/// it is given and so only one has to be drawn again for a new one.
Wrapped,
OneLine,
Rect {
color: usize,
alpha: u8,
},
/// Scrolling reads the pixel length of its box, which nothing else here
/// does, and gives its child a box longer than its own.
Scroll {
axis: Axis,
inner: Box<Plan>,
},
/// All three sides are grown either way, so a tree that draws one has the
/// same ids as a tree that draws another.
Branch {
probe: Box<Plan>,
wide: Box<Plan>,
narrow: Box<Plan>,
threshold: f32,
},
/// Each side its own, since a padding that is the same all round hides
/// anything that treats one edge differently from another.
Pad {
padding: [i32; 4],
inner: Box<Plan>,
},
Stack {
children: Vec<Plan>,
},
Span {
dir: usize,
gap: i32,
/// Grown for this span, in the order they are made.
children: Vec<Plan>,
/// Grown beside it whether or not they end up in it, so the widget
/// after them has the same id in a tree that leaves them out as in
/// one that puts them in.
spares: Vec<Plan>,
/// Which of `children` then `spares` are actually in the span, and
/// in what order -- kept apart from the two lists above so that a
/// tree which detaches, attaches or reorders its children still
/// makes the same widgets in the same order, and two builds line up
/// index for index. Anything not named here is built and held
/// rather than dropped, since freeing an id hands it to the next
/// widget and puts two trees out of step.
order: Vec<usize>,
},
}
impl Plan {
/// A widget carrying nothing anybody has offered it yet.
fn bare(kind: Kind) -> Self {
Self {
kind,
size: None,
align: None,
region_node: None,
}
}
/// How many widgets building it makes, spares and detached children
/// included, since those are made either way.
pub fn size(&self) -> usize {
1 + match &self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.size(),
Kind::Branch {
probe,
wide,
narrow,
..
} => probe.size() + wide.size() + narrow.size(),
Kind::Stack { children } => children.iter().map(Plan::size).sum(),
Kind::Span {
children, spares, ..
} => children.iter().chain(spares).map(Plan::size).sum(),
_ => 0,
}
}
/// The trees to try instead of this one when reducing a counterexample,
/// biggest cut first: a shrinker takes the first that still fails, so
/// offering "this subtree alone" before "this subtree with one child
/// fewer" is what gets from six hundred widgets to six rather than to
/// five hundred and ninety.
///
/// Every one of these is a tree the generator could have grown, so a
/// reduced plan is a counterexample in its own right rather than a
/// special case only the shrinker can make.
pub fn smaller(&self) -> Vec<Plan> {
let mut out = Vec::new();
// Standing in for the whole of it, which is the largest cut there is.
for kid in self.kids() {
out.push(kid.clone());
}
// Then what it carries, which costs nothing to put back if it was
// not the thing that mattered.
for dropped in [
self.region_node.map(|_| Plan {
region_node: None,
..self.clone()
}),
self.align.map(|_| Plan {
align: None,
..self.clone()
}),
self.size.map(|_| Plan {
size: None,
..self.clone()
}),
]
.into_iter()
.flatten()
{
out.push(dropped);
}
out.extend(self.kind.smaller().into_iter().map(|kind| Plan {
kind,
..self.clone()
}));
out
}
/// Visits every widget in the order [`build`] makes them, so a count
/// kept by the visitor indexes the same widget as the matching [`Tree`]
/// vector does.
pub fn walk_mut(&mut self, at: &mut impl FnMut(&mut Plan)) {
match &mut self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.walk_mut(at),
Kind::Branch {
probe,
wide,
narrow,
..
} => {
probe.walk_mut(at);
wide.walk_mut(at);
narrow.walk_mut(at);
}
Kind::Stack { children } => {
for child in children {
child.walk_mut(at);
}
}
Kind::Span {
children, spares, ..
} => {
for child in children.iter_mut().chain(spares) {
child.walk_mut(at);
}
}
_ => {}
}
at(self);
}
/// The same tree with `edits` applied, by the indices the generator would
/// have used for them.
///
/// [`plan`] resolves edits while drawing, which needs a seed. A scenario
/// needs them applied to a tree that already exists -- one it has built,
/// and one a shrinker may already have cut down, where no seed grows it
/// any more. Both routes take the same [`Edits`], so a case written
/// against one reads the same against the other.
pub fn edited(&self, edits: &Edits) -> Plan {
let mut out = self.clone();
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
out.walk_mut(&mut |plan| {
if let Kind::Span {
children,
spares,
order,
..
} = &mut plan.kind
{
if let Some(edit) = edits.spans.get(&spans) {
*order = span_edited(order, children.len(), spares.len(), edit);
}
spans += 1;
}
if let Kind::Branch { threshold, .. } = &mut plan.kind
&& edits.fixed_branches
{
*threshold = f32::MIN;
}
if plan.size.is_some() {
if let Some(lens) = edits.sizes.get(&sized) {
plan.size = Some(*lens);
}
sized += 1;
}
if plan.align.is_some() {
if let Some(align) = edits.aligns.get(&aligned) {
plan.align = Some(*align);
}
aligned += 1;
}
if plan.region_node.is_some() {
if let Some(take) = edits.nodes.get(&nodes) {
plan.region_node = Some(*take);
}
nodes += 1;
}
});
out
}
fn kids(&self) -> Vec<&Plan> {
match &self.kind {
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => vec![inner],
Kind::Branch {
probe,
wide,
narrow,
..
} => vec![probe, wide, narrow],
Kind::Stack { children } => children.iter().collect(),
Kind::Span { children, .. } => children.iter().collect(),
_ => Vec::new(),
}
}
}
impl Kind {
/// Simplifications of the shape alone, leaving what the widget carries to
/// [`Plan::smaller`]. Replacing a node with one of its children is there
/// rather than here, since it answers with a whole `Plan`.
fn smaller(&self) -> Vec<Kind> {
let mut out = Vec::new();
/// One child reduced at a time, rebuilt into the same shape. Every
/// answer has the same number of children as it was given, so it is
/// for the shapes whose child count is part of what they are.
fn reduced(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
let mut out = Vec::new();
for (i, kid) in kids.iter().enumerate() {
for small in kid.smaller() {
let mut next = kids.to_vec();
next[i] = small;
out.push(rebuild(next));
}
}
out
}
/// One child dropped, then [`reduced`]. For the shapes that hold any
/// number of children, where dropping one is the cut that matters.
fn each(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
let mut out = Vec::new();
for i in 0..kids.len() {
if kids.len() > 1 {
let mut less = kids.to_vec();
less.remove(i);
out.push(rebuild(less));
}
}
out.extend(reduced(kids, rebuild));
out
}
match self {
// The one leaf that reads the width it is given, then the one
// that does not, then the one that measures nothing at all.
Kind::Wrapped => out.push(Kind::OneLine),
Kind::OneLine => out.push(Kind::Rect {
color: 0,
alpha: 255,
}),
Kind::Rect { .. } => {}
Kind::Scroll { axis, inner } => {
let axis = *axis;
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Scroll {
axis,
inner: Box::new(k.remove(0)),
}));
}
Kind::Branch {
probe,
wide,
narrow,
threshold,
} => {
let threshold = *threshold;
// All three sides stay: a branch is the widget that draws
// one of two on a measurement, and one with a side missing
// is a different widget rather than a smaller one. Dropping
// the branch for a side is offered by `Plan::smaller`.
let sides = [(**probe).clone(), (**wide).clone(), (**narrow).clone()];
out.extend(reduced(&sides, &|k| Kind::Branch {
probe: Box::new(k[0].clone()),
wide: Box::new(k[1].clone()),
narrow: Box::new(k[2].clone()),
threshold,
}));
}
Kind::Pad { padding, inner } => {
let padding = *padding;
if padding != [0; 4] {
out.push(Kind::Pad {
padding: [0; 4],
inner: inner.clone(),
});
}
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Pad {
padding,
inner: Box::new(k.remove(0)),
}));
}
Kind::Stack { children } => {
out.extend(each(children, &|children| Kind::Stack { children }))
}
Kind::Span {
dir,
gap,
children,
spares,
order,
} => {
let (dir, gap, n) = (*dir, *gap, children.len());
let span = |children: Vec<Plan>, spares: Vec<Plan>, order: Vec<usize>| Kind::Span {
dir,
gap,
children,
spares,
order,
};
let identity: Vec<usize> = (0..n).collect();
// An order the generator did not choose is part of the tree,
// so take that off before taking the tree apart.
if *order != identity {
out.push(span(children.clone(), spares.clone(), identity));
}
// Spares exist to be attached; with none attached they are
// widgets the span never holds.
if !spares.is_empty() && order.iter().all(|&i| i < n) {
out.push(span(children.clone(), Vec::new(), order.clone()));
}
if gap != 0 {
out.push(Kind::Span {
dir,
gap: 0,
children: children.clone(),
spares: spares.clone(),
order: order.clone(),
});
}
for k in 0..n {
if n > 1 {
let mut less = children.clone();
less.remove(k);
// Everything after it shifts down, spares included,
// since they are indexed past the children.
let order = order
.iter()
.filter(|&&i| i != k)
.map(|&i| if i > k { i - 1 } else { i })
.collect();
out.push(span(less, spares.clone(), order));
}
}
for (i, kid) in children.iter().enumerate() {
for small in kid.smaller() {
let mut next = children.clone();
next[i] = small;
out.push(span(next, spares.clone(), order.clone()));
}
}
}
}
out
}
}
/// A [`SpanEdit`] applied to the order a span already holds its children in.
///
/// `detach` names positions in that order and `attach` takes from the front
/// of what the span is not holding, both of which is what a test changing a
/// live span does -- so an edit means the same thing said to a tree and said
/// to the plan it was built from. On a span nobody has edited the order is
/// the children in the order they were grown, and this is then "leave these
/// out and put that many spares on the end".
fn span_edited(order: &[usize], children: usize, spares: usize, edit: &SpanEdit) -> Vec<usize> {
let mut detach = edit.detach.clone();
detach.sort_unstable();
detach.dedup();
let mut next: Vec<usize> = order
.iter()
.enumerate()
.filter(|(at, _)| !detach.contains(at))
.map(|(_, &which)| which)
.collect();
// What the span is not holding, in the order it hands them back: what it
// was already not holding first, in the order the widgets were made, and
// what this edit takes out after that, highest position first. A child
// just detached goes to the back rather than straight back in, which is
// what makes detaching one and attaching one a trade.
let mut free: Vec<usize> = (0..children + spares)
.filter(|i| !order.contains(i))
.collect();
free.extend(detach.iter().rev().filter_map(|&at| order.get(at).copied()));
next.extend(free.into_iter().take(edit.attach));
next
}
/// Plans the tree `seed` describes, `edits` replacing what it would otherwise
/// have given the widgets that carry them.
///
/// The edits are resolved here rather than at build time, so that a plan is
/// the whole of what a tree is and building one has nothing left to decide.
pub fn plan(seed: u64, depth: usize, edits: &Edits) -> Plan {
let mut sow = Sow {
rng: Rng::new(seed),
edits,
sized: 0,
aligned: 0,
nodes: 0,
spans: 0,
};
sow.node(depth)
}
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it /// Grows the tree `seed` describes, `edits` replacing the declared sizes it
/// would otherwise have given those wrappers. /// would otherwise have given those wrappers.
pub fn grow<Rsc: UiRsc + 'static>( pub fn grow<Rsc: UiRsc + 'static>(
@@ -144,233 +599,317 @@ pub fn grow<Rsc: UiRsc + 'static>(
depth: usize, depth: usize,
edits: &Edits, edits: &Edits,
) -> (StrongWidget, Tree) { ) -> (StrongWidget, Tree) {
let mut grow = Grow { build(rsc, &plan(seed, depth, edits))
rsc,
rng: Rng::new(seed),
tree: Tree::default(),
edits,
};
let root = grow.node(depth);
(root, grow.tree)
} }
struct Grow<'a, Rsc> { /// Draws a plan out of the random stream. Every draw happens in the order it
rsc: &'a mut Rsc, /// always has and before the decision it feeds, including the decisions that
/// are then dropped, because a seed has to keep meaning the same tree.
struct Sow<'a> {
rng: Rng, rng: Rng,
tree: Tree,
edits: &'a Edits, edits: &'a Edits,
sized: usize,
aligned: usize,
nodes: usize,
spans: usize,
} }
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> { impl Sow<'_> {
fn leaf(&mut self) -> StrongWidget { fn leaf(&mut self) -> Plan {
let id: StrongWidget = match self.rng.below(4) { Plan::bare(match self.rng.below(4) {
// Wrapped and unwrapped, because only one of them reads the width 0 => Kind::Wrapped,
// it is given and so only one has to be drawn again for a new one. 1 => Kind::OneLine,
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
1 => wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(self.rsc),
_ => { _ => {
let color = COLORS[self.rng.below(COLORS.len())]; let color = self.rng.below(COLORS.len());
let alpha = (self.rng.below(5) * 63) as u8; let alpha = (self.rng.below(5) * 63) as u8;
rect(color.alpha(alpha)).add_strong(self.rsc) Kind::Rect { color, alpha }
} }
}; })
self.tree.ids.push(id.id());
id
} }
fn len(&mut self) -> Option<Len> { fn len(&mut self) -> Option<LayoutLen> {
match self.rng.below(4) { match self.rng.below(4) {
0 => Some(Len::px(20.0 + self.rng.below(180) as f32)), 0 => Some(LayoutLen::px(20.0 + self.rng.below(180) as f32)),
1 => Some(Len::LEFTOVER), 1 => Some(LayoutLen::LEFTOVER),
_ => None, _ => None,
} }
} }
fn align(&mut self) -> Align { fn align(&mut self) -> Aligns {
let mut axis = || match self.rng.below(4) { let axis = |s: &mut Self| match s.rng.below(4) {
0 => None, 0 => None,
1 => Some(AxisAlign::NEG), 1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER), 2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS), _ => Some(AxisAlign::POS),
}; };
let (mut x, y) = (axis(), axis()); let (x, y) = (axis(self), axis(self));
// Aligning on neither axis leaves the branch unexercised. // Aligning on neither axis leaves the branch unexercised.
if x.is_none() && y.is_none() { match x.is_none() && y.is_none() {
x = Some(AxisAlign::CENTER); true => [Some(AxisAlign::CENTER), y],
false => [x, y],
} }
Align { x, y }
} }
/// A declared size over half the tree, kept where a test can change it. /// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: StrongWidget) -> StrongWidget { fn sized(&mut self, inner: &mut Plan) {
// A rule is a property now, so a node already carrying one would take
// a second entry in `sized` -- and two edits naming one widget settle
// in the order they are applied, which is grow order cold and edit
// order warm. One entry per widget instead. Both draws are taken
// whatever is decided, and the decision is grow order alone, so the
// two trees consume the same random stream.
let take = self.rng.chance(); let take = self.rng.chance();
let lens = [self.len(), self.len()]; let lens = [self.len(), self.len()];
if !take || self.tree.sized.contains(&inner.id()) { if !take || inner.size.is_some() {
return inner; return;
} }
let idx = self.tree.sized.len(); let idx = self.sized;
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens); self.sized += 1;
let id = inner.id(); inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens));
self.rsc
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.tree.sized.push(id);
inner
} }
/// An alignment over some of the tree, kept where a test can change it. /// An alignment over some of the tree, kept where a test can change it.
/// One entry per widget for the reason `sized` gives. fn aligned(&mut self, inner: &mut Plan) {
fn aligned(&mut self, inner: StrongWidget) -> StrongWidget {
let align = self.align(); let align = self.align();
let align = [align.x, align.y]; if inner.align.is_some() {
if self.tree.aligned.contains(&inner.id()) { return;
return inner;
} }
let idx = self.tree.aligned.len(); let idx = self.aligned;
let align = self.edits.aligns.get(&idx).copied().unwrap_or(align); self.aligned += 1;
let id = inner.id(); inner.align = Some(self.edits.aligns.get(&idx).copied().unwrap_or(align));
let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
widgets.set_alignment(id, axis, align.unwrap_or_default());
}
self.tree.aligned.push(id);
inner
} }
/// A movable region of its own over some of the tree. What it changes is /// A movable region of its own over some of the tree. What it changes is
/// how a move is written and how long a primitive's chain is, neither of /// how a move is written and how long a primitive's chain is, neither of
/// which any other branch here varies. /// which any other branch here varies.
fn noded(&mut self, inner: StrongWidget) -> StrongWidget { fn noded(&mut self, inner: &mut Plan) {
let take = self.rng.below(4) == 0; let take = self.rng.below(4) == 0;
if self.tree.nodes.contains(&inner.id()) { if inner.region_node.is_some() {
return inner; return;
} }
let idx = self.tree.nodes.len(); let idx = self.nodes;
let take = self.edits.nodes.get(&idx).copied().unwrap_or(take); self.nodes += 1;
let id = inner.id(); inner.region_node = Some(self.edits.nodes.get(&idx).copied().unwrap_or(take));
self.rsc.ui_mut().widgets.set_region_node(id, take);
self.tree.nodes.push(id);
inner
} }
fn node(&mut self, depth: usize) -> StrongWidget { fn offered(&mut self, inner: &mut Plan) {
self.sized(inner);
self.noded(inner);
}
fn node(&mut self, depth: usize) -> Plan {
if depth == 0 { if depth == 0 {
return self.leaf(); return self.leaf();
} }
let positioned = self.rng.below(6); let positioned = self.rng.below(6);
if positioned == 0 { if positioned == 0 {
// Scrolling reads the pixel length of its box, which nothing let mut inner = self.node(depth - 1);
// else here does, and gives its child a box longer than its own. self.offered(&mut inner);
let inner = self.node(depth - 1);
let inner = self.sized(inner);
let inner = self.noded(inner);
let axis = if self.rng.chance() { Axis::X } else { Axis::Y }; let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
let id = Scroll::new(inner, axis).add(self.rsc); return Plan::bare(Kind::Scroll {
self.tree.scrolls.push(id); axis,
self.tree.ids.push(id.id()); inner: Box::new(inner),
return id.add_strong(self.rsc); });
} }
if positioned == 2 { 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 probe = self.node(depth - 1);
let wide = self.node(depth - 1); let wide = self.node(depth - 1);
let narrow = self.node(depth - 1); let narrow = self.node(depth - 1);
let threshold = self.rng.below(500) as f32; // Drawn either way, so the side a fixed branch takes is still a
let id = Branch { // side the generator chose -- and it consumes the same randomness
probe, // as a measured one, so the two grow the same ids.
wide, let measured = self.rng.below(500) as f32;
narrow, let threshold = match self.edits.fixed_branches {
true => f32::MIN,
false => measured,
};
return Plan::bare(Kind::Branch {
probe: Box::new(probe),
wide: Box::new(wide),
narrow: Box::new(narrow),
threshold, 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); // Carries an alignment and makes no widget of its own, so the
let inner = self.sized(inner); // plan for it is the child it aligned.
let inner = self.noded(inner); let mut inner = self.node(depth - 1);
return self.aligned(inner); self.offered(&mut inner);
self.aligned(&mut inner);
return inner;
} }
if self.rng.below(4) == 0 { if self.rng.below(4) == 0 {
let inner = self.node(depth - 1); let mut inner = self.node(depth - 1);
let inner = self.sized(inner); self.offered(&mut inner);
let inner = self.noded(inner); let side = |s: &mut Self| s.rng.below(24) as i32;
// Each side its own, since a padding that is the same all round let padding = [side(self), side(self), side(self), side(self)];
// hides anything that treats one edge differently from another. return Plan::bare(Kind::Pad {
let mut side = || Px::from_int(self.rng.below(24) as i32); padding,
let padding = Padding { inner: Box::new(inner),
left: side(), });
right: side(),
top: side(),
bottom: side(),
};
let id = Pad { padding, inner }.add_strong(self.rsc);
self.tree.ids.push(id.id());
return id;
} }
let grown = 2 + self.rng.below(3); let grown = 2 + self.rng.below(3);
let mut children = Vec::with_capacity(grown); let mut children = Vec::with_capacity(grown);
for _ in 0..grown { for _ in 0..grown {
let child = self.node(depth - 1); let mut child = self.node(depth - 1);
let child = self.sized(child); self.offered(&mut child);
let child = self.noded(child);
children.push(child); children.push(child);
} }
if self.rng.chance() { if self.rng.chance() {
let id = Stack { return Plan::bare(Kind::Stack { children });
children,
size: StackSize::Child(0),
} }
.add_strong(self.rsc); let spares: Vec<Plan> = (0..SPARES).map(|_| self.leaf()).collect();
self.tree.ids.push(id.id()); let idx = self.spans;
return id; self.spans += 1;
}
// Grown either way, so the widget after them has the same id in a
// tree that leaves them out as in one that puts them in.
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
let idx = self.tree.spans.len();
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default(); let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
// Highest first, so an index means the same child however many of its let dir = self.rng.below(4);
// neighbours are going too.
let mut detach = edit.detach.clone();
detach.sort_unstable();
for j in detach.into_iter().rev() {
if j < children.len() {
self.tree.detached.push(children.remove(j));
}
}
let attach = edit.attach.min(spares.len());
children.extend(spares.drain(..attach));
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
let id = Span {
children,
dir,
gap: Px::from_int(self.rng.below(3) as i32 * 4),
}
.add(self.rsc);
// A row takes the height it is given rather than its tallest child, // A row takes the height it is given rather than its tallest child,
// which is a rule beside it. Derived from an existing choice and // which is a rule beside it. Derived from an existing choice and
// consuming no randomness: a seed must keep growing the same tree // consuming no randomness: a seed must keep growing the same tree
// when the generator gains another configuration. // when the generator gains another configuration.
let gap = self.rng.below(3) as i32 * 4;
let grown: Vec<usize> = (0..children.len()).collect();
let order = span_edited(&grown, children.len(), spares.len(), &edit);
Plan::bare(Kind::Span {
dir,
gap,
children,
spares,
order,
})
}
}
/// Builds a plan's widgets in the order it describes them, so two builds of
/// one plan line up index for index and their boxes can be compared.
pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget, Tree) {
let mut build = Build {
rsc,
tree: Tree::default(),
};
let root = build.node(plan);
(root, build.tree)
}
struct Build<'a, Rsc> {
rsc: &'a mut Rsc,
tree: Tree,
}
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
fn node(&mut self, plan: &Plan) -> StrongWidget {
let built = self.kind(&plan.kind);
let id = built.id();
if let Some(lens) = plan.size {
self.rsc
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.tree.sized.push(id);
}
if let Some(align) = plan.align {
let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
widgets.set_alignment(id, axis, align.unwrap_or_default());
}
self.tree.aligned.push(id);
}
if let Some(take) = plan.region_node {
self.rsc.ui_mut().widgets.set_region_node(id, take);
self.tree.nodes.push(id);
}
built
}
fn kind(&mut self, kind: &Kind) -> StrongWidget {
let id: StrongWidget = match kind {
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
Kind::OneLine => wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(self.rsc),
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
Kind::Scroll { axis, inner } => {
let inner = self.node(inner);
let id = Scroll::new(inner, *axis).add(self.rsc);
self.tree.scrolls.push(id);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
Kind::Branch {
probe,
wide,
narrow,
threshold,
} => {
let probe = self.node(probe);
let wide = self.node(wide);
let narrow = self.node(narrow);
let id = Branch {
probe,
wide,
narrow,
threshold: *threshold,
}
.add(self.rsc);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
Kind::Pad { padding, inner } => {
let inner = self.node(inner);
let [left, right, top, bottom] = padding.map(Px::from_int);
let padding = Padding {
left,
right,
top,
bottom,
};
Pad { padding, inner }.add_strong(self.rsc)
}
Kind::Stack { children } => {
let children = children.iter().map(|c| self.node(c)).collect();
Stack {
children,
size: StackSize::Child(0),
}
.add_strong(self.rsc)
}
Kind::Span {
dir,
gap,
children,
spares,
order,
} => {
let grown = children.len();
// Every one of them is made, in this order, whether or not
// the span ends up holding it.
let made: Vec<StrongWidget> = children
.iter()
.chain(spares)
.map(|c| self.node(c))
.collect();
let mut left: Vec<Option<StrongWidget>> = made.into_iter().map(Some).collect();
let children: Vec<StrongWidget> = order
.iter()
.filter_map(|&i| left.get_mut(i).and_then(Option::take))
.collect();
// What the span does not hold is still held here: dropping
// the last share of a widget frees its id for the next one
// to be given, which puts two trees out of step.
let spares: Vec<StrongWidget> = left.into_iter().flatten().collect();
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][*dir % 4];
let id = Span {
children,
dir,
gap: Px::from_int(*gap),
}
.add(self.rsc);
if dir.axis == Axis::X { if dir.axis == Axis::X {
self.rsc self.rsc
.widgets_mut() .widgets_mut()
.set_size_rules(id, None, Some(Len::rel(1.0))); .set_size_rules(id, None, Some(LayoutLen::rel(1.0)));
} }
self.tree.ids.push(id.id()); self.tree.ids.push(id.id());
self.tree.spans.push(Spanned { id, spares, grown }); self.tree.spans.push(Spanned { id, spares, grown });
id.add_strong(self.rsc) return id.add_strong(self.rsc);
}
};
self.tree.ids.push(id.id());
id
} }
} }
+2 -2
View File
@@ -11,8 +11,8 @@ impl Widget for Image {
Size::px(self.handle.size()) Size::px(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
Some(Len::px(self.handle.size().axis(axis))) Some(LayoutLen::px(self.handle.size().axis(axis)))
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.widget(&self.inner); painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll` // What it occupies is its box, on both axes, for the reason `Scroll`
// reports the same: it clips what is inside to that box, so it can // reports the same: it clips what is inside to that box, so it can
+2 -2
View File
@@ -1,15 +1,15 @@
mod image; mod image;
mod mask; mod mask;
mod position; mod position;
mod ptr;
mod rect; mod rect;
mod text; mod text;
mod trait_fns; mod trait_fns;
mod wrapper;
pub use image::*; pub use image::*;
pub use mask::*; pub use mask::*;
pub use position::*; pub use position::*;
pub use ptr::*;
pub use rect::*; pub use rect::*;
pub use text::*; pub use text::*;
pub use trait_fns::*; pub use trait_fns::*;
pub use wrapper::*;
+16 -7
View File
@@ -7,15 +7,20 @@ pub struct Pad {
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let inner = painter // The inner's own alignment, not the near edge. This reports the
.widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR) // inner's size plus the padding, so where the box is that answer the
.size(); // inset box is exactly the inner and alignment has no room to move
// it; where the box is bigger -- a share of a row, a rule over this
// widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for.
let inside = DrawRegion::Extent(self.padding.region());
let inner = painter.widget_within(&self.inner, inside).size();
Size { Size {
x: Len { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
..inner.x ..inner.x
}, },
y: Len { y: LayoutLen {
px: inner.y.px + self.padding.top + self.padding.bottom, px: inner.y.px + self.padding.top + self.padding.bottom,
..inner.y ..inner.y
}, },
@@ -47,14 +52,18 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
pub fn region(&self) -> UiRegion { /// `region` less this padding on each side.
let mut region = UiRegion::FULL; pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left; region.x.start.px += self.left;
region.y.start.px += self.top; region.y.start.px += self.top;
region.x.end.px -= self.right; region.x.end.px -= self.right;
region.y.end.px -= self.bottom; region.y.end.px -= self.bottom;
region region
} }
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = Px::from_num(amt);
Self { Self {
+11 -7
View File
@@ -14,10 +14,10 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis); let container_len = painter.px_len(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 draw it at the scrolled offset. // not already known, then draw it at the scrolled offset.
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) { let whole = UiRegion::FULL;
Some(len) => len, let own = painter.placement();
None => painter.widget(&self.inner).size().axis(self.axis), let answer_len =
}; painter.measure_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]);
let content = answer_len.apply_leftover(); let content = answer_len.apply_leftover();
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(container_len); self.content_len = content.to_px(container_len);
@@ -57,13 +57,17 @@ impl Widget for Scroll {
if moved || self.content_len != self.container_len { if moved || self.content_len != self.container_len {
let offset = UiVec2::from_axis( let offset = UiVec2::from_axis(
self.axis, self.axis,
UiScalar::from_parts(Rel::ZERO, anchor - self.amt), Len::from_parts(Rel::ZERO, anchor - self.amt),
UiScalar::ZERO, Len::ZERO,
); );
region = region.offset(offset); region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
} }
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR); // The viewport is the inner's region, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is put is the content
// box, scrolled.
painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
+60 -63
View File
@@ -10,20 +10,34 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The row: this span's own box, as a span of the region it was given.
// Its children are laid out along it, and what they declare or report
// is a fraction of the region -- the area this span was told it has,
// which it passes on unchanged.
let own = painter.placement();
let row = *own.axis(axis);
// Across itself the span's own box is the child's region: a span is
// what contains its children there, and nothing divides that axis.
// Along it the whole region is, so a fraction means the same thing
// for every child however much of the row is left when it is asked.
let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(row.start + from, row.start + to),
Sign::Neg => UiSpan::new(row.end - to, row.end - from),
};
let far = row.len();
// A length for every child before their final boxes are chosen: from // A length for every child before their final boxes are chosen: from
// a hint where one exists, and from drawing otherwise. // a hint where one exists, and from drawing otherwise.
let mut cursor = UiScalar::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let mut span = UiSpan::new(cursor, UiScalar::rel_max()); // The whole region is the child's, so `rel(0.5)` is half the area
if self.dir.sign == Sign::Neg { // this span was given whatever else is in it and wherever this
span.flip(); // child sits among them. What it is placed in is the room left
} // from the cursor, because a text has to wrap at the width
let region = UiRegion::from_axis(axis, span, UiSpan::FULL); // actually there.
let len = match painter.known_len(child, axis, region) { let room = axis.pair(Some(along(cursor, far)), None);
Some(len) => len, let len = painter.measure_len(child, axis, region, room);
None => painter.widget_within(child, region).len(axis),
};
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
lens.push(len); lens.push(len);
@@ -33,67 +47,51 @@ impl Widget for Span {
.gap .gap
.mul_int(self.children.len().saturating_sub(1) as i32); .mul_int(self.children.len().saturating_sub(1) as i32);
let total = lens.iter().fold( let total = lens.iter().fold(
Len { LayoutLen {
px: gaps, px: gaps,
..Len::ZERO ..LayoutLen::ZERO
}, },
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// What is left for the shares to divide: the row less everything
// fixed, as a length of the region rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. The room to // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// divide is `len * fixed - total.px`, and the length where it runs // itself, and answered back through the same expression, so the
// out is exactly the box a parent sizing itself from this answer // boundary is the drawing's own and not a second way of finding it:
// hands back -- which is why this used to need a margin either side // the three cases a rounded division needed -- the fixed parts
// of the boundary, and why it does not now: that box and this sum are // growing slower than the box, faster, or exactly with it -- are the
// whole counts of the same step, and both routes to it land on the // sign of `room.rel`, which `through` already reads. What the
// same count. What the generated oracle checks is the consequence, // generated oracle checks is the consequence, since which children
// since which children exist at all turns on this. // exist at all turns on this.
let fixed = Rel::ONE - total.rel;
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
let current = painter.px_len(axis); shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
let holds = if fixed > Rel::ZERO { let holds = match shares {
// The box length the fixed parts alone fill. true => Holds::from(Px::STEP..=Px::MAX),
let full = total.px.div(fixed); false => Holds::from(Px::MIN..=Px::ZERO),
shares = current > full;
match shares {
true => Holds::from(full.next_up()..=Px::MAX),
false => Holds::from(Px::MIN..=full),
}
} else if fixed < Rel::ZERO {
// The relative parts grow faster than the box does, so here
// a shorter box is the one that leaves room.
let full = total.px.div(fixed);
shares = current < full;
match shares {
true => Holds::from(Px::MIN..=full.next_down()),
false => Holds::from(full..=Px::MAX),
}
} else {
// The relative parts take exactly the box, whatever it is, so
// the only room is what negative pixels leave.
shares = total.px < Px::ZERO;
Holds::ANY
}; };
painter.holds(axis, holds); painter.region_holds(axis, holds.through(room));
} }
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
// rule beside it already says how long it is, and then reading them // rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. // answers nothing and makes its size depend on theirs for it. A rule
let shrinks = !painter.ruled(!axis); // that only bounds the length does not count: the answer is still
// this span's to give.
let shrinks = !painter.has_exact_size(!axis);
// What the fixed parts and the gaps before here take, which is a sum // What the fixed parts and the gaps before here take, which is a sum
// of lengths and exact, and how much of the leftover weight is // of lengths and exact, and how much of the leftover weight is
// spoken for. A position is one from the other rather than a step // spoken for. A position is one from the other rather than a step
// from the last child: the share of the room is rounded, and taking // from the last child: the share of the room is rounded, and taking
// each from the one before it would carry every rounding along the // each from the one before it would carry every rounding along the
// row. // row.
let mut fixed = UiScalar::rel_min(); let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let room = UiScalar::rel_max() - UiScalar::from_parts(total.rel, total.px); let mut start = Len::rel_min();
let mut start = UiScalar::rel_min(); let mut ortho = LayoutLen::ZERO;
let mut ortho = Len::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for (child, len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over, // A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for // when nothing is, is not drawn at all. One that also asked for
@@ -104,20 +102,19 @@ impl Widget for Span {
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let mut span = UiSpan::FULL; let from = start;
span.start = start;
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
taken += len.leftover; taken += len.leftover;
} }
fixed.px += len.px; fixed.px += len.px;
fixed.rel += len.rel; fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room); start = shared(fixed, taken, total.leftover, room);
span.end = start; // Along the row the span says where the child goes; across it the
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); // child sits where its own alignment says. Its region is the
if self.dir.sign == Sign::Neg { // whole of what this span was given either way, which is what its
region.flip(axis); // fractions are of.
} let placed =
let placed = painter.widget_within(child, region); painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
if shrinks { if shrinks {
let used = placed.len(!axis); let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
@@ -125,7 +122,7 @@ impl Widget for Span {
// A scalable child therefore makes Children scalable too; // A scalable child therefore makes Children scalable too;
// only fixed children are compared with one another. // only fixed children are compared with one another.
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO { if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
ortho = Len::LEFTOVER; ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO { } else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px); ortho.px = ortho.px.max(used.px);
} }
@@ -144,7 +141,7 @@ impl Widget for Span {
let along = total; let along = total;
let ortho = match shrinks { let ortho = match shrinks {
true => ortho, true => ortho,
false => Len::rel(1.0), false => LayoutLen::rel(1.0),
}; };
Size::from_axis(axis, along, ortho) Size::from_axis(axis, along, ortho)
} }
@@ -153,7 +150,7 @@ impl Widget for Span {
/// Where a row has reached: everything fixed before this point, which is a /// Where a row has reached: everything fixed before this point, which is a
/// sum and exact, plus the share of the room the weights so far are worth, /// sum and exact, plus the share of the room the weights so far are worth,
/// which is one rounding wherever it is asked for. /// which is one rounding wherever it is asked for.
fn shared(fixed: UiScalar, taken: Weight, weight: Weight, room: UiScalar) -> UiScalar { fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
if taken == Weight::ZERO { if taken == Weight::ZERO {
return fixed; return fixed;
} }
+11 -5
View File
@@ -13,9 +13,10 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
}; };
// Whichever child sizes the stack decides the box every child gets. // Whichever child sizes the stack keeps the stack's whole region as
// The stack reports that size, so a child given a longer box would // its own -- the stack is the length that child asked for, so taking
// draw outside what the stack says it occupies. // the fraction of the stack's box again would take it twice -- and is
// put where the stack itself is put.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse // On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained // rather than a second drawing of it somewhere else: a retained
@@ -26,10 +27,15 @@ impl Widget for Stack {
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i); painter.child_layer_at(i);
painter.widget_aligned(child, region, RegionAlign::NEAR); // Every other child has the stack's own box for its region, since
// the stack is what contains it, and where it sits in one bigger
// than itself is its own business.
painter.widget_within(child, DrawRegion::Extent(UiRegion::FULL));
} }
size size
} }
+2 -2
View File
@@ -38,8 +38,8 @@ impl Widget for Rect {
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<Len> { fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(Len::LEFTOVER) Some(LayoutLen::LEFTOVER)
} }
} }
+11 -11
View File
@@ -50,15 +50,11 @@ impl TextView {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X)); let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
// The shaper measures in floats, which is where a glyph advance comes // The shaper measures in floats, which is where a glyph advance comes
// from; what it answers goes back on the grid. // from; what it answers goes back on the grid.
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32)); painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
// A greedy break is the same break at every width from its longest if width.is_some() {
// line up to the one it was made at: each line still fits, and none painter.holds(Axis::X, self.buf.width_holds());
// could take a word that did not fit in the wider box. A line too
// long to fit at all says nothing about narrower boxes.
if let Some(width) = width {
painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width);
} }
text self.buf.rendered().expect("render_text placed the glyphs")
} }
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
@@ -78,9 +74,13 @@ 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::px(tex.size); // The step at or above what the shaper measured, so a parent that
let within = region.within(&painter.region()); // hands back the length this reports hands back a box the longest
painter.glyphs(tex, within); // line fits in. Rounded to the nearest step it is half the time a
// hair under that line, and the break made in it is not the break a
// cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
painter.glyphs(tex, DrawRegion::Extent(region));
(region, size) (region, size)
} }
+9 -5
View File
@@ -59,7 +59,7 @@ widget_trait! {
} }
} }
fn width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into(); let len = len.into();
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
@@ -71,7 +71,7 @@ widget_trait! {
} }
} }
fn height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into(); let len = len.into();
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
@@ -134,9 +134,13 @@ widget_trait! {
|state| self.add(state) |state| self.add(state)
} }
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) { // Named for the type it makes rather than as `wrapped`, which would read
let id = self.add_strong(state); // as the text setting. `widget_trait!` takes no attributes, so what it is
state.ui_mut().widgets[ptr].inner = Some(id); // for is on `Wrapper` itself.
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|state| Wrapper {
inner: Some(self.add_strong(state)),
}
} }
} }
+12 -4
View File
@@ -1,11 +1,19 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::Unsize; use std::marker::Unsize;
pub struct WidgetPtr { /// One widget in a box of its own, doing as little as possible on the way:
/// it draws its child in the whole of its box and reports back what the child
/// said. It exists because a length and an alignment are properties of one
/// widget, so a widget cannot both be 100 wide and take two shares of a row
/// -- the two lengths need two widgets, and this is the smaller one.
///
/// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for.
pub struct Wrapper {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
} }
impl Widget for WidgetPtr { impl Widget for Wrapper {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
match &self.inner { match &self.inner {
Some(id) => painter.widget(id).size(), Some(id) => painter.widget(id).size(),
@@ -14,7 +22,7 @@ impl Widget for WidgetPtr {
} }
} }
impl WidgetPtr { impl Wrapper {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -35,7 +43,7 @@ impl WidgetPtr {
} }
} }
impl Default for WidgetPtr { impl Default for Wrapper {
fn default() -> Self { fn default() -> Self {
Self::empty() Self::empty()
} }
+272 -15
View File
@@ -20,6 +20,86 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
assert_corners!(h, right, (100, 0), (400, 200)); assert_corners!(h, right, (100, 0), (400, 200));
} }
/// A span places each child in the room left after the one before, because a
/// text has to wrap at the width actually there, but the child's region is
/// the whole row. So two children asking for half each take the whole row
/// between them, however much of it was left when each was asked, and a third
/// overflows -- and a span passes its own region on unchanged, so a child of
/// a nested span asking for half asks for half of the same row.
#[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported, and its own child
// asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100));
}
/// The same fraction either way round: after a 100 px child in a 400 px row,
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
/// drew half of what it was offered and reported that. Half the row, not half
/// of the 300 px left of it.
#[test]
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
let mut declaring = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(declaring, declared, (100, 0), (300, 100));
let mut reporting = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(reporting, reported, (100, 0), (300, 100));
}
/// What the fraction a child reports is of and what box it is offered are
/// two different lengths, and only the first is the whole row: a text still
/// wraps at the room actually left after its neighbour, so the same
/// paragraph is taller where less of the row is left for it.
#[test]
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
let paragraph = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer.";
let height_after = |head_width: i32| {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
let region = h.region(&text).unwrap();
(region.bot_right.y - region.top_left.y).to_f32()
};
let (crowded, whole_row) = (height_after(300), height_after(0));
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// The same reading through a pad: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two.
#[test]
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
// Ruled to the window: a root reporting a fraction of it is otherwise
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
}
#[test] #[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() { fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -27,7 +107,7 @@ fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let span = (child,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc); let span = (child,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
h.set_root(span); h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, Len::rel(1.0)); assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
} }
#[test] #[test]
@@ -38,7 +118,7 @@ fn a_span_reports_its_tallest_fixed_child() {
let span = (short, tall).span(Dir::RIGHT).add(&mut h.rsc); let span = (short, tall).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(span); h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, Len::px(70.0)); assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::px(70.0));
} }
#[test] #[test]
@@ -225,21 +305,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
h.set_root((bar, buried).span(Dir::RIGHT)); h.set_root((bar, buried).span(Dir::RIGHT));
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1, "only the root region"); assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
h.rsc.widgets_mut().set_region_node(buried, true); h.rsc.widgets_mut().set_region_node(buried, true);
h.frame(); h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!( assert_eq!(
h.render.moves.depth(move_idx), h.render.moves.depth(move_idx),
2, 1,
"the opted-in widget's region and the root region" "the opted-in widget's region alone"
); );
h.rsc.widgets_mut().set_region_node(buried, false); h.rsc.widgets_mut().set_region_node(buried, false);
h.frame(); h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1); assert_eq!(h.render.moves.depth(move_idx), 0);
} }
/// A span that sizes from its children passes their `leftover` weight up /// A span that sizes from its children passes their `leftover` weight up
@@ -266,6 +346,11 @@ fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
/// The same space, unevenly nested: weights carried up mean a share is a /// 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. /// share of the whole, not of whatever branch a widget happens to sit in.
///
/// Each edge lands on the even division or one step below it, since a share
/// is a fraction of the room and a truncating multiply gives up what that
/// fraction does not divide. What stays exact is that each share starts
/// where the last one ended and the row ends at its own edge.
#[test] #[test]
fn an_uneven_nesting_still_gives_every_share_the_same_length() { fn an_uneven_nesting_still_gives_every_share_the_same_length() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -279,10 +364,24 @@ fn an_uneven_nesting_still_gives_every_share_the_same_length() {
let three = (b, c, d).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)); h.set_root((one, three).span(Dir::RIGHT));
let mut start = Px::ZERO;
for (i, id) in [a, b, c, d].into_iter().enumerate() { for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0; let got = h.region(&id).expect("widget drew nothing");
assert_corners!(h, id, (x, 0), (x + 100.0, 200)); let even = Px::from_int((i as i32 + 1) * 100);
assert_eq!(got.top_left, PxVec2::new(start, Px::ZERO), "share {i}");
assert_eq!(got.bot_right.y, Px::from_int(200), "share {i}");
assert!(
got.bot_right.x == even || got.bot_right.x == even.next_down(),
"share {i} ends at {:?}, not {even:?}",
got.bot_right.x
);
start = got.bot_right.x;
} }
assert_eq!(
start,
Px::from_int(400),
"the row stopped short of its edge"
);
} }
/// However many ways a row is divided, the shares add up to the row: each /// However many ways a row is divided, the shares add up to the row: each
@@ -322,16 +421,15 @@ fn a_row_of_equal_shares_fills_it_exactly() {
} }
} }
/// Where the shader puts an edge: the two parts of a scalar are floored /// Where the shader puts an edge: the fraction resolved against the window
/// apart, so a fraction and a pixel offset snap independently, and each is /// plus the pixel offset, taken to the boundary it composes to within half
/// taken to the boundary it composes to within half a step of. Kept in step /// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
/// with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region); let region = h.render.moves.resolve(active.parent_move, active.region);
let dim = h.size().axis(axis); let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor(); let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: UiScalar| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32()); let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis); let span = region.axis(axis);
(edge(span.start), edge(span.end)) (edge(span.start), edge(span.end))
} }
@@ -343,7 +441,7 @@ fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
} }
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget { fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
h.set_len(&inner, Axis::X, Len::leftover(ratio)); h.set_len(&inner, Axis::X, LayoutLen::leftover(ratio));
inner inner
} }
@@ -454,7 +552,7 @@ fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
let mut h = Harness::new((100, 20)); let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc); let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let mixed = rect(Color::BLUE) let mixed = rect(Color::BLUE)
.width(Len::px(20) + Len::LEFTOVER) .width(LayoutLen::px(20) + LayoutLen::LEFTOVER)
.add(&mut h.rsc); .add(&mut h.rsc);
h.set_root((fixed, mixed).span(Dir::RIGHT)); h.set_root((fixed, mixed).span(Dir::RIGHT));
@@ -480,3 +578,162 @@ fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
assert!(h.region(&a).is_none()); assert!(h.region(&a).is_none());
assert!(h.region(&b).is_none()); assert!(h.region(&b).is_none());
} }
/// **A stack child smaller than the stack sits where its own alignment
/// says.** `Stack` gives every child the box its sizing child defines and
/// used to force the near edge on all of them; that override is owed only to
/// the sizing child, which has already placed its own content in the box the
/// stack derived from its answer. Every other child is handed a box that owes
/// nothing to it, so where it sits in one bigger than itself is its own
/// business -- and with the override it could not be aligned at all, which is
/// what moved the `tabs` example's counters to the wrong corner.
#[test]
fn a_stack_child_smaller_than_the_stack_keeps_its_own_alignment() {
let mut h = Harness::new((400, 200));
let big = rect(Color::BLUE).add(&mut h.rsc);
let small = rect(Color::RED).sized((50, 50)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(small.id(), Axis::X, AxisAlign::POS);
let (a, b) = (big.add_strong(&mut h.rsc), small.add_strong(&mut h.rsc));
let children: Vec<StrongWidget> = vec![a, b];
h.set_root(Stack {
children,
size: StackSize::Default,
});
assert_corners!(h, big, (0, 0), (400, 200));
// The far edge on X because it asked for it, the middle on Y because
// that is the default.
assert_corners!(h, small, (350, 75), (400, 125));
}
/// Five children of one span, buried under three containers that are each a
/// fraction of their parent so no length reaches the window without being
/// composed and rounded on the way. Returns each child's drawn width and
/// each gap between them, in pixels.
fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>, Vec<Px>) {
let mut h = Harness::new((box_w, 400.0));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..5 {
let r = rect(Color::RED).add(&mut h.rsc);
if let Some(len) = kid {
h.rsc
.widgets_mut()
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
}
ids.push(r.id());
kids.push(r.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::from_f32(gap),
}
.add(&mut h.rsc);
let a = (span.width(rel(0.9)),).span(Dir::RIGHT).add(&mut h.rsc);
let b = (a.width(rel(0.8)),).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((b.width(rel(0.7)),).span(Dir::RIGHT));
let boxes: Vec<_> = ids
.iter()
.map(|id| h.region(id).expect("a child drew nothing"))
.collect();
(
boxes.iter().map(|b| b.bot_right.x - b.top_left.x).collect(),
boxes
.windows(2)
.map(|p| p[1].top_left.x - p[0].bot_right.x)
.collect(),
)
}
/// **A length given in pixels is that many pixels, wherever it ends up.** A
/// gap and a declared width compose additively -- `Len::within` adds a part's
/// own pixels rather than scaling them, and both ends of a gap carry the same
/// fraction, so the multiply that rounds is the same on each -- which is why
/// nesting the row inside fractions of fractions cannot move them. Swept over
/// 2,100 box widths when this was written and exact at every one; five here,
/// including widths that divide badly by five.
#[test]
fn a_length_in_pixels_is_that_many_pixels_however_it_is_nested() {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let want = Px::from_int(7);
let (_, gaps) = row_under_fractions(None, 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between leftover children are {gaps:?}"
);
let (widths, gaps) = row_under_fractions(Some(LayoutLen::px(100.0)), 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between fixed children are {gaps:?}"
);
assert!(
widths.iter().all(|w| *w == Px::from_int(100)),
"box {box_w}: declared widths came out {widths:?}"
);
}
}
/// **Children asking for the same share of a row are not the same length**,
/// and this pins by how much rather than claiming they are equal. A position
/// is the quantity that gets rounded, so the row fills exactly and no two
/// children leave a seam; what that costs is a step or two between lengths
/// that were asked for identically. Exact composition would shrink the
/// spread, not remove it: five equal lengths cannot fill a row whose step
/// count is not a multiple of five.
#[test]
fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
for kid in [None, Some(LayoutLen::rel(0.2))] {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let (widths, gaps) = row_under_fractions(kid, 0.0, box_w);
let spread = *widths.iter().max().unwrap() - *widths.iter().min().unwrap();
assert!(
spread <= Px::from_raw(2),
"box {box_w}, {kid:?}: widths {widths:?} spread {spread:?}"
);
assert!(
gaps.iter().all(|g| *g == Px::ZERO),
"box {box_w}, {kid:?}: children left seams {gaps:?}"
);
}
}
}
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
#[test]
fn a_fixed_child_is_centered_in_its_wrappers_share() {
let mut h = Harness::new((600, 300));
let leaf = rect(Color::RED).sized((100, 100)).center().add(&mut h.rsc);
let wrapper = leaf
.wrapper()
.width(leftover(2))
.height(rel(1.0))
.add(&mut h.rsc);
let other = rect(Color::BLUE).width(200).add(&mut h.rsc);
h.set_root((other, wrapper).span(Dir::RIGHT));
assert_corners!(h, wrapper, (200, 0), (600, 300));
assert_corners!(h, leaf, (350, 100), (450, 200));
h.resize((900, 400));
h.frame();
assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250));
}
+121
View File
@@ -0,0 +1,121 @@
//! The tree a seed describes, as a value rather than as widgets.
//!
//! Two things have to hold for a plan to be worth having. Editing a plan has
//! to mean what growing with those edits means, or a scenario reads one thing
//! and the oracle another. And reducing a plan has to end, or a shrinker
//! searching for the smallest counterexample never returns.
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
use std::collections::HashMap;
fn some_edits(seed: u64, of: &Plan) -> Edits {
let mut rng = Rng::new(seed);
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
let mut of = of.clone();
of.walk_mut(&mut |p| {
if matches!(p.kind, Kind::Span { .. }) {
spans += 1;
}
sized += p.size.is_some() as usize;
aligned += p.align.is_some() as usize;
nodes += p.region_node.is_some() as usize;
});
let pick =
|n: usize, rng: &mut Rng| -> Vec<usize> { (0..n).filter(|_| rng.chance()).collect() };
Edits {
sizes: pick(sized, &mut rng)
.into_iter()
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None]))
.collect(),
aligns: pick(aligned, &mut rng)
.into_iter()
.map(|i| (i, [Some(AxisAlign::POS), None]))
.collect(),
nodes: pick(nodes, &mut rng)
.into_iter()
.map(|i| (i, true))
.collect(),
spans: pick(spans, &mut rng)
.into_iter()
.map(|i| {
(
i,
SpanEdit {
detach: vec![0],
attach: 2,
},
)
})
.collect::<HashMap<_, _>>(),
fixed_branches: false,
}
}
use iris::prelude::*;
/// The two routes to an edited tree are one tree. `plan` resolves edits out
/// of the random stream as it draws; `edited` puts them on a tree that
/// already exists, which is the only route a shrunk plan has, since no seed
/// grows one. A scenario written against either has to read the same.
#[test]
fn editing_a_plan_is_growing_one_with_those_edits() {
for seed in 1..=60 {
let bare = plan(seed, 5, &Edits::default());
let edits = some_edits(seed, &bare);
assert_eq!(
bare.edited(&edits),
plan(seed, 5, &edits),
"seed {seed}: edited and grown-with-edits disagree"
);
}
}
/// Every simplification is strictly smaller, so taking them in turn reaches a
/// fixed point instead of circling. A shrinker that can return to a tree it
/// has already tried does not stop.
#[test]
fn every_simplification_of_a_plan_is_smaller_than_it() {
for seed in 1..=60 {
let tree = plan(seed, 4, &Edits::default());
let mut queue = vec![tree];
let mut seen = 0;
while let Some(node) = queue.pop() {
seen += 1;
if seen > 400 {
break;
}
for small in node.smaller() {
assert!(
small.size() <= node.size(),
"seed {seed}: a simplification grew from {} to {}",
node.size(),
small.size()
);
if small.size() < node.size() {
queue.push(small);
}
}
}
}
}
/// Reducing until nothing reduces ends, and ends at something small enough to
/// read rather than at the tree it started from.
#[test]
fn reducing_a_plan_all_the_way_ends() {
for seed in 1..=30 {
let mut node = plan(seed, 5, &Edits::default());
let grown = node.size();
let mut steps = 0;
while let Some(next) = node.smaller().into_iter().next() {
node = next;
steps += 1;
assert!(steps < 10_000, "seed {seed}: reducing did not end");
}
assert!(
node.size() < grown.max(2),
"seed {seed}: reduced {grown} widgets to {}",
node.size()
);
}
}
+736 -17
View File
@@ -125,10 +125,7 @@ fn moving_an_ordinary_subtree_remaps_its_mask() {
let active = &h.render.active[&masked.id()]; let active = &h.render.active[&masked.id()];
assert_eq!( assert_eq!(
h.rsc.ui().masks[active.mask.idx()].region, h.rsc.ui().masks[active.mask.idx()].region,
UiRegion::new( UiRegion::new(UiSpan::new(Len::px(150.0), Len::rel_max()), UiSpan::FULL,)
UiSpan::new(UiScalar::px(150.0), UiScalar::rel_max()),
UiSpan::FULL,
)
); );
assert_corners!(h, inner, (150, 0), (400, 200)); assert_corners!(h, inner, (150, 0), (400, 200));
} }
@@ -159,16 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT)); h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1); assert_eq!(told_draws.get(), 1);
// Reading its box makes its drawing hold for the measuring box alone, // Only the available length changes: positioning the final slot does
// and it reports less than that box: so it is drawn again in the box its // not invalidate a numeric size read.
// answer places it in, and once more in the final box the span chooses. assert_eq!(asked_draws.get(), 2);
// A widget that says what it holds for, as text does, skips the middle
// one.
assert_eq!(
asked_draws.get(),
3,
"drawn to be measured, in its placed box, then in its final box"
);
} }
#[test] #[test]
@@ -312,7 +302,7 @@ fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
assert_eq!(draws.get(), settled); assert_eq!(draws.get(), settled);
assert_corners!(h, leaf, (0, 0), (400, 100)); assert_corners!(h, leaf, (0, 0), (400, 100));
assert_eq!(h.render.active[&span.id()].size.y, Len::rel(1.0)); assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
} }
/// The output is the root of the box chain, so a resize is a box that changed /// The output is the root of the box chain, so a resize is a box that changed
@@ -407,12 +397,12 @@ fn a_box_change_within_one_step_is_not_a_change() {
let step = Px::STEP.to_f32(); let step = Px::STEP.to_f32();
for part in [0.1, 0.2, 0.3] { for part in [0.1, 0.2, 0.3] {
h.rsc[first].size.x = Len::px(100.0 + step * part); h.rsc[first].size.x = LayoutLen::px(100.0 + step * part);
h.frame(); h.frame();
assert_eq!(draws.get(), settled); assert_eq!(draws.get(), settled);
} }
h.rsc[first].size.x = Len::px(100.0 + step); h.rsc[first].size.x = LayoutLen::px(100.0 + step);
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 1); assert_eq!(draws.get(), settled + 1);
} }
@@ -616,3 +606,732 @@ fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
assert_ne!(layer(front.id()), layer(background.id())); assert_ne!(layer(front.id()), layer(background.id()));
assert_eq!(draws.get(), 1); assert_eq!(draws.get(), 1);
} }
/// A widget's own mask is not the one it inherited, and a redraw of it
/// inherits the second: handing back the first is handing it its own mask to
/// set a second time, which `set_mask` asserts against.
#[test]
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = inner.masked().add(&mut h.rsc);
let other = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((other, masked).span(Dir::RIGHT));
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_corners!(h, inner, (100, 0), (400, 200));
}
/// The two spans a subtree changes hands between, and the branch that is not
/// in the tree yet -- kept alive by the test until it is.
struct Handover {
leaf: WidgetId,
first: WeakWidget<Span>,
second: WeakWidget<Span>,
root: WeakWidget<Span>,
spare: StrongWidget,
}
/// A subtree that changes hands while its box does not move, so nothing about
/// reusing its drawing says it changed parents. `deeper` puts a span between
/// the root and `second`, so it changes depth by changing hands as well.
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
let leaf = rect(Color::RED).add(&mut h.rsc);
let sized = leaf.width(width).add(&mut h.rsc);
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
let first = Span {
children: match moved {
true => Vec::new(),
false => vec![holder.add_strong(&mut h.rsc)],
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let second = Span {
children: match moved {
true => vec![holder.add_strong(&mut h.rsc)],
false => Vec::new(),
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let branch = match deeper {
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
false => second.add_strong(&mut h.rsc),
};
let (in_tree, spare) = match moved {
true => (branch, first.add_strong(&mut h.rsc)),
false => (first.add_strong(&mut h.rsc), branch),
};
let root = Span {
children: vec![in_tree],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
Handover {
leaf: sized.id(),
first,
second,
root,
spare,
}
}
/// Moves the subtree and swaps the branch it sits in for the one it left.
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
let holder = h.rsc[tree.first].children.remove(0);
h.rsc[tree.second].children.push(holder);
h.rsc[tree.root].children.clear();
h.rsc[tree.root].children.push(tree.spare);
h.frame();
tree.leaf
}
#[test]
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, false, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, false, 40.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it left still listed it and undrew it"
);
}
#[test]
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, true, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
// After it has changed hands, so what has to reach the new parent is a
// change made under the subtree it now holds.
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
warm.frame();
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, true, 90.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it moved to is the one the change has to reach"
);
}
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
let instance = &h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx];
h.render
.moves
.resolve(instance.move_idx, instance.region)
.to_px(h.render.output_size())
})
.collect()
}
#[test]
fn frame_geometry_and_extent_geometry_keep_their_references() {
struct Both(Rc<Cell<usize>>);
impl Widget for Both {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.primitive_within(RectPrimitive::color(Color::RED), UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::GREEN).width(100).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let both = Both(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(both, node);
h.set_root((first, both).span(Dir::RIGHT));
let count = draws.get();
h.set_len(first, Axis::X, 200);
h.frame();
assert_eq!(draws.get(), count);
let bounds = primitive_bounds(&h, both.id());
assert_eq!(bounds[0].top_left.x, Px::ZERO);
assert_eq!(bounds[0].bot_right.x, Px::from_int(400));
assert_eq!(bounds[1].top_left.x, Px::from_int(200));
assert_eq!(bounds[1].bot_right.x, Px::from_int(400));
}
}
#[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
let first = rect(Color::RED).width(width).add(&mut h.rsc);
let words = wtext(text).size(20).wrap(true).add(&mut h.rsc);
let through = Stretchy {
inner: words.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add(&mut h.rsc);
h.set_root((first, through).span(Dir::RIGHT));
(words, first)
}
let short = "one two";
let long = "one two three four five six seven eight nine ten eleven twelve";
let mut warm = Harness::new((400, 200));
let (words, first) = build(&mut warm, 50, short);
warm.set_len(first, Axis::X, 200);
warm.frame();
*warm.rsc[words].content = long.to_string();
warm.frame();
let mut cold = Harness::new((400, 200));
let (other, _) = build(&mut cold, 200, long);
assert_eq!(warm.region(&words), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, words.id()),
primitive_bounds(&cold, other.id())
);
}
#[test]
fn widening_text_without_soft_breaks_reuses_its_drawing() {
struct CountedText {
text: Text,
draws: Rc<Cell<usize>>,
}
impl Widget for CountedText {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.text.draw(painter)
}
}
for content in ["Short text", "Two hard\nline breaks\nhere", ""] {
let plant = |h: &mut Harness| {
let mut text = Text::new(content);
text.wrap = true;
let draws = Rc::new(Cell::new(0));
let root = CountedText {
text,
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(root);
(root, draws)
};
let mut warm = Harness::new((300, 200));
let (root, draws) = plant(&mut warm);
let before = draws.get();
warm.resize((500, 200));
warm.frame();
assert_eq!(draws.get(), before, "{content:?}");
let mut cold = Harness::new((500, 200));
let (other, _) = plant(&mut cold);
assert_eq!(warm.region(&root), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, root.id()),
primitive_bounds(&cold, other.id())
);
}
}
#[test]
fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
struct Frame {
child: StrongWidget,
region: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_within(&self.child, self.region);
Size::LEFTOVER
}
}
struct Painted(Rc<Cell<usize>>);
impl Widget for Painted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
let fixed = |start, end| UiRegion::new(UiSpan::new(Len::px(start), Len::px(end)), UiSpan::FULL);
for node in [false, true] {
let plant = |h: &mut Harness, region| {
let draws = Rc::new(Cell::new(0));
let leaf = Painted(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node);
let inner = Frame {
child: leaf.add_strong(&mut h.rsc),
region: UiRegion::new(UiSpan::new(Len::rel(0.23), Len::rel(0.83)), UiSpan::FULL),
}
.add_strong(&mut h.rsc);
let root = Frame {
child: inner,
region,
}
.add(&mut h.rsc);
h.set_root(root);
(root, leaf, draws)
};
let mut warm = Harness::new((400, 200));
let (root, leaf, draws) = plant(&mut warm, fixed(7.0, 104.0));
let before = draws.get();
warm.rsc[root].region = fixed(19.0, 180.0);
warm.frame();
assert_eq!(draws.get(), before);
let mut cold = Harness::new((400, 200));
let (_, other, _) = plant(&mut cold, fixed(19.0, 180.0));
assert_eq!(warm.region(&leaf), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, leaf.id()),
primitive_bounds(&cold, other.id())
);
let mask = |h: &Harness, id: WidgetId| {
let active = &h.render.active[&id];
let mask = &h.rsc.ui().masks[active.mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
}
}
#[test]
fn a_span_does_not_place_its_measurement_before_assigning_the_childs_slot() {
struct MeasuredBox(Rc<Cell<usize>>);
impl Widget for MeasuredBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.px_size();
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((100, 50))
}
}
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = MeasuredBox(draws.clone()).add(&mut h.rsc);
h.set_root((leaf,).span(Dir::RIGHT).width(rel(1.0)).height(rel(1.0)));
assert_eq!(draws.get(), 3);
assert_corners!(h, leaf, (0, 75), (100, 125));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
h.frame();
assert_eq!(draws.get(), 3);
h.resize((600, 300));
h.frame();
assert_eq!(draws.get(), 6);
assert_corners!(h, leaf, (0, 125), (100, 175));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
}
#[test]
fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
struct Glyphs {
buffer: TextBuffer,
draws: Rc<Cell<usize>>,
}
impl Widget for Glyphs {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
let text = painter.render_text(&mut self.buffer, &TextAttrs::default(), None);
let origin = UiRegion::new(
UiSpan::new(Len::rel(0.23) + Len::px(-7.125), Len::FULL),
UiSpan::new(Len::rel(0.37) + Len::px(3.25), Len::FULL),
);
painter.glyphs(text, DrawRegion::Frame(origin));
painter.glyphs(text, DrawRegion::Extent(origin));
Size::LEFTOVER
}
}
struct Frame {
child: StrongWidget,
region: UiRegion,
extent: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
self.region,
[Some(self.extent.x), Some(self.extent.y)],
);
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((403, 211));
let draws = Rc::new(Cell::new(0));
let text = Glyphs {
buffer: TextBuffer::new("Glyphs: gj AV\nsecond line"),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(text, node);
let root = Frame {
child: text.add_strong(&mut h.rsc),
region: UiRegion::FULL,
extent: UiRegion::FULL,
}
.add(&mut h.rsc);
h.set_root(root);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let before = draws.get();
h.rsc[root].region.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
h.rsc[root].extent = UiRegion::new(
UiSpan::new(Len::rel(start), Len::rel(end)),
UiSpan::new(Len::px(7.25), Len::rel(end)),
);
h.frame();
assert_eq!(draws.get(), before);
let retained = primitive_bounds(&h, text.id());
assert!(!retained.is_empty());
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
h.frame();
assert!(draws.get() > before);
assert_eq!(retained, primitive_bounds(&h, text.id()));
}
}
}
#[test]
fn resizing_does_not_remeasure_a_fixed_stack_for_its_unmeasured_overlay() {
let mut h = Harness::new((400, 200));
let (sizing, _) = counted(&mut h, Size::from((100, 80)), false);
let (overlay, draws) = counted(&mut h, Size::LEFTOVER, true);
h.set_root((sizing, overlay).stack().size(StackSize::Child(0)));
let settled = draws.get();
h.resize((800, 300));
h.frame();
assert_eq!(draws.get(), settled);
assert_corners!(h, overlay, (350, 110), (450, 190));
}
struct Unmeasured {
child: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Unmeasured {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.child);
Size::LEFTOVER
}
}
#[test]
fn a_declared_size_change_stops_at_an_independent_parent() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::RED).width(100).add(&mut h.rsc);
let parent = Unmeasured {
child: leaf.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add_strong(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
h.set_root(Unmeasured {
child: parent,
draws: draws.clone(),
});
let settled = draws.get();
h.set_len(leaf, Axis::X, 150);
h.frame();
assert_corners!(h, leaf, (125, 0), (275, 200));
assert_eq!(draws.get(), settled);
}
#[test]
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((leaf,).stack());
let settled = draws.get();
h.resize((800, 200));
h.frame();
assert!(draws.get() > settled);
assert_corners!(h, leaf, (300, 90), (500, 110));
}
#[test]
fn changed_drawing_dependencies_reach_ancestors_without_a_size_change() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
h.set_root(((leaf,).stack(),).stack());
h.rsc[leaf].reads_box = true;
h.frame();
let settled = draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(draws.get(), settled + 1);
assert_corners!(h, leaf, (0, 0), (800, 200));
}
#[test]
fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
let mut h = Harness::new((400, 200));
let (leaf, leaf_draws) = counted(&mut h, Size::LEFTOVER, true);
let draws = Rc::new(Cell::new(0));
let child = leaf.add_strong(&mut h.rsc);
h.set_root(Unmeasured {
child,
draws: draws.clone(),
});
let settled = draws.get();
for reads_box in [false, true, false, true] {
h.rsc[leaf].reads_box = reads_box;
h.frame();
assert_eq!(draws.get(), settled);
}
let settled = leaf_draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(leaf_draws.get(), settled + 1);
}
#[test]
fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
struct Observed<W> {
widget: W,
draws: Rc<Cell<usize>>,
}
impl<W: Widget> Widget for Observed<W> {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.widget.draw(painter)
}
}
struct Frame {
child: StrongWidget,
extent: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
);
Size::LEFTOVER
}
}
for node in [false, true] {
let plant = |h: &mut Harness, extent| {
let draws = Rc::new(Cell::new(0));
let leaf = rect(Color::BLUE).masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node);
let fixed = rect(Color::RED).width(31).height(19).add(&mut h.rsc);
let stack = Observed {
widget: Stack {
children: vec![leaf.add_strong(&mut h.rsc), fixed.add_strong(&mut h.rsc)],
size: StackSize::Default,
},
draws: draws.clone(),
}
.add_strong(&mut h.rsc);
let pad = Observed {
widget: Pad {
inner: stack,
padding: Padding::uniform(7).with_left(13),
},
draws: draws.clone(),
}
.add_strong(&mut h.rsc);
let root = Frame { child: pad, extent }.add(&mut h.rsc);
h.set_root(root);
(root, leaf, fixed, draws)
};
let mut warm = Harness::new((403, 211));
let (root, leaf, fixed, draws) = plant(&mut warm, UiRegion::FULL);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let extent = UiRegion::new(
UiSpan::new(Len::rel(start) + Len::px(3.125), Len::rel(end)),
UiSpan::new(Len::px(11.25), Len::rel(end)),
);
let before = draws.get();
warm.rsc[root].extent = extent;
warm.frame();
assert_eq!(draws.get(), before);
let mut cold = Harness::new((403, 211));
let (_, other, other_fixed, _) = plant(&mut cold, extent);
for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] {
assert_eq!(warm.region(&a), cold.region(&b));
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
}
let mask = |h: &Harness, id: WidgetId| {
let active = &h.render.active[&id];
let mask = &h.rsc.ui().masks[active.mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
}
}
}
#[test]
fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
struct Measured;
impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size {
let width = painter.px_len(Axis::X);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
}
}
struct Frame {
child: StrongWidget,
start: f32,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
UiRegion::FULL,
[
Some(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
)),
Some(UiSpan::FULL),
],
);
Size::LEFTOVER
}
}
let mut h = Harness::new((400, 200));
let leaf = Measured.add(&mut h.rsc);
let stack = (leaf,).stack().add_strong(&mut h.rsc);
let root = Frame {
child: stack,
start: 0.0,
}
.add(&mut h.rsc);
h.set_root(root);
assert_corners!(h, leaf, (60, 80), (140, 120));
h.rsc[root].start = 30.0;
h.frame();
assert_corners!(h, leaf, (90, 80), (170, 120));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf).unwrap()]
);
}
#[test]
fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
struct Container {
child: StrongWidget,
region: UiRegion,
}
impl Widget for Container {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter
.widget_within(&self.child, DrawRegion::Extent(self.region))
.size()
}
}
struct Frame {
child: StrongWidget,
extent: UiRegion,
answer: Rc<Cell<Size>>,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.answer.set(
painter
.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
)
.size(),
);
Size::LEFTOVER
}
}
for fractional in [false, true] {
for region in [
UiRegion::FULL,
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
] {
let plant = |h: &mut Harness, extent| {
let size = if fractional {
Size {
x: rel(0.5),
y: LayoutLen::px(27),
}
} else {
Size::from((80, 27))
};
let (leaf, _) = counted(h, size, !fractional);
let child = Container {
child: leaf.add_strong(&mut h.rsc),
region,
}
.add_strong(&mut h.rsc);
let answer = Rc::new(Cell::new(Size::ZERO));
let root = Frame {
child,
extent,
answer: answer.clone(),
}
.add(&mut h.rsc);
h.set_root(root);
(root, leaf, answer)
};
let mut warm = Harness::new((403, 211));
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
for width in [191.125, 297.25, 83.75] {
let extent =
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
warm.rsc[root].extent = extent;
warm.frame();
let mut cold = Harness::new((403, 211));
let (_, other, other_answer) = plant(&mut cold, extent);
assert_eq!(answer.get(), other_answer.get());
assert_eq!(warm.region(&leaf), cold.region(&other));
}
}
}
}
+282 -8
View File
@@ -3,12 +3,16 @@
//! frame that had not settled: a wrapping text shaped at a width it was //! 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 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 //! measured again in a box its own answer had decided, where the old answer
//! is a fixed point whatever the content now says. The last is neither: one //! is a fixed point whatever the content now says. The last three are
//! box length, composed two ways, landing either side of the boundary that //! neither: one box length, composed two ways, landing either side of the
//! decided whether a child was drawn at all. //! boundary that decided whether a child was drawn at all, and two boxes
//! reached through a region node's own entry rather than through the offer
//! that node was given. The last is a wrapping text handed back the width
//! it measured, rounded to a step below the line it measured there.
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch;
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// 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 /// the tree changes -- every widget is marked for redraw and the frame is
@@ -281,11 +285,11 @@ struct Wider {
impl Widget for Wider { impl Widget for Wider {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
Size { Size {
x: Len { x: LayoutLen {
px: painter.px_len(Axis::X) + Px::from_f32(self.extra), px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
..Len::ZERO ..LayoutLen::ZERO
}, },
y: Len::LEFTOVER, y: LayoutLen::LEFTOVER,
} }
} }
} }
@@ -341,7 +345,7 @@ fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
let whole = rect(Color::RED).add(&mut h.rsc); let whole = rect(Color::RED).add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_size_rules(whole, None, Some(Len::rel(1.0))); .set_size_rules(whole, None, Some(LayoutLen::rel(1.0)));
let mut inner_children: Vec<StrongWidget> = vec![ let mut inner_children: Vec<StrongWidget> = vec![
measured.add_strong(&mut h.rsc), measured.add_strong(&mut h.rsc),
whole.add_strong(&mut h.rsc), whole.add_strong(&mut h.rsc),
@@ -357,7 +361,7 @@ fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
.add(&mut h.rsc); .add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_size_rules(inner, None, Some(Len::px(198.0))); .set_size_rules(inner, None, Some(LayoutLen::px(198.0)));
// One more span above it: without a box composed through it, both trees // One more span above it: without a box composed through it, both trees
// round the same way and the boundary is never crossed. // round the same way and the boundary is never crossed.
let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc); let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc);
@@ -398,3 +402,273 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
} }
assert!(wrong.is_empty(), "{}", wrong.join("\n")); assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
/// 18 grows at depth 6. A scroll inside a scroll, the inner one owning a
/// movable region of its own, and only its text marked for redraw. Nothing
/// about the tree changes, so no box may.
fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
let text = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let inner = Scroll::new(text.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(inner.id(), true);
let filler = rect(Color::RED).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rules(
filler.id(),
Some(LayoutLen::px(87.0)),
Some(LayoutLen::px(24.0)),
);
let span = Span {
children: vec![inner.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)],
dir: Dir::DOWN,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let root = Scroll::new(span.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
h.set_root(root);
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
}
/// A local redraw asks a dirty widget in the box its parent gave it, and only
/// where that box is as long as the one it was offered; anything else is a
/// question its parent has to ask. This inner scroll's offer is the outer
/// scroll's whole viewport and the box it was given is 24px shorter -- the
/// height of the sized child the outer scroll snaps to the end of -- so what
/// it must not do is settle itself. It was drawn at its offer once, and the
/// inner scroll and its text stayed 24px too low.
#[test]
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
let mut warm = Harness::new((900, 1200));
let ids = plant_nested_scrolls(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(ids[0]);
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_nested_scrolls(&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"));
}
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
/// to. The pad owns a movable region and is the scroll's content, so the box
/// the scroll places it in is as long as that content while the box it was
/// offered is the viewport -- and with no padding to tell those two apart,
/// the span inside it looked like it was still at its offer. So everything
/// under the pad was asked again in the *placed* box, the offer resolving
/// against the node's own entry, which holds that box: the texts kept the
/// widths they had, the content stayed the length those widths make, and the
/// old answer confirmed itself. What the branch adds is a tree that differs
/// rather than a box that moved, since a probe measured at the wrong width
/// takes the other side.
fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
let probe = rect(Color::RED).add(&mut h.rsc);
let wide = rect(Color::GREEN).add(&mut h.rsc);
let narrow = rect(Color::BLUE).add(&mut h.rsc);
let branch = Branch {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold: 213.0,
}
.add(&mut h.rsc);
let wrapped = wtext(
"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.",
)
.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 row = |h: &mut Harness, mut children: Vec<StrongWidget>| {
if swapped {
children.rotate_left(1);
}
Span {
children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc)
};
let texts: Vec<StrongWidget> =
vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
let inner = row(h, texts);
let pair: Vec<StrongWidget> = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
let outer = row(h, pair);
let pad = Pad {
padding: Padding::ZERO,
inner: outer.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(pad.id(), true);
let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.set_root(root);
(
vec![
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
wrapped.id(),
plain.id(),
inner.id(),
outer.id(),
pad.id(),
root.id(),
],
[outer, inner],
)
}
#[test]
fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
let mut warm = Harness::new((900, 1200));
let (ids, spans) = plant_under_a_node(&mut warm, false);
warm.frame();
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_under_a_node(&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"));
}
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
box leaves room for, so a paragraph's height is an answer and not a setting.";
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
/// the span takes its width from the longest line of the texts in it, and
/// the text below the span is then wrapped at that width -- so a width the
/// shaper measured comes back to it as the box to break in.
fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget<Span>, WidgetId) {
let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc);
let mut inner = Span::empty(Dir::UP);
inner.children = match swapped {
true => swapped_in(h),
false => vec![first],
};
let inner = inner.height(142).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let stack = Stack {
children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::DOWN).width(195));
(inner, text.id())
}
/// What the span holds once its children have been swapped, which is what
/// the warm tree is changed to and what the cold one is grown with.
fn swapped_in(h: &mut Harness) -> Vec<StrongWidget> {
let paragraph = |h: &mut Harness| -> StrongWidget {
wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc)
};
vec![
paragraph(h),
rect(Color::YELLOW).add_strong(&mut h.rsc),
paragraph(h),
]
}
/// A text handed back the width it measured breaks there the way it broke
/// when it measured it. The width the shaper answers is not on the grid, and
/// a report rounded to the nearest step is under the longest line half the
/// time: a warm tree then keeps a break made in a wider box while a cold one
/// makes a narrower break in the same box, and the paragraph gains a line.
#[test]
fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
let mut warm = Harness::new((900, 1200));
let (inner, text) = plant_a_measured_width(&mut warm, false);
warm.frame();
warm.rsc[inner].children = swapped_in(&mut warm);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (_, cold_text) = plant_a_measured_width(&mut cold, true);
cold.frame();
assert_eq!(warm.region(&text), cold.region(&cold_text));
}
#[test]
fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
fn build(
h: &mut Harness,
changed: bool,
) -> (WeakWidget<Span>, WeakWidget<Text>, Vec<StrongWidget>) {
let wrap = wtext("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.").size(16).wrap(true).add_strong(&mut h.rsc);
let one = || {
wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
};
let plain = one().add_strong(&mut h.rsc);
let shared = one()
.width(LayoutLen::LEFTOVER)
.height(LayoutLen::LEFTOVER)
.add(&mut h.rsc);
let mut extra: Vec<StrongWidget> = vec![
rect(Color::RED).add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
];
let children: Vec<StrongWidget> = if changed {
let mut children: Vec<StrongWidget> = vec![plain, shared.add_strong(&mut h.rsc)];
children.append(&mut extra);
children
} else {
vec![wrap, plain, shared.add_strong(&mut h.rsc)]
};
let row = Span {
children,
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
let fill: StrongWidget = rect(Color::BLUE).add_strong(&mut h.rsc);
let children: Vec<StrongWidget> = vec![fill, row.add_strong(&mut h.rsc)];
let root = Span {
children,
dir: Dir::RIGHT,
gap: Px::from_int(4),
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
h.set_root(root);
(row, shared, extra)
}
let mut warm = Harness::new((900, 1200));
let (row, shared, extra) = build(&mut warm, false);
warm.rsc[row].children.remove(0);
warm.rsc[row].children.extend(extra);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other));
}
+3 -3
View File
@@ -16,8 +16,8 @@
use iris::prelude::*; use iris::prelude::*;
use iris_core::{ use iris_core::{
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState, Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
UiScalar, UiSpan, UiRenderState, UiSpan,
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
@@ -79,7 +79,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::px(v); let px = |v: f32| Len::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;
+80 -538
View File
@@ -1,19 +1,21 @@
//! Random trees, checked against building the same tree cold. //! Laying a tree out again has to land where growing it that way would.
//! //!
//! A frame reaches its layout by keeping most of the last one: movable regions //! Every case is one of `scenario`'s, over the trees `iris::random` grows
//! or primitive boxes rewritten, some widgets drawn again, the rest untouched. //! from a seed. The fast test takes a handful of seeds and the ignored one
//! The result must be the tree a cold start would have produced, so anything //! takes as many as it is asked for; both run the same cases the shrinker
//! wrongly retained shows up as a difference in somebody's box. //! does over the same trees, so a seed that fails here is reduced by
//! //!
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is //! SHRINK_SEED=<seed> SHRINK_DEPTH=<depth> SHRINK_CASE=<case> \
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep //! cargo test --release --test shrink -- --ignored --nocapture
//! for when it is worth spending the time. //!
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
//! select what the long run covers.
use std::collections::HashMap; #[path = "scenario/mod.rs"]
mod scenario;
use iris::harness::Harness; use iris::random::{Edits, plan};
use iris::prelude::*; use scenario::{ALL, Case, diverges, env, over_seeds};
use iris::random::{Aligns, Edits, Lens, Rng, SpanEdit, Tree, grow};
/// How deep the generator branches. The generator widens two to four ways per /// How deep the generator branches. The generator widens two to four ways per
/// level, so depth is exponential in width and a deep narrow tree is not /// level, so depth is exponential in width and a deep narrow tree is not
@@ -23,562 +25,102 @@ fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4) env("IRIS_GENERATED_DEPTH", 4)
} }
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T { /// The seeds the ordinary tests take. Seven that have never failed; 86,
std::env::var(name) /// which a `Scroll` fixed point once settled differently on; and 20, which
.ok() /// caught a locally redrawn widget being placed twice in the box its parent
.and_then(|value| value.parse().ok()) /// had already placed it in.
.unwrap_or(fallback) const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
}
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
/// The same box, to a step of the grid per level of nesting between the two fn check(seed: u64, depth: usize, case: Case) {
/// ways of reaching it. A move, a repaint and a row of shares land on the let grown = plan(seed, depth, &Edits::default());
/// same number now; what is left is a box centred in a fraction of its parent if let Some(how) = diverges(&grown, case, seed) {
/// against the same box centred in its own pixels. A step is a thousandth of panic!(
/// a pixel, where this was a twentieth of one before any of it was on a grid. "seed {seed} at depth {depth} differs after {}: {how}\n\
const AGREE_STEPS: i32 = 2; reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
SHRINK_CASE={} cargo test --release --test shrink -- --ignored --nocapture",
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool { case.name(),
match (got, want) { case.name(),
(Some(got), Some(want)) => { );
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
same(got.top_left.x, want.top_left.x)
&& same(got.top_left.y, want.top_left.y)
&& same(got.bot_right.x, want.bot_right.x)
&& same(got.bot_right.y, want.bot_right.y)
}
(None, None) => true,
_ => false,
} }
} }
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree { macro_rules! case {
let (root, tree) = grow(&mut h.rsc, seed, depth(), edits); ($name:ident, $case:expr) => {
h.state.root = Some(root); #[test]
h.frame(); fn $name() {
tree for seed in SEEDS {
check(seed, depth(), $case);
} }
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
let lens = [
Some(Len::px(20.0 + rng.below(180) as f32)),
Some(Len::px(20.0 + rng.below(180) as f32)),
];
h.rsc
.widgets_mut()
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
lens
}
/// Changes a few of the declared sizes, and says which, so the cold tree can
/// be grown with the same ones.
fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
let mut edits = HashMap::new();
for _ in 0..4 {
let idx = rng.below(tree.sized.len());
edits.insert(idx, resize_one(h, tree, idx, rng));
}
edits
}
/// Every declared size at once, so every reader of a size in the tree has a
/// changed descendant in the same frame and the whole dirty set has to settle
/// together.
fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
(0..tree.sized.len())
.map(|idx| (idx, resize_one(h, tree, idx, rng)))
.collect()
}
/// A way of changing what a span holds. Each is a shape worth its own case:
/// taking a child out of the middle is not the same as emptying a span, and
/// adding one is not the same as adding three.
#[derive(Clone, Copy, Debug)]
enum Shuffle {
/// Every other child, so what is left is interleaved with what went.
EveryOther,
/// Everything but the first, which is the last step before empty.
AllButFirst,
/// Three more on the end at once.
AddThree,
/// The first out and three more on, so the count moves both ways.
SwapForThree,
/// One out of the middle and one on the end.
TradeOne,
}
const SHUFFLES: [Shuffle; 5] = [
Shuffle::EveryOther,
Shuffle::AllButFirst,
Shuffle::AddThree,
Shuffle::SwapForThree,
Shuffle::TradeOne,
];
impl Shuffle {
fn of(self, grown: usize) -> SpanEdit {
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
match self {
Self::EveryOther => SpanEdit {
detach: all(2, 0),
attach: 0,
},
Self::AllButFirst => SpanEdit {
detach: all(1, 1),
attach: 0,
},
Self::AddThree => SpanEdit {
detach: Vec::new(),
attach: 3,
},
Self::SwapForThree => SpanEdit {
detach: vec![0],
attach: 3,
},
Self::TradeOne => SpanEdit {
detach: vec![grown / 2],
attach: 1,
},
}
}
}
/// Applies `shuffle` to every third span, and says what it did so the cold
/// tree can be grown that way. The widgets it takes out are given back: the
/// last share of one must outlive the comparison, or its id is handed to
/// something else and the two trees stop lining up.
fn reshuffle(
h: &mut Harness,
tree: &mut Tree,
shuffle: Shuffle,
) -> (HashMap<usize, SpanEdit>, Vec<StrongWidget>) {
let mut edits = HashMap::new();
let mut detached = Vec::new();
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
let span_edit = shuffle.of(span.grown);
let mut take = span_edit.detach.clone();
take.sort_unstable();
let children = &mut h.rsc[span.id].children;
// Highest first, so an index means the same child however many of
// its neighbours are going too.
for j in take.into_iter().rev() {
if j < children.len() {
detached.push(children.remove(j));
}
}
let attach = span_edit.attach.min(span.spares.len());
children.extend(span.spares.drain(..attach));
edits.insert(idx, span_edit);
}
(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 rules = h.rsc.widgets().size_rules(id);
let rule = |r: SizeRule| match r.known() {
Some(len) => format!("{len}"),
None => "-".into(),
};
let align = h.rsc.widgets().alignment(id);
let side = |a: AxisAlign| {
if a == AxisAlign::NEG {
"neg".into()
} else if a == AxisAlign::CENTER {
"mid".into()
} else if a == AxisAlign::POS {
"pos".into()
} else {
format!("{:.2}", a.rel())
} }
}; };
// A rule and an alignment are properties of whatever carries them, so
// they print with that widget rather than as widgets of their own.
let mut out = describe_widget(id, h);
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
}
if align != RegionAlign::default() {
out += &format!("@{},{}", side(align.x), side(align.y));
}
out
} }
fn describe_widget(id: WidgetId, h: &Harness) -> String { case!(
let label = h.rsc.widgets().label(id).to_string(); many_widgets_redrawing_at_once_leaves_every_box_where_it_was,
let Some(widget) = h.rsc.widgets().get_dyn(id) else { Case::RepaintSome
return label;
};
let any: &dyn std::any::Any = widget;
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()
); );
} case!(
if let Some(w) = any.downcast_ref::<Pad>() { everything_redrawing_at_once_leaves_every_box_where_it_was,
let p = &w.padding; Case::Repaint
return format!(
"Pad{{l:{},r:{},t:{},b:{}}}",
p.left, p.right, p.top, p.bottom
); );
} case!(
if let Some(w) = any.downcast_ref::<Stack>() { a_resize_lands_where_starting_at_that_size_would,
return format!("Stack{{n:{}}}", w.children.len()); Case::Resize
}
label
}
/// Every widget in one tree against the matching widget in the other. A
/// mismatch prints the widget's ancestry, marking region nodes, since where
/// two trees disagree is rarely where the cause is.
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
let ((wh, wt), (ch, ct)) = (warm, cold);
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
let mut drawn = 0;
let mut wrong = 0;
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
let (got, want) = (wh.region(&w), ch.region(&c));
drawn += usize::from(got.is_some());
// This oracle cares where rasterization lands, not whether equivalent
// arithmetic produced the same f32. Keep the tolerance to one
// twentieth of a physical pixel, while whether a widget drew remains
// exact.
if same_region(got, want) {
continue;
}
wrong += 1;
if wrong <= 3 {
let mut chain = Vec::new();
let mut at = Some(w);
while let Some(id) = at {
let active = &wh.render.active[&id];
let node = match active.move_idx == active.parent_move {
true => "",
false => "*",
};
chain.push(format!("{}{node}", describe(id, wh)));
at = active.parent;
}
println!(
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
chain.join(" < ")
); );
} case!(
} a_resize_and_a_repaint_land_where_starting_that_way_would,
assert!(drawn > 0, "seed {seed}: nothing was drawn"); Case::ResizeRepaint
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
}
fn changed_size(seed: u64) {
let mut warm = Harness::new((900, 1200));
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 sizes = edit(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
sizes,
..Default::default()
},
); );
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same)); case!(
} a_size_change_after_a_resize_lands_the_same_way,
Case::ResizeSize
/// Moves one widget to a different corner of the box it is given.
fn realign_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
let mut side = || match rng.below(4) {
0 => None,
1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS),
};
let aligns = [side(), side()];
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(aligns) {
h.rsc
.widgets_mut()
.set_alignment(tree.aligned[idx], axis, align.unwrap_or_default());
}
aligns
}
fn changed_alignment(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
if grown.aligned.is_empty() {
return;
}
let mut rng = Rng::new(seed ^ 0xa11);
let aligns = (0..grown.aligned.len())
.step_by(3)
.map(|idx| (idx, realign_one(&mut warm, &grown, idx, &mut rng)))
.collect();
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
aligns,
..Default::default()
},
); );
assert_same(seed, "an alignment change", (&warm, &grown), (&cold, &same)); case!(
} a_size_change_lands_where_growing_it_that_way_would,
Case::Size
/// Giving a widget a movable region of its own, or taking it away, is a
/// structural change: every primitive under it changes which chain resolves
/// it. A cold tree built that way is what says the rebuild was complete.
fn changed_region_node(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
if grown.nodes.is_empty() {
return;
}
let nodes: HashMap<usize, bool> = (0..grown.nodes.len())
.step_by(2)
.map(|idx| {
let id = grown.nodes[idx];
let was = warm.rsc.widgets().is_region_node(id);
warm.rsc.widgets_mut().set_region_node(id, !was);
(idx, !was)
})
.collect();
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
nodes,
..Default::default()
},
); );
assert_same( case!(
seed, every_size_changing_at_once_lands_where_growing_it_that_way_would,
"a region-node change", Case::EverySize
(&warm, &grown),
(&cold, &same),
); );
} case!(
an_alignment_change_lands_where_growing_it_that_way_would,
fn reshuffled(seed: u64, shuffle: Shuffle) { Case::Align
let mut warm = Harness::new((900, 1200));
let mut grown = plant(&mut warm, seed, &Edits::default());
// 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. A span behind
// 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;
}
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
spans,
..Default::default()
},
); );
case!(
let what = format!("{shuffle:?}"); giving_and_taking_a_movable_region_rebuilds_what_resolves_it,
assert_same(seed, &what, (&warm, &grown), (&cold, &same)); Case::RegionNode
}
fn changed_every_size(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
if grown.sized.is_empty() {
return;
}
let mut rng = Rng::new(seed ^ 0xa11);
let sizes = edit_every(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
sizes,
..Default::default()
},
); );
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same)); case!(
} reordering_a_span_lands_where_growing_it_that_way_would,
Case::Reorder
/// Marks a spread of widgets for redraw at once. Nothing changes, so no box
/// may either; what this exercises is the order a frame settles a dirty set
/// in, which the other cases reach one dependency path at a time.
fn repainted_together(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
for &id in grown.ids.iter().step_by(5) {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
assert!(
!warm.rsc.widgets().needs_redraw.is_empty(),
"seed {seed}: nothing was marked"
); );
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(&mut cold, seed, &Edits::default());
let what = "many repaints at once";
assert_same(seed, what, (&warm, &grown), (&cold, &same));
}
fn resized(seed: u64) {
let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let same = plant(&mut cold, seed, &Edits::default());
assert_same(seed, "a resize", (&warm, &grown), (&cold, &same));
}
fn resized_then_changed(seed: u64) {
let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
if grown.sized.is_empty() {
return;
}
warm.resize((640, 900));
warm.frame();
let mut rng = Rng::new(seed ^ 0xb0a7);
let sizes = edit(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((640, 900));
let same = plant(
&mut cold,
seed,
&Edits {
sizes,
..Default::default()
},
);
let what = "a resize then a size change";
assert_same(seed, what, (&warm, &grown), (&cold, &same));
}
#[test]
fn a_changed_size_lands_where_growing_it_that_way_would() {
SEEDS.into_iter().for_each(changed_size);
}
#[test]
fn a_changed_alignment_lands_where_growing_it_that_way_would() {
SEEDS.into_iter().for_each(changed_alignment);
}
#[test]
fn a_toggled_region_node_lands_where_growing_it_that_way_would() {
SEEDS.into_iter().for_each(changed_region_node);
}
#[test]
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
SEEDS.into_iter().for_each(changed_every_size);
}
#[test]
fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() {
SEEDS.into_iter().for_each(repainted_together);
}
#[test]
fn a_resize_lands_where_starting_at_that_size_would() {
SEEDS.into_iter().for_each(resized);
}
#[test]
fn a_size_change_after_a_resize_lands_the_same_way() {
SEEDS.into_iter().for_each(resized_then_changed);
}
#[test] #[test]
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() { fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
for shuffle in SHUFFLES { for case in ALL {
if matches!(case, Case::Shuffle(_)) {
for seed in SEEDS { for seed in SEEDS {
reshuffled(seed, shuffle); check(seed, depth(), case);
}
} }
} }
} }
/// The same property over a hundred seeds and every scenario. What it has
/// 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
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
/// small enough to read.
#[test] #[test]
#[ignore = "a hundred seeds, rather than the nine the others check"] #[ignore = "as many seeds as it is asked for, rather than the nine the others check"]
fn a_long_run_of_seeds_agrees() { fn a_long_run_of_seeds_agrees() {
let seeds = std::env::var("IRIS_GENERATED_SEED") let depth = depth();
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
.ok() .ok()
.and_then(|seed| seed.parse().ok()) .and_then(|v| v.parse().ok())
.map(|seed| seed..=seed) {
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100)); Some(seed) => vec![seed],
over_seeds(seeds.collect(), |seed| { None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
changed_size(seed); };
changed_every_size(seed); over_seeds(seeds, |seed| {
repainted_together(seed); for case in ALL {
resized(seed); check(seed, depth, case);
resized_then_changed(seed);
for shuffle in SHUFFLES {
reshuffled(seed, shuffle);
}
});
}
/// Every seed on its own thread's share of them. A tree is grown, laid out
/// and dropped inside one call, so seeds share nothing, and this is most of
/// the time a run takes. A thread that fails takes the scope down with it,
/// which is the same panic libtest would have seen.
///
/// One core short of all of them, so the machine this runs on stays usable.
pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
let threads =
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
let chunk = seeds.len().div_ceil(threads).max(1);
std::thread::scope(|scope| {
for part in seeds.chunks(chunk) {
let run = &run;
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
} }
}); });
} }
+19 -7
View File
@@ -5,11 +5,11 @@
//! cargo test --release --features layout-diagnostics \ //! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture //! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! Uninstrumented hardware totals for one phase: //! Build the uninstrumented test with `cargo test --release --test
//! layout_diagnostics --no-run`, then run the emitted executable directly:
//! //!
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \ //! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
//! -e cycles:u,instructions:u cargo test --release \ //! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
//! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or //! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and //! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
@@ -92,9 +92,18 @@ fn trace_selected(tree: &Tree) {
#[cfg(not(feature = "layout-diagnostics"))] #[cfg(not(feature = "layout-diagnostics"))]
fn trace_selected(_: &Tree) {} fn trace_selected(_: &Tree) {}
/// The shape a cost is measured on must not depend on what layout measured,
/// or two commits are compared on two different trees. See `Edits`.
fn rig_edits() -> Edits {
Edits {
fixed_branches: true,
..Default::default()
}
}
fn warm(seed: u64, depth: usize) -> (Harness, Tree) { fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
harness.state.root = Some(root); harness.state.root = Some(root);
harness.frame(); harness.frame();
println!( println!(
@@ -125,6 +134,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
{ {
let diagnostics = iris::core::layout_diagnostics::take(); let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames)); print!("{}", diagnostics.per_frame(frames));
for event in diagnostics.traces() {
println!(" {event:?}");
}
for callsite in diagnostics.hot_text().iter().take(3) { for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new(); let mut ancestry = Vec::new();
let mut id = Some(callsite.id); let mut id = Some(callsite.id);
@@ -173,7 +185,7 @@ fn layout_cost() {
if selected("cold") { if selected("cold") {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
harness.state.root = Some(root); harness.state.root = Some(root);
println!( println!(
"fixture: seed {seed}, depth {depth}, {} widgets", "fixture: seed {seed}, depth {depth}, {} widgets",
@@ -216,7 +228,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| {
let len = Len::px(100.0 + (frame % 2) as f32 * 40.0); let len = LayoutLen::px(100.0 + (frame % 2) as f32 * 40.0);
harness harness
.rsc .rsc
.widgets_mut() .widgets_mut()
+5 -1
View File
@@ -94,7 +94,11 @@ 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(rect(Color::RED).width(Len::px(40.0)).add_strong(&mut h.rsc)); row.push(
rect(Color::RED)
.width(LayoutLen::px(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)
+438
View File
@@ -0,0 +1,438 @@
//! The scenarios both fuzzers run, over the tree a [`Plan`] describes.
//!
//! One implementation rather than two. The oracle grew its trees from a seed
//! and the shrinker grew its own, with every scenario written out on each
//! side, so a failure the oracle found could not be handed to the shrinker:
//! there was no tree to pass it, only a seed, and a seed cannot be made
//! smaller. Both take a plan now, so whatever finds a counterexample can also
//! reduce it.
//!
//! Each target compiles this for itself, so what only one of them calls is
//! dead code in the other.
#![allow(dead_code)]
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build};
use std::collections::HashMap;
/// A seed per thread but one, since a seed grows, lays out and drops its tree
/// alone. A failing seed still shrinks and panics on its own thread.
pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
let threads =
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
let chunk = seeds.len().div_ceil(threads).max(1);
std::thread::scope(|scope| {
for part in seeds.chunks(chunk) {
let run = &run;
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
}
});
}
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
/// The window a tree is grown in, and the one a resize takes it to.
const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0);
const STILL: (f32, f32) = (900.0, 1200.0);
/// A way of changing what a span holds. Each is a shape worth its own case:
/// taking a child out of the middle is not the same as emptying a span, and
/// adding one is not the same as adding three.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Shuffle {
/// Every other child, so what is left is interleaved with what went.
EveryOther,
/// Everything but the first, which is the last step before empty.
AllButFirst,
/// Three more on the end at once.
AddThree,
/// The first out and three more on, so the count moves both ways.
SwapForThree,
/// One out of the middle and one on the end.
TradeOne,
}
impl Shuffle {
fn of(self, grown: usize) -> SpanEdit {
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
match self {
Self::EveryOther => SpanEdit {
detach: all(2, 0),
attach: 0,
},
Self::AllButFirst => SpanEdit {
detach: all(1, 1),
attach: 0,
},
Self::AddThree => SpanEdit {
detach: Vec::new(),
attach: 3,
},
Self::SwapForThree => SpanEdit {
detach: vec![0],
attach: 3,
},
Self::TradeOne => SpanEdit {
detach: vec![grown / 2],
attach: 1,
},
}
}
}
/// What a warm tree is put through before it is compared with a cold one
/// grown the way it was left.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Case {
/// Nothing changes, so no box may either. What this exercises is the
/// order a frame settles a dirty set in.
Repaint,
/// Every fifth widget rather than all of them: marking all of them
/// redraws the whole tree, which is a cold start reached the long way,
/// where the mixed case leaves a redrawn subtree beside a retained one.
RepaintSome,
Resize,
ResizeRepaint,
/// A resize and then a size change, so a retained answer is asked to
/// survive two different kinds of invalidation in a row.
ResizeSize,
/// A few declared sizes.
Size,
/// Every declared size at once, so every reader of a size has a changed
/// descendant in the same frame and the whole dirty set settles together.
EverySize,
Align,
/// Giving a widget a movable region of its own, or taking it away, is a
/// structural change: every primitive under it changes which chain
/// resolves it.
RegionNode,
/// The same children in a different order, which moves every one of them
/// without changing what any of them is.
Reorder,
Shuffle(Shuffle),
}
pub const ALL: [Case; 15] = [
Case::Repaint,
Case::RepaintSome,
Case::Resize,
Case::ResizeRepaint,
Case::ResizeSize,
Case::Size,
Case::EverySize,
Case::Align,
Case::RegionNode,
Case::Reorder,
Case::Shuffle(Shuffle::EveryOther),
Case::Shuffle(Shuffle::AllButFirst),
Case::Shuffle(Shuffle::AddThree),
Case::Shuffle(Shuffle::SwapForThree),
Case::Shuffle(Shuffle::TradeOne),
];
impl Case {
/// The name `CASE` selects it by, and the one a failure prints.
pub fn name(self) -> &'static str {
match self {
Self::Repaint => "repaint",
Self::RepaintSome => "repaint-some",
Self::Resize => "resize",
Self::ResizeRepaint => "resize-repaint",
Self::ResizeSize => "resize-size",
Self::Size => "size",
Self::EverySize => "every-size",
Self::Align => "align",
Self::RegionNode => "region-node",
Self::Reorder => "reorder",
Self::Shuffle(Shuffle::EveryOther) => "shuffle-every-other",
Self::Shuffle(Shuffle::AllButFirst) => "shuffle-all-but-first",
Self::Shuffle(Shuffle::AddThree) => "shuffle-add-three",
Self::Shuffle(Shuffle::SwapForThree) => "shuffle-swap-for-three",
Self::Shuffle(Shuffle::TradeOne) => "shuffle-trade-one",
}
}
pub fn named(name: &str) -> Option<Self> {
ALL.into_iter().find(|case| case.name() == name)
}
/// Grown in the first, compared in the second.
fn window(self) -> ((f32, f32), (f32, f32)) {
match self {
Self::Resize | Self::ResizeRepaint | Self::ResizeSize => (OUTER, INNER),
_ => (STILL, STILL),
}
}
}
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
for &id in tree.ids.iter().step_by(step) {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
}
fn a_len(rng: &mut Rng) -> Option<LayoutLen> {
Some(LayoutLen::px(20.0 + rng.below(180) as f32))
}
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
let lens = [a_len(rng), a_len(rng)];
warm.rsc
.widgets_mut()
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
lens
}
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
let side = |rng: &mut Rng| match rng.below(4) {
0 => None,
1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS),
};
let align = [side(rng), side(rng)];
let id = tree.aligned[idx];
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
warm.rsc
.widgets_mut()
.set_alignment(id, axis, align.unwrap_or_default());
}
align
}
/// Every span's children in a different order, said both to the warm tree and
/// to the plan the cold one is grown from.
fn reorder(warm: &mut Harness, tree: &Tree, plan: &Plan) -> Plan {
for span in &tree.spans {
let children = &mut warm.rsc[span.id].children;
if !children.is_empty() {
children.rotate_left(1);
}
}
let mut out = plan.clone();
out.walk_mut(&mut |node| {
if let Kind::Span { order, .. } = &mut node.kind
&& !order.is_empty()
{
order.rotate_left(1);
}
});
out
}
/// Applies `shuffle` to every third span. What it takes out is given back to
/// the span's spares: the last share of a widget must outlive the comparison,
/// or its id is handed to something else and the two trees stop lining up.
fn reshuffle(warm: &mut Harness, tree: &mut Tree, shuffle: Shuffle) -> HashMap<usize, SpanEdit> {
let mut edits = HashMap::new();
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
let edit = shuffle.of(span.grown);
let mut take = edit.detach.clone();
take.sort_unstable();
let children = &mut warm.rsc[span.id].children;
// Highest first, so an index means the same child however many of its
// neighbours are going too.
for j in take.into_iter().rev() {
if j < children.len() {
span.spares.push(children.remove(j));
}
}
let attach = edit.attach.min(span.spares.len());
let moved: Vec<_> = span.spares.drain(..attach).collect();
warm.rsc[span.id].children.extend(moved);
edits.insert(idx, edit);
}
edits
}
/// Changes the warm tree and answers with the plan a cold tree grown that way
/// comes from. Each arm settles its own frame, so a case that changes nothing
/// does not get a second one that could settle what the first left.
fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mut Rng) -> Plan {
let some_sizes = |warm: &mut Harness, tree: &Tree, rng: &mut Rng| {
let mut sizes = HashMap::new();
for _ in 0..4 {
if tree.sized.is_empty() {
break;
}
let idx = rng.below(tree.sized.len());
sizes.insert(idx, resize_one(warm, tree, idx, rng));
}
sizes
};
let edits = match case {
Case::Resize => return plan.clone(),
Case::Repaint | Case::ResizeRepaint => {
mark(warm, tree, 1);
warm.frame();
return plan.clone();
}
Case::RepaintSome => {
mark(warm, tree, 5);
warm.frame();
return plan.clone();
}
Case::Reorder => {
let out = reorder(warm, tree, plan);
warm.frame();
return out;
}
Case::Size | Case::ResizeSize => Edits {
sizes: some_sizes(warm, tree, rng),
..Default::default()
},
Case::EverySize => Edits {
sizes: (0..tree.sized.len())
.map(|idx| (idx, resize_one(warm, tree, idx, rng)))
.collect(),
..Default::default()
},
Case::Align => Edits {
aligns: (0..tree.aligned.len())
.step_by(3)
.map(|idx| (idx, realign_one(warm, tree, idx, rng)))
.collect(),
..Default::default()
},
Case::RegionNode => {
let mut nodes = HashMap::new();
for idx in (0..tree.nodes.len()).step_by(2) {
let id = tree.nodes[idx];
let take = !warm.rsc.widgets().is_region_node(id);
warm.rsc.widgets_mut().set_region_node(id, take);
nodes.insert(idx, take);
}
Edits {
nodes,
..Default::default()
}
}
Case::Shuffle(shuffle) => Edits {
spans: reshuffle(warm, tree, shuffle),
..Default::default()
},
};
warm.frame();
plan.edited(&edits)
}
/// What a widget was configured with, so a tree a fuzzer found can be written
/// out by hand. A 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 rules = h.rsc.widgets().size_rules(id);
let rule = |r: SizeRule| match r.exact() {
Some(len) => format!("{len}"),
None => "-".into(),
};
let align = h.rsc.widgets().alignment(id);
let side = |a: AxisAlign| {
if a == AxisAlign::NEG {
"neg".into()
} else if a == AxisAlign::CENTER {
"mid".into()
} else if a == AxisAlign::POS {
"pos".into()
} else {
format!("{:.2}", a.rel())
}
};
// A rule and an alignment are properties of whatever carries them, so
// they print with that widget rather than as widgets of their own.
let mut out = describe_widget(id, h);
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
}
if align != RegionAlign::default() {
out += &format!("@{},{}", side(align.x), side(align.y));
}
out
}
fn describe_widget(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;
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::<Stack>() {
return format!("Stack{{n:{}}}", w.children.len());
}
label
}
/// Runs `case` on the tree `plan` describes, warm and cold, and says where
/// the two disagree. `seed` chooses only the values a case picks at random,
/// so one plan under one case is one comparison however it was reached.
pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
let (start, end) = case.window();
let mut warm = Harness::new(start);
let (root, mut tree) = build(&mut warm.rsc, plan);
warm.state.root = Some(root);
// The frame that makes it warm: without it nothing is retained and the
// comparison is two cold starts agreeing with each other.
warm.frame();
if start != end {
warm.resize(end);
warm.frame();
}
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
let mut cold = Harness::new(end);
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
cold.state.root = Some(root);
cold.frame();
let mut drawn = 0;
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
drawn += got.is_some() as usize;
if got == want {
continue;
}
// Where two trees disagree is rarely where the cause is, so the
// ancestry comes with it, marking the widgets that own a region.
let mut chain = Vec::new();
let mut at = Some(w);
while let Some(id) = at {
let active = &warm.render.active[&id];
let node = match active.move_idx == active.parent_move {
true => "",
false => "*",
};
chain.push(format!("{}{node}", describe(id, &warm)));
at = active.parent;
}
return Some(format!(
"widget {i}\n warm {got:?}\n cold {want:?}\n {}",
chain.join(" < ")
));
}
match drawn {
0 => Some("nothing was drawn".into()),
_ => None,
}
}
+61 -581
View File
@@ -1,556 +1,38 @@
//! A property test that shrinks its own counterexample. //! A fuzzer that reduces its own counterexample.
//! //!
//! `generated.rs` reproduces a failure from a seed, but a seed is not a lead //! A seed is not a lead anybody can read: the tree is hundreds of widgets,
//! anybody can read: the tree is hundreds of widgets, and reconstructing the //! and reconstructing the part that matters by hand has failed every time it
//! part that matters by hand has failed every time it has been tried. This //! has been tried. This grows the trees `iris::random` describes, takes them
//! grows trees it can take apart, so a failure is reduced to the smallest //! apart, and prints the smallest one that still fails as something to write
//! tree that still shows it and printed as something to write a fast test //! a fast test from.
//! from.
//! //!
//! cargo test --release --test shrink -- --ignored --nocapture //! cargo test --release --test shrink -- --ignored --nocapture
//! //!
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow //! `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 //! them, `SHRINK_CASE` which scenario or `all` for every one. `SHRINK_SEED`
//! ordinary tests pass, and turn what it finds into a test of its own rather //! takes a single seed, which is how a failure `generated` printed is handed
//! than leaving a seed as the record. //! straight here: the two run the same cases over the same trees, so a seed
//! that fails there fails here and is reduced.
//!
//! 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; #[path = "scenario/mod.rs"]
use iris::prelude::*; mod scenario;
use iris::random::{Branch, Rng};
/// The same two leaves `iris::random` grows, since only one of them reads the use iris::random::{Edits, Plan, plan};
/// width it is given and that is the difference that matters. use scenario::{ALL, Case, diverges, env, over_seeds};
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"; /// Takes the first simplification that still fails, until none does. The
/// simplifications come biggest first, so this walks down rather than
const OUTER: (f32, f32) = (1920.0, 1200.0); /// nibbling: a six-hundred-widget tree reaches single figures in a few
/// Steps of the grid two ways of reaching a box may differ by: one per level /// hundred builds.
/// of nesting between them, and these trees are five deep. See fn shrink(mut node: Plan, case: Case, seed: u64) -> Plan {
/// `docs/HANDOFF.md`'s "Fixed point" in `ai-app-2` for what is left.
const AGREE_STEPS: i32 = 2;
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>>,
sized: &mut Vec<WidgetId>,
) -> 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, sized)))
.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: Px::from_f32(*gap),
}
.add(&mut h.rsc);
// A row takes the height it is given; a column is as wide
// as its widest child, which needs no rule.
if !*down {
h.rsc
.widgets_mut()
.set_size_rules(handle, None, Some(Len::rel(1.0)));
}
spans.push(handle);
handle.add_strong(&mut h.rsc)
}
Node::Stack(kids) => {
let children = kids.iter().map(|k| k.build(h, out, spans, sized)).collect();
Stack {
children,
size: StackSize::Child(0),
}
.add_strong(&mut h.rsc)
}
Node::Pad(p, kid) => {
let inner = kid.build(h, out, spans, sized);
Pad {
padding: Padding::uniform(*p),
inner,
}
.add_strong(&mut h.rsc)
}
Node::Aligned(x, y, kid) => {
let inner = kid.build(h, out, spans, sized);
for (axis, align) in [(Axis::X, axis_align(*x)), (Axis::Y, axis_align(*y))] {
if let Some(align) = align {
h.rsc.widgets_mut().set_alignment(&inner, axis, align);
}
}
inner
}
Node::Sized(x, y, kid) => {
let inner = kid.build(h, out, spans, sized);
h.rsc.widgets_mut().set_size_rules(&inner, *x, *y);
sized.push(inner.id());
inner
}
Node::Scroll(down, kid) => {
let inner = kid.build(h, out, spans, sized);
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, sized);
let wide = a.build(h, out, spans, sized);
let narrow = b.build(h, out, spans, sized);
Branch {
probe,
wide,
narrow,
threshold: *at,
}
.add_strong(&mut h.rsc)
}
};
out.push(id.id());
id
}
/// The lengths every `Sized` node would carry after `resized`, in the
/// order `build` pushes them.
fn sized_lens(&self, out: &mut Vec<(Option<Len>, Option<Len>)>) {
match self {
Node::Text(..) | Node::OneLine | Node::Rect => {}
Node::Span(_, _, kids, _) | Node::Stack(kids) => {
kids.iter().for_each(|k| k.sized_lens(out));
}
Node::Pad(_, k) | Node::Aligned(_, _, k) | Node::Scroll(_, k) => k.sized_lens(out),
Node::Sized(x, y, k) => {
k.sized_lens(out);
out.push((resized_len(*x), resized_len(*y)));
}
Node::Branch(p, a, b, _) => {
p.sized_lens(out);
a.sized_lens(out);
b.sized_lens(out);
}
}
}
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::LEFTOVER),
_ => 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::LEFTOVER),
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,
SizeChange,
}
/// A different declared length, kept the same kind so the change is to the
/// value alone.
fn resized_len(len: Option<Len>) -> Option<Len> {
let half = Rel::from_f32(0.5);
len.map(|len| Len {
px: len.px.mul(half) + Px::from_int(13),
rel: len.rel.mul(half),
leftover: len.leftover,
})
}
/// Every declared size changed, as a tree rather than as a change.
fn resized(node: &Node) -> Node {
match node {
Node::Span(down, gap, kids, order) => Node::Span(
*down,
*gap,
kids.iter().map(resized).collect(),
order.clone(),
),
Node::Stack(kids) => Node::Stack(kids.iter().map(resized).collect()),
Node::Pad(p, k) => Node::Pad(*p, Box::new(resized(k))),
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(resized(k))),
Node::Sized(x, y, k) => Node::Sized(resized_len(*x), resized_len(*y), Box::new(resized(k))),
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(resized(k))),
Node::Branch(p, a, b, at) => Node::Branch(
Box::new(resized(p)),
Box::new(resized(a)),
Box::new(resized(b)),
*at,
),
leaf => leaf.clone(),
}
}
/// 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 mut warm_sized = Vec::new();
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans, &mut warm_sized);
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();
}
if case == Case::SizeChange {
let mut lens = Vec::new();
node.sized_lens(&mut lens);
for (id, (x, y)) in warm_sized.iter().zip(lens) {
warm.rsc.widgets_mut().set_size_rules(*id, x, y);
}
warm.frame();
}
// What the warm tree was moved into, grown that way from the start.
let want = match case {
Case::Reorder => reordered(node),
Case::SizeChange => resized(node),
_ => node.clone(),
};
let mut cold = Harness::new(INNER);
let mut cold_ids = Vec::new();
let mut cold_spans = Vec::new();
let mut cold_sized = Vec::new();
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans, &mut cold_sized);
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));
// To a couple of steps of the grid, each a thousandth of a pixel: a
// move or a resize lands on the same number now, and a length
// measured one way against the same length composed another can
// still be a step out per composition between them.
let same = match (got, want) {
(Some(g), Some(c)) => {
let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
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 { loop {
let Some(next) = node let Some(next) = node
.smaller() .smaller()
.into_iter() .into_iter()
.find(|small| diverges(small, case).is_some()) .find(|small| diverges(small, case, seed).is_some())
else { else {
return node; return node;
}; };
@@ -558,62 +40,60 @@ fn shrink(mut node: Node, case: Case) -> Node {
} }
} }
/// One thread per core but one, each taking a share of the seeds: a tree is fn cases() -> Vec<Case> {
/// grown, laid out and dropped within a seed, so nothing is shared. A seed match env("SHRINK_CASE", String::from("all")).as_str() {
/// that fails shrinks on its own thread and panics there, which brings the "all" => ALL.to_vec(),
/// scope down with it. name => match Case::named(name) {
fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) { Some(case) => vec![case],
let threads = None => panic!(
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1)); "unknown SHRINK_CASE {name:?}; one of all, {}",
let chunk = seeds.len().div_ceil(threads).max(1); ALL.map(Case::name).join(", ")
std::thread::scope(|scope| { ),
for part in seeds.chunks(chunk) { },
let run = &run;
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
} }
});
}
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] #[test]
#[ignore = "a fuzzer; run it once the ordinary tests pass"] #[ignore = "a fuzzer; run it once the ordinary tests pass"]
fn no_grown_tree_lays_out_differently_warm_than_cold() { 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 depth: usize = env("SHRINK_DEPTH", 5);
let case = match env("SHRINK_CASE", String::from("resize")).as_str() { let cases = cases();
"repaint" => Case::Repaint, let seeds: Vec<u64> = match std::env::var("SHRINK_SEED")
"resize-repaint" => Case::ResizeRepaint, .ok()
"reorder" => Case::Reorder, .and_then(|v| v.parse().ok())
"size-change" => Case::SizeChange, {
_ => Case::Resize, Some(seed) => vec![seed],
None => (1..=env("SHRINK_SEEDS", 400_u64)).collect(),
}; };
let count = seeds.len();
over_seeds((1..=seeds).collect(), |seed| { over_seeds(seeds, |seed| {
let node = grow(&mut Rng::new(seed), depth); let grown = plan(seed, depth, &Edits::default());
let Some(how) = diverges(&node, case) else { for &case in &cases {
return; let Some(how) = diverges(&grown, case, seed) else {
continue;
}; };
let small = shrink(node.clone(), case); let small = shrink(grown.clone(), case, seed);
println!( println!(
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}", "seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
node.size(), case.name(),
grown.size(),
small.size() small.size()
); );
panic!("seed {seed} lays out differently warm than cold"); panic!(
"seed {seed} lays out differently warm than cold after {}",
case.name()
);
}
}); });
let sizes: Vec<usize> = (1..=seeds)
.map(|seed| grow(&mut Rng::new(seed), depth).size()) let sizes: Vec<usize> = (1..=count as u64)
.map(|seed| plan(seed, depth, &Edits::default()).size())
.collect(); .collect();
let total: usize = sizes.iter().sum();
println!( println!(
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}", "{count} trees at depth {depth} agree over {} case(s): {} widgets total, largest {}",
total, cases.len(),
sizes.iter().sum::<usize>(),
sizes.iter().max().copied().unwrap_or(0) sizes.iter().max().copied().unwrap_or(0)
); );
} }
+2
View File
@@ -16,6 +16,8 @@ mod drift;
mod idempotence; mod idempotence;
#[path = "cases/layout.rs"] #[path = "cases/layout.rs"]
mod layout; mod layout;
#[path = "cases/plan.rs"]
mod plan;
#[path = "cases/pointer.rs"] #[path = "cases/pointer.rs"]
mod pointer; mod pointer;
#[path = "cases/pointer_routing.rs"] #[path = "cases/pointer_routing.rs"]