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
36 changed files with 2729 additions and 857 deletions

No files matched your search

+67 -23
View File
@@ -10,10 +10,10 @@ use std::{
/// 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
/// 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
/// multiply or a conversion rounds, back onto the same steps, so two routes
/// that come within half a step land on one number and everything downstream
/// compares for equality instead of for nearness.
/// rather than where. Here adding and subtracting are exact, a multiply
/// drops to the step below, and a conversion between grids takes the nearest
/// one, so two routes to one place land on one number and everything
/// downstream compares for equality instead of for nearness.
///
/// `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
@@ -31,9 +31,9 @@ use std::{
)]
pub struct Fixed<const SHIFT: u32>(i32);
/// A length or a coordinate in pixels, to a sixty-fourth. Finer than anything
/// a display can show, and exact in `f32` up to 262,144 px, which is what lets
/// the same number reach the GPU.
/// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
/// anything a display can show, and exact in `f32` up to 16,384 px, which is
/// what lets the same number reach the GPU.
pub type Px = Fixed<PX_SHIFT>;
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
@@ -109,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
/// the other place a value enters the grid.
pub fn from_num(v: impl UiNum) -> Self {
@@ -142,18 +154,16 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// 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
/// 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 {
Self(shift_round(self.0 as i64 * by.0 as i64, BY) as i32)
}
/// A part of a span that is often nothing: no part of nothing is
/// nothing, for the cost of a comparison rather than a widening
/// multiply and a rounding.
pub const fn scaled<const BY: u32>(self, by: Fixed<BY>) -> Self {
match self.0 == 0 {
true => self,
false => self.mul(by),
}
Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
}
/// Repeated a whole number of times, which no grid rounds.
@@ -198,7 +208,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// `from` and `to` a fraction of the way apart, the fraction being the
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
from.add(to.sub(from).scaled(self))
from.add(to.sub(from).mul(self))
}
pub const fn min(self, other: Self) -> Self {
@@ -374,6 +384,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
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 {
Vec2::new(self.x.to_f32(), self.y.to_f32())
}
@@ -465,22 +481,50 @@ mod tests {
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]
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.
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));
}
/// 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]
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 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));
}
/// 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]
fn a_ceiling_never_lands_below_the_number_it_came_from() {
let step = 1.0 / (1 << PX_SHIFT) as f32;
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);
+7 -1
View File
@@ -54,10 +54,13 @@ pub(crate) enum Counter {
TextShapes,
TextBreaks,
GlyphPlacements,
OutsidePlacement,
OutsideFrame,
OutsideExtent,
}
impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1;
const COUNT: usize = Self::OutsideExtent as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"updates",
@@ -89,6 +92,9 @@ impl Counter {
"text shapes",
"text line breaks",
"glyph placements",
"reuse outside: the placement it was pinned to",
"reuse outside: a frame length",
"reuse outside: an extent length",
];
}
+1 -2
View File
@@ -84,8 +84,7 @@ pub struct RegionAlign {
}
impl RegionAlign {
/// Both axes at the near edge. What a container passes as an override for
/// a child it is going to position itself.
/// Both axes at the near edge: the start of a box in its own orientation.
pub const NEAR: Self = Self {
x: AxisAlign::NEG,
y: AxisAlign::NEG,
+11
View File
@@ -7,6 +7,17 @@ pub enum Axis {
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 {
type Output = Self;
+19
View File
@@ -125,6 +125,13 @@ impl Size {
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 LayoutLen {
@@ -151,6 +158,18 @@ impl LayoutLen {
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 {
Self {
px: Px::from_num(px),
+6 -20
View File
@@ -219,7 +219,7 @@ impl Len {
}
}
pub fn within_len(&self, len: Len) -> Self {
pub const fn within_len(&self, len: Len) -> Self {
self.within(&UiSpan {
start: Len::ZERO,
end: len,
@@ -282,26 +282,12 @@ impl UiSpan {
self.end += offset;
}
/// The whole of the box it sits in: a span that composes to nothing and
/// a parent that changes nothing.
pub const fn is_full(&self) -> bool {
self.start.rel.raw() == Rel::ZERO.raw()
&& self.start.px.raw() == Px::ZERO.raw()
&& self.end.rel.raw() == Rel::ONE.raw()
&& self.end.px.raw() == Px::ZERO.raw()
}
/// 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 {
// 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 rounds to what it started as -- and both are common enough to
// be worth four comparisons rather than four multiplies to find out.
if self.is_full() {
return *parent;
}
if parent.is_full() {
return *self;
}
Self {
start: self.start.within(parent),
end: self.end.within(parent),
+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.
pub struct TextBuffer {
text: String,
@@ -183,6 +176,23 @@ impl TextBuffer {
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 {
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
// to the longest line it produced: each line still fits, and none can
// take a word that would not fit in the wider box. So the layout in
// hand already answers, and re-breaking would only be a chance to
// disagree with itself -- which is what happens when a parent offers
// a child the length that child just reported, and the two land
// either side of a float.
// hand already answers, and re-breaking would only be work.
//
// At the longest line exactly, with no margin below it. A narrower
// 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
&& key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at
&& want + BREAK_EPSILON_PX >= self.layout.width()
&& want >= self.layout.width()
{
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
+2 -1
View File
@@ -106,7 +106,8 @@ impl UiRenderNode {
self.active.push(i);
for change in draws.apply_free() {
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 {
h.inst_idx = change.new;
break;
+8 -4
View File
@@ -35,6 +35,10 @@ struct MoveOffset {
// 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
// `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> {
return floor(v + PX_STEP * 0.5);
}
@@ -147,8 +151,8 @@ fn vs_main(
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 top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px);
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
let size = bot_right - top_left;
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_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px);
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px);
let top_left = snap_floor(tl * window.dim + tl_px);
let bot_right = snap_floor(br * window.dim + br_px);
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 {
return color * 0.0;
+76 -25
View File
@@ -1,6 +1,6 @@
use crate::{
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
UiRegion, WidgetId,
DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
};
/// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,20 +9,32 @@ use crate::{
#[derive(Debug)]
pub struct ActiveData {
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,
/// The box its parent first asked about it in, as a part of the box the
/// parent was itself asked in. Any later box it was given was decided
/// knowing its answer, so this is where a question about it is asked
/// again -- and it is kept relative so that it follows the parent's.
pub offer: UiRegion,
/// What it answered there: the size and what that held for.
pub answer: (Size, [Holds; 2]),
/// Where its drawing sits inside that box, in the box's own coordinates.
pub placement: UiRegion,
/// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion,
/// The frame it was first asked in, in the same coordinates: the offer's
/// 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.
pub size: Size,
/// The pixel lengths of `region`, per axis, that its drawing and `size`
/// hold for.
pub holds: [Holds; 2],
/// The frame, extent and explicit placement reads that this drawing holds for.
pub holds: LayoutHolds,
pub drawn: bool,
pub parent: Option<WidgetId>,
/// 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.
pub depth: usize,
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>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
@@ -41,24 +59,57 @@ pub struct ActiveData {
/// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2],
/// The alignment its parent asked it with. A local redraw repeats that
/// question, including an override chosen by a container.
pub align: RegionAlign,
/// Whether that alignment was the parent's override rather than the
/// widget's own property.
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.
/// The axes along which its parent chose its box from its own answer,
/// so a local redraw asks the question its parent asked.
pub decided: [bool; 2],
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses.
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,
/// 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,
}
impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths.
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
/// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its
/// 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,
}
+74 -22
View File
@@ -10,9 +10,9 @@ use std::ops::RangeInclusive;
///
/// 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
/// as the same number, so a range means what it says. What widening there is
/// belongs to [`Self::through`], which has a rounding to undo, and is derived
/// from that rounding rather than chosen.
/// as the same number, so a range means what it says. The one place a range
/// is wider than the length it came from is [`Self::through`], and what it is
/// wider by is the floor that inverting a fraction undoes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holds {
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
/// in this range. A part with no relative extent is a fixed length: it
/// was drawn at that length and any box keeps it there.
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
/// 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
/// of exactly `lo` came from anything within half a step of it and the
/// answer is an interval even where this range is one length. Inverting
/// the length alone instead gives a point that need not even contain the
/// box the part was drawn in, which is a range excluding the drawing it
/// was made for.
/// The answer is an interval even where this range is a single length,
/// because the multiply on the way in drops to the step below and many
/// boxes therefore give one length. That is a floor rather than an
/// allowance: inverting it is two divisions and nothing else, and the
/// whole of a box maps back to itself.
pub const fn through(self, len: Len) -> Self {
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
return Self::ANY;
}
let rel = len.rel.raw() as i64;
if rel == 0 {
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 half_rel = REL_SHIFT - 1;
let lo = ((self.lo.raw() as i64 - px) * 2 - 3) << half_rel;
let hi = ((self.hi.raw() as i64 - px) * 2 + 3) << half_rel;
// Dividing by a negative turns the ends around, so which end each
// bound comes from is decided before dividing rather than by taking
// the min and max of four divisions.
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
// Dividing by a negative fraction turns the ends around, so which
// bound each comes from is decided before dividing rather than by
// taking the min and max of four divisions.
match rel > 0 {
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)),
@@ -95,6 +95,16 @@ mod tests {
use super::*;
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]
fn through_reverses_a_range_for_a_negative_fraction() {
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
@@ -116,6 +126,48 @@ mod tests {
}
}
/// 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]
fn a_boundary_the_next_step_along_does_not_admit_it() {
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;
mod active;
mod draw_region;
mod holds;
mod layout_holds;
mod painter;
mod render_state;
pub use active::*;
pub use draw_region::*;
pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike};
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.
/// The same walk the vertex shader does, in the same `Len` the shader is
/// 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 {
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;
for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE {
return region;
return;
}
let entry = self.arena[at.idx()];
region = region.within(&entry.region);
let entry = &self.arena[at.idx()];
step(&entry.region);
at = entry.parent;
}
debug_assert!(
@@ -86,7 +97,6 @@ impl Moves {
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth"
);
region
}
/// How many slots a region in `idx` is composed through, which is what
+388 -139
View File
@@ -1,12 +1,12 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
WidgetId, Widgets,
Axis, DrawRegion, ExtentPlacement, Holds, LayoutHolds, LayoutLen, Len, Px, PxVec2, RegionAlign,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive,
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
ui::render_state::DrawInfo,
};
@@ -17,27 +17,51 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
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,
/// 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) 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>,
/// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer.
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,
/// Whether this draw is in that box, which makes the questions it asks
/// the ones a cold layout asks and their answers the ones to keep.
/// Whether this draw is in a box of those lengths, which makes the
/// questions it asks the ones a cold layout asks and their answers the
/// ones to keep.
pub(super) at_offer: bool,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of its box in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2],
/// What the children it asked about and drew keep it to.
pub(super) under: [Holds; 2],
/// Dependencies of every child drawing, including unmeasured overlays.
pub(super) under: LayoutHolds,
/// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx,
@@ -50,13 +74,28 @@ pub struct 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>();
self.write(kind, primitive, region);
}
/// 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")]
diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write(
@@ -65,15 +104,15 @@ impl<'a> Painter<'a> {
kind,
id: self.id,
primitive,
region,
region: resolved,
mask_idx: self.mask,
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 {
// TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -81,28 +120,48 @@ impl<'a> Painter<'a> {
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) {
let at = DrawRegion::Extent(UiRegion::FULL);
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);
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);
self.mask = self.rsc.ui_mut().masks.push(Mask {
region,
region: region.resolve(self.region, self.placement),
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> {
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
@@ -118,50 +177,77 @@ impl<'a> Painter<'a> {
/// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.extent_children.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// Draws a widget somewhere within this one. `region` is in this widget's
/// own coordinates, and the child's declared lengths are still to be
/// taken from it. Where the child's drawing sits inside what it is given
/// 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
/// length or by leaving it room and letting its alignment decide.
/// Draws a child in `region`, relative to this widget's frame. The child
/// resolves declared lengths and reports against that frame, then places
/// its drawing by its own alignment.
///
/// `DrawRegion::Extent` gives a part of where this widget's drawing sits
/// 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>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
region: impl Into<DrawRegion>,
) -> 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
/// the widget's property. Containers use this when the box they hand down
/// already expresses the size they report around the child.
pub fn widget_aligned<'s, W: ?Sized>(
/// Draws a widget in `region`, saying where in it the drawing goes.
///
/// `region` is the child's own area: what a fraction it declares or
/// 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,
id: &'s StrongWidget<W>,
region: UiRegion,
align: RegionAlign,
placement: [Option<UiSpan>; 2],
) -> 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,
id: &'s StrongWidget<W>,
region: UiRegion,
align_override: Option<RegionAlign>,
placement: [Option<UiSpan>; 2],
extent: Option<ExtentPlacement>,
measuring: bool,
) -> 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 declared = self.declared_lens(id);
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id()));
// Composing `FULL` through a box is not quite the identity in f32,
// 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 align = self.rsc.widgets().alignment(id.id());
let (local, placement) = ask_box(region, declared, align, placement);
let within = match local == UiRegion::FULL {
true => self.region,
false => local.within(&self.region),
@@ -176,15 +262,37 @@ impl<'a> Painter<'a> {
self.children.push(id.id());
}
let first_ask = self.offer(id.id());
let offer = match first_ask {
true => local,
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
let given_len = local.size();
let offer_len = match first_ask {
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
// child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box.
let (size, holds) = self.state.draw_inner(
let (size, answer_holds, holds) = self.state.draw_inner(
id.id(),
within,
DrawInfo {
@@ -194,25 +302,74 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx,
region_node,
mask: self.mask,
offer,
offered_px: self.px_within_offer(offer),
align: align_override,
given_region: local,
offer_region,
offer_len,
offer_placement,
px,
offered_px,
placement,
},
None,
measuring,
self.rsc,
);
if answers_offer {
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds);
let in_parent = |holds: LayoutHolds| {
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.
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
// Its box is a part of this widget's extent, so what it
// holds for is a range on that extent and none of it a
// 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 {
child: id,
painter: self,
size,
size: in_parent_frame(size, local.size(), declared),
answer_holds,
}
}
@@ -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
/// had without drawing it: from its hint, or from a drawing it already
/// has that holds for that box.
pub fn known_len<W: ?Sized>(
/// Measures a child's length from its hint, a retained answer, or `draw`.
/// A fresh draw evaluates the offer without placing its answer. The caller
/// must later place or undraw the child.
pub fn measure_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
) -> Option<LayoutLen> {
placement: [Option<UiSpan>; 2],
) -> LayoutLen {
let offered = placement;
let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align);
let within = local.within(&self.region);
let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer = local;
}
let (local, placement) = ask_box(region, declared, align, placement);
let first_ask = self.at_offer && !self.offered.contains(&child.id());
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 (size, holds) =
let px = local.size().to_px(self.px);
let retained =
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")]
diag::bump(Counter::RetainedSizeHits);
self.depend_on(child);
if first_ask {
self.offered.push(child.id());
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()));
}
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
@@ -292,15 +461,6 @@ impl<'a> Painter<'a> {
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>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
@@ -319,11 +479,16 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width)
}
/// Writes glyphs in the selected frame or extent coordinates.
// 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>();
for glyph in text.glyphs.iter() {
let mut region = origin;
let place = |mut region: UiRegion| {
region.x.end = region.x.start;
region.y.end = region.y.start;
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.y.end = region.y.start.offset(size.y);
self.write(
region
};
self.write_resolved(
kind,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
@@ -342,17 +509,32 @@ impl<'a> Painter<'a> {
color: text.color,
flags: glyph.entry.flags(),
},
region,
origin.map(place),
place(resolved),
);
}
}
/// This widget's box, in the coordinates its own primitives are written
/// in -- so a region composed `within` it may be drawn directly.
/// The box this widget's parent gave it, in the coordinates its own
/// 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 {
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
/// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget.
@@ -382,25 +564,49 @@ impl<'a> Painter<'a> {
/// 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.
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
/// holds for this box only, until `holds` says how far it goes.
/// This widget's own box in pixels. Reading it makes the drawing one
/// that holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 {
let px = self.state.px_of(self.move_idx, self.region);
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
}
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.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 {
*own = Holds::at(len);
}
}
px
len
}
/// One axis of this widget's 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 len = self.state.px_of(self.move_idx, self.region).axis(axis);
/// The lengths of this widget's own box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its length
/// 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];
if *own == Holds::ANY {
*own = Holds::at(len);
@@ -408,15 +614,14 @@ impl<'a> Painter<'a> {
len
}
/// The lengths of this widget's box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its
/// length in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
/// [`Self::holds`] stated about the region rather than about this
/// widget's own box, for a container whose drawing turns on the box it
/// was given rather than on the part of it it took.
pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its region",
self.label(),
self.id
);
@@ -463,6 +668,7 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>,
size: Size,
answer_holds: LayoutHolds,
}
impl<W: ?Sized> DrawResult<'_, '_, W> {
@@ -473,6 +679,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on(self.child);
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
self.size
}
@@ -506,6 +713,21 @@ 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
/// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead.
@@ -526,52 +748,79 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
})
}
/// The box a drawing occupies: the size the widget reported, on the side of
/// the box it was asked in that its alignment says. An axis reported as a
/// share fills, because a share is a length only to whoever divides one, and
/// whoever did is the one that handed down this box. A declared axis is
/// left alone too: `declared_box` already placed it, in the parent's box,
/// and the rule's length is what the widget reports there.
/// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. A share fills, because a
/// share is a length only to whoever divides one, and whoever did is the one
/// that handed down this box. A declared axis does too: the rule already gave
/// the region its length, and the rule's length is what the widget reports
/// 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
/// 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
/// fraction of. So this scales by the box rather than composing into it.
pub(crate) fn placed_box(
region: UiRegion,
/// fraction of. So this is a length of the box rather than a length composed
/// into it, and a box in pixels is this step from the given box's pixels.
pub(crate) fn placed_lens(
size: Size,
align: RegionAlign,
declared: [Option<LayoutLen>; 2],
) -> UiRegion {
let mut placed = region;
for (axis, declared) in AXES.into_iter().zip(declared) {
decided: [bool; 2],
) -> UiVec2 {
let mut lens = UiVec2::FULL_SIZE;
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
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;
}
let span = placed.axis_mut(axis);
let len = span.len().scale(reported.rel) + Len::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.end = span.start + len;
}
placed
}
/// Takes a widget's declared lengths in the box `region` is given in, since a
/// fraction of a length means a fraction of that one, and puts what is left
/// over on the side its alignment says. A caller that already reserved the
/// space hands back the same length, so this is the identity for it.
pub(crate) fn declared_box(
/// A declared axis gets a frame of that length, aligned within the parent's
/// slot (or the offer). Undeclared axes keep the offered frame and chosen
/// placement, so their reported fractions retain that reference.
pub(crate) fn ask_box(
mut region: UiRegion,
declared: [Option<LayoutLen>; 2],
align: RegionAlign,
) -> UiRegion {
for (axis, len) in AXES.into_iter().zip(declared) {
let Some(len) = len else { continue };
placement: [Option<UiSpan>; 2],
) -> (UiRegion, [Option<UiSpan>; 2]) {
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 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;
}
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 {
idx: u32,
genr: u32,
+5 -1
View File
@@ -20,10 +20,14 @@ impl DefaultAppState for Client {
let pad_test = (
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
.color(Color::RED)
.sized((100, 100))
.center()
.wrapper()
.width(leftover(2)),
(
rrect.color(Color::ORANGE),
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN)
.add(rsc);
let main = WidgetPtr::new().add(rsc);
let main = Wrapper::new().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| {
+7 -3
View File
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
.pad(16)
.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 = (
wtext("left").size(24).text_align(Align::LEFT),
wtext("centred").size(24).text_align(Align::CENTER),
wtext("right").size(24).text_align(Align::RIGHT),
label("left", Align::LEFT),
label("centred", Align::H_CENTER),
label("right", Align::RIGHT),
)
.span(Dir::DOWN)
.gap(8)
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw();
}
WindowEvent::Resized(size) => {
render.resize((size.width, size.height));
render.resize((size.width, size.height), rsc.widgets_mut());
ui_state.renderer.resize(size)
}
WindowEvent::KeyboardInput { event, .. } => {
+3 -3
View File
@@ -144,9 +144,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test
// leaves unread.
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();
render.resize(size);
render.resize(size, rsc.widgets_mut());
Self {
rsc,
render,
@@ -161,7 +161,7 @@ impl Harness {
}
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.
+1 -1
View File
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region());
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.widget(&self.inner);
// 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
+2 -2
View File
@@ -1,15 +1,15 @@
mod image;
mod mask;
mod position;
mod ptr;
mod rect;
mod text;
mod trait_fns;
mod wrapper;
pub use image::*;
pub use mask::*;
pub use position::*;
pub use ptr::*;
pub use rect::*;
pub use text::*;
pub use trait_fns::*;
pub use wrapper::*;
+14 -5
View File
@@ -7,9 +7,14 @@ pub struct Pad {
impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size {
let inner = painter
.widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR)
.size();
// The inner's own alignment, not the near edge. This reports the
// inner's size plus the padding, so where the box is that answer the
// 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 {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
@@ -47,14 +52,18 @@ impl Padding {
bottom: amt,
}
}
pub fn region(&self) -> UiRegion {
let mut region = UiRegion::FULL;
/// `region` less this padding on each side.
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left;
region.y.start.px += self.top;
region.x.end.px -= self.right;
region.y.end.px -= self.bottom;
region
}
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt);
Self {
+9 -5
View File
@@ -14,10 +14,10 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis);
// Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset.
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) {
Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis),
};
let whole = UiRegion::FULL;
let own = painter.placement();
let answer_len =
painter.measure_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]);
let content = answer_len.apply_leftover();
self.container_len = container_len;
self.content_len = content.to_px(container_len);
@@ -63,7 +63,11 @@ impl Widget for Scroll {
region = region.offset(offset);
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
// 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
+46 -51
View File
@@ -10,20 +10,34 @@ pub struct Span {
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size {
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 hint where one exists, and from drawing otherwise.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let mut span = UiSpan::new(cursor, Len::rel_max());
if self.dir.sign == Sign::Neg {
span.flip();
}
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
let len = match painter.known_len(child, axis, region) {
Some(len) => len,
None => painter.widget_within(child, region).len(axis),
};
// The whole region is the child's, so `rel(0.5)` is half the area
// this span was given whatever else is in it and wherever this
// child sits among them. What it is placed in is the room left
// from the cursor, because a text has to wrap at the width
// actually there.
let room = axis.pair(Some(along(cursor, far)), None);
let len = painter.measure_len(child, axis, region, room);
cursor.px += len.px + self.gap;
cursor.rel += len.rel;
lens.push(len);
@@ -40,43 +54,26 @@ impl Widget for Span {
|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)`
// beside 300 px is full at 600 and overfull at 400. The room to
// divide is `len * fixed - total.px`, and the length where it runs
// out is exactly the box a parent sizing itself from this answer
// hands back -- which is why this used to need a margin either side
// of the boundary, and why it does not now: that box and this sum are
// whole counts of the same step, and both routes to it land on the
// same count. What the generated oracle checks is the consequence,
// since which children exist at all turns on this.
let fixed = Rel::ONE - total.rel;
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the
// boundary is the drawing's own and not a second way of finding it:
// the three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which `through` already reads. What the
// generated oracle checks is the consequence, since which children
// exist at all turns on this.
let mut shares = false;
if total.leftover > Weight::ZERO {
let current = painter.px_len(axis);
let holds = if fixed > Rel::ZERO {
// The box length the fixed parts alone fill.
let full = total.px.div(fixed);
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
shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.holds(axis, holds);
painter.region_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a
@@ -93,7 +90,6 @@ impl Widget for Span {
// row.
let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO;
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) {
@@ -106,20 +102,19 @@ impl Widget for Span {
fixed.px += self.gap;
continue;
}
let mut span = UiSpan::FULL;
span.start = start;
let from = start;
if len.leftover > Weight::ZERO && shares {
taken += len.leftover;
}
fixed.px += len.px;
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
span.end = start;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
region.flip(axis);
}
let placed = painter.widget_within(child, region);
// Along the row the span says where the child goes; across it the
// child sits where its own alignment says. Its region is the
// whole of what this span was given either way, which is what its
// fractions are of.
let placed =
painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
+11 -5
View File
@@ -13,9 +13,10 @@ impl Widget for Stack {
StackSize::Default => None,
StackSize::Child(i) => Some(i),
};
// Whichever child sizes the stack decides the box every child gets.
// The stack reports that size, so a child given a longer box would
// draw outside what the stack says it occupies.
// Whichever child sizes the stack keeps the stack's whole region as
// its own -- the stack is the length that child asked for, so taking
// 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))) {
// 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
@@ -26,10 +27,15 @@ impl Widget for Stack {
}
None => Size::LEFTOVER,
};
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
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
}
+11 -11
View File
@@ -50,15 +50,11 @@ impl TextView {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
// The shaper measures in floats, which is where a glyph advance comes
// from; what it answers goes back on the grid.
let text = 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
// line up to the one it was made at: each line still fits, and none
// 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);
painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
if width.is_some() {
painter.holds(Axis::X, self.buf.width_holds());
}
text
self.buf.rendered().expect("render_text placed the glyphs")
}
pub fn tex(&self) -> Option<&RenderedText> {
@@ -78,9 +74,13 @@ impl TextView {
let tex = self.render(painter);
let region = tex.size.align(align);
let size = Size::px(tex.size);
let within = region.within(&painter.region());
painter.glyphs(tex, within);
// The step at or above what the shaper measured, so a parent that
// hands back the length this reports hands back a box the longest
// 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)
}
+7 -3
View File
@@ -134,9 +134,13 @@ widget_trait! {
|state| self.add(state)
}
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
let id = self.add_strong(state);
state.ui_mut().widgets[ptr].inner = Some(id);
// Named for the type it makes rather than as `wrapped`, which would read
// as the text setting. `widget_trait!` takes no attributes, so what it is
// 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 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>,
}
impl Widget for WidgetPtr {
impl Widget for Wrapper {
fn draw(&mut self, painter: &mut Painter) -> Size {
match &self.inner {
Some(id) => painter.widget(id).size(),
@@ -14,7 +22,7 @@ impl Widget for WidgetPtr {
}
}
impl WidgetPtr {
impl Wrapper {
pub fn new() -> Self {
Self::default()
}
@@ -35,7 +43,7 @@ impl WidgetPtr {
}
}
impl Default for WidgetPtr {
impl Default for Wrapper {
fn default() -> Self {
Self::empty()
}
+268 -11
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));
}
/// 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]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200));
@@ -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));
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.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(
h.render.moves.depth(move_idx),
2,
"the opted-in widget's region and the root region"
1,
"the opted-in widget's region alone"
);
h.rsc.widgets_mut().set_region_node(buried, false);
h.frame();
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
@@ -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
/// 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]
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
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);
h.set_root((one, three).span(Dir::RIGHT));
let mut start = Px::ZERO;
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
let got = h.region(&id).expect("widget drew nothing");
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
@@ -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
/// apart, so a fraction and a pixel offset snap independently, and each is
/// taken to the boundary it composes to within half a step of. Kept in step
/// with `snap_floor` in `prelude.wgsl`.
/// Where the shader puts an edge: the fraction resolved against the window
/// plus the pixel offset, taken to the boundary it composes to within half
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region);
let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32());
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis);
(edge(span.start), edge(span.end))
}
@@ -480,3 +578,162 @@ fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
assert!(h.region(&a).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));
}
+732 -10
View File
@@ -156,16 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
// Reading its box makes its drawing hold for the measuring box alone,
// and it reports less than that box: so it is drawn again in the box its
// answer places it in, and once more in the final box the span chooses.
// 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"
);
// Only the available length changes: positioning the final slot does
// not invalidate a numeric size read.
assert_eq!(asked_draws.get(), 2);
}
#[test]
@@ -613,3 +606,732 @@ fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
assert_ne!(layer(front.id()), layer(background.id()));
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));
}
}
}
}
+228 -10
View File
@@ -3,13 +3,16 @@
//! frame that had not settled: a wrapping text shaped at a width it was
//! measured in rather than the one it was given. The rest are a widget
//! measured again in a box its own answer had decided, where the old answer
//! is a fixed point whatever the content now says. The last two are neither:
//! one box length, composed two ways, landing either side of the boundary
//! that decided whether a child was drawn at all, and one box as long as the
//! box a widget was offered but somewhere else.
//! is a fixed point whatever the content now says. The last three are
//! neither: one box length, composed two ways, landing either side of the
//! 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::prelude::*;
use iris::random::Branch;
/// 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
@@ -428,12 +431,12 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
}
/// A local redraw asks a dirty widget in the box its parent asked it in, and
/// then again in the box its parent chose from that answer. Skipping the
/// second ask because the two boxes are the same *length* left this inner
/// scroll, which owns a region node, drawn at its offer. The offer is the
/// outer scroll's whole viewport and the final box is 24px above it -- the
/// height of the sized child the outer scroll snaps to the end of -- so the
/// 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() {
@@ -454,3 +457,218 @@ fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
}
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));
}
+5 -3
View File
@@ -25,9 +25,11 @@ fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4)
}
/// The seeds the ordinary tests take. Eight that have never failed and one,
/// 86, that a `Scroll` fixed point once settled differently on.
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
/// The seeds the ordinary tests take. Seven that have never failed; 86,
/// which a `Scroll` fixed point once settled differently on; and 20, which
/// caught a locally redrawn widget being placed twice in the box its parent
/// had already placed it in.
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
fn check(seed: u64, depth: usize, case: Case) {
let grown = plan(seed, depth, &Edits::default());
+7 -4
View File
@@ -5,11 +5,11 @@
//! cargo test --release --features layout-diagnostics \
//! --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 \
//! -e cycles:u,instructions:u cargo test --release \
//! --test layout_diagnostics -- --ignored --nocapture
//! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
//! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
//!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
@@ -134,6 +134,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
{
let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames));
for event in diagnostics.traces() {
println!(" {event:?}");
}
for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new();
let mut id = Some(callsite.id);
+1 -22
View File
@@ -42,13 +42,6 @@ const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0);
const STILL: (f32, f32) = (900.0, 1200.0);
/// The same box, to a step of the grid per level of nesting between the two
/// ways of reaching it. A move, a repaint and a row of shares land on the
/// same number; what is left is a box centred in a fraction of its parent
/// against the same box centred in its own pixels. A step is a thousandth of
/// a pixel, where this was a twentieth of one before any of it was on a grid.
const AGREE_STEPS: i32 = 2;
/// 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.
@@ -391,20 +384,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label
}
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
match (got, want) {
(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,
}
}
/// 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.
@@ -431,7 +410,7 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
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 same_region(got, want) {
if got == want {
continue;
}
// Where two trees disagree is rarely where the cause is, so the