Compare commits

...
Author SHA1 Message Date
iris-aiandClaude Fable 5.1 aaba7dbfee Keep the step 3/4 experiment as evidence
The worker's uncommitted attempt at evaluating children in parent-decided
boxes, preserved as it stood when it stopped: separate answer-only pixel
reads, per-child drawing contracts, provisional Fill asks in Span, and an
assertion that a placed drawing hold for its answer box. The suite passes
and every generated case stops on that assertion at Text. Not the
protocol; wip/one-ask is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:21:24 -04:00
iris-ai 4328eac756 Keep leftover shares inside scroll viewports 2026-09-18 14:11:02 -04:00
iris-ai b842e4f474 Pin decided-box warm/cold failures 2026-09-18 14:09:00 -04:00
iris-ai 49cec82c1b Say which box a pin reaches the parent through, and derive the offer
The review pass over the two commits before it:

- `LayoutHolds`'s comment said a pin does not compose into the parent. It
  does, where the box it pinned is the parent's own box, which is the one
  case where the parent's own length is what was pinned.
- `DrawInfo::offer` was stored beside the two fields it is computed from.
  It is a method now, with the open question written where it is asked
  rather than only in the handoff.
- `Painter::own` is `frame_own` beside `extent_own`, since a widget's own
  box is the extent and the frame is what it is a part of.
- One expression for the length a rule gives a frame (`narrowed_by`) and
  one for what a widget answered (`ActiveData::measured`), each of which
  had two.
- The counter said "the placement it was pinned to" for what is now a
  length; the deferral in `redraw` named the seeds that made it necessary
  before the last commit rather than the ones that do now; three comments
  claimed an inset that does not exist yet.

No behaviour change: 108 suite tests, 20 core, the 11 generated cases, the
same two shrinker seeds failing at 400/5, and `view` and `tabs` byte-identical
to the renders taken before it.
2026-09-18 01:21:03 -04:00
iris-ai 0954770ceb Say what a child gets of a container's box in that box's own lengths
Three things the measurements asked for, all about how much a box that came
from an answer costs.

**A part in the box's own coordinates.** Saying "less eleven pixels at the
end" in frame lengths from the box's start means reading how long the box is,
and a container whose box is its own answer then depends on its own answer:
`Pad` drew sixty-four times in one resize frame at seed 13, chasing its own
width. `Part::Of` says the same thing as a part of the box, which composes
without a length -- pixels are pixels wherever the box lands -- and what a
child under it holds for maps back through that part onto the container's
own box rather than onto the frame.

**One axis of the box at a time.** `extent_len` pinned both axes, so a span
dividing one of them held for one length of the other as well, and a resize
broke every span whose cross-axis answer moved.

**No lazy placement.** Leaving a child's answer to be placed at the end of
the parent's draw, rather than as the child answers, was meant to save a
recomposition. It costs one instead: the drawing is put in the part first
and in the answer's box after, and where it does not hold for both that is
two drawings rather than one. Seed 1 at depth 8 went from 391 widget draws
on a resize to 29 with it gone. The test that pinned three draws for a
numeric leaf in a span goes with it.

Seed 1 at depth 8, widget draws / distinct widgets / update, against #18's
head and against the commit this branch started from:

| phase   | e44dea3      | 34cafb6      | here          |
| ---     | ---          | ---          | ---           |
| cold    | 369/261/10.6 | 463/274/13.3 | 516/288/12.0  |
| repaint | 1            | 1            | 1             |
| many    | 157/95/0.33  | 263/108/0.59 | 187/119/0.52  |
| size    | 16/12/0.018  | 3/3          | 3/3/0.010     |
| scroll  | 2/0.002      | 1            | 1/0.004       |
| resize  | 13/13/0.019  | 22/15/0.032  | 24/76/0.090   |

Seed 13 at depth 8 is where the protocol still costs: `many` 1091 draws
against #18's 524, and `resize` 2215 against a frame #18 does not draw at
all. Both are the same shape -- an answer measured in one box and drawn in
another -- and the handoff says where that comes from.

Checked: fmt, clippy with -D warnings, 108 suite tests, 20 core tests, the
11 generated cases, and the shrinker at 400 trees of depth 5, which fails
seeds 2 (repaint) and 108 (reorder).
2026-09-18 01:06:05 -04:00
iris-ai 1956be3f3d Lay out in a frame that passes through and a box placed in it
A widget is asked in two boxes rather than one. Its frame is what a fraction
it declares or reports is a fraction of, and it passes through a span, a
stack and a scroll unchanged, so `rel(0.5)` is half the same area however
many containers sit between: a frame is narrowed only by what is decided
above the widget -- a declared length, the root. Its extent is where the
drawing goes, given as a `Place` per axis: a part of the parent's own box,
measured in frame lengths from where that box starts, which the child either
fills or has its answer placed inside.

What that buys is that nothing under a container depends on where the
container sits. A container reads `extent_len` for the length it divides and
nothing about the start, so moving it re-places its children by re-adding
that start and draws nobody again; and a fraction is resolved once, against
the frame, rather than once per box it is composed through -- a stack sized
by a child that reports `rel(0.5)` no longer takes half of half.

`Place` replaces `DrawRegion`, `ExtentPlacement`, `widget_within`,
`measure_len`, `region()`, `placement()` and `box_of`. Primitives and masks
are written in the widget's own box's coordinates alone, so the drawing has
one reference rather than two. The placement pin goes with them: reading the
extent's length pins that length symbolically, and pins compose only where a
child's box is its parent's own.

Placing an answer waits for the end of the parent's draw or for the next ask
of that child in it, so a span child is one drawing and one move rather than
two moves.

`Pad` is transparent: its padding goes around what it pads and its child
keeps the outer frame, which is where `Outset` was going anyway. A fraction
under a pad is now a fraction of the frame rather than of the inset box.

Checked: fmt, clippy with -D warnings, 109 suite tests and 20 core tests in
debug, the 11 generated cases, and the shrinker at 400 trees of depth 5 over
all fifteen cases -- which still finds seed 108 under `reorder`, where a
wrapping text measured in one box and drawn in another settles differently
warm than cold. `redraw` therefore keeps the baseline's deferral for a box
that is not as long as the one the widget was measured in; the plan's step
6 is not done, and the next commit message or the handoff says why.
2026-09-18 00:40:59 -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
39 changed files with 2946 additions and 1037 deletions

No files matched your search

+37
View File
@@ -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 /// From a number as it is written in source -- `16`, `1.5` -- which is
/// the other place a value enters the grid. /// the other place a value enters the grid.
pub fn from_num(v: impl UiNum) -> Self { pub fn from_num(v: impl UiNum) -> Self {
@@ -372,6 +384,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y)) Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
} }
/// The first step at or above each part, for a measurement reported as a
/// box: what it occupies is not less than what was measured.
pub fn ceil_from_f32(v: Vec2) -> Self {
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
}
pub fn to_f32(self) -> Vec2 { pub fn to_f32(self) -> Vec2 {
Vec2::new(self.x.to_f32(), self.y.to_f32()) Vec2::new(self.x.to_f32(), self.y.to_f32())
} }
@@ -488,6 +506,25 @@ mod tests {
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200)); assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
} }
/// The bound a greedy line break needs: the width it was measured at is
/// not on the grid, and the narrowest box the break still holds for is
/// the step at or above it, never the one below.
#[test]
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] #[test]
fn a_number_from_outside_is_clamped_to_the_grid() { fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::from_f32(1e12), Px::MAX); assert_eq!(Px::from_f32(1e12), Px::MAX);
+7 -1
View File
@@ -54,10 +54,13 @@ pub(crate) enum Counter {
TextShapes, TextShapes,
TextBreaks, TextBreaks,
GlyphPlacements, GlyphPlacements,
OutsidePinnedLen,
OutsideFrame,
OutsideExtent,
} }
impl Counter { impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1; const COUNT: usize = Self::OutsideExtent as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"updates", "updates",
@@ -89,6 +92,9 @@ impl Counter {
"text shapes", "text shapes",
"text line breaks", "text line breaks",
"glyph placements", "glyph placements",
"reuse outside: the length 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 { impl RegionAlign {
/// Both axes at the near edge. What a container passes as an override for /// Both axes at the near edge: the start of a box in its own orientation.
/// a child it is going to position itself.
pub const NEAR: Self = Self { pub const NEAR: Self = Self {
x: AxisAlign::NEG, x: AxisAlign::NEG,
y: AxisAlign::NEG, y: AxisAlign::NEG,
+11
View File
@@ -7,6 +7,17 @@ pub enum Axis {
Y, Y,
} }
impl Axis {
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
match self {
Self::X => [aligned, ortho],
Self::Y => [ortho, aligned],
}
}
}
impl std::ops::Not for Axis { impl std::ops::Not for Axis {
type Output = Self; type Output = Self;
+19
View File
@@ -125,6 +125,13 @@ impl Size {
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl LayoutLen { impl LayoutLen {
@@ -151,6 +158,18 @@ impl LayoutLen {
Len::from_parts(self.rel.add(share), self.px) Len::from_parts(self.rel.add(share), self.px)
} }
/// This length, given as a part of a box `len` long, as a part of the
/// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len);
Self {
px: part.px,
rel: part.rel,
leftover: self.leftover,
}
}
pub fn px(px: impl UiNum) -> Self { pub fn px(px: impl UiNum) -> Self {
Self { Self {
px: Px::from_num(px), px: Px::from_num(px),
+1 -1
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 { self.within(&UiSpan {
start: Len::ZERO, start: Len::ZERO,
end: len, end: len,
+26 -12
View File
@@ -107,13 +107,6 @@ impl Default for TextAttrs {
} }
} }
/// How far below the longest line a width may fall and still be answered by
/// the break in hand. A parent that offers a child the length it reported
/// composes that length back through the box chain, so the two differ in the
/// last bits -- and at exactly the longest line, that decides whether a line
/// fits. Sub-pixel, so no break it admits is one a reader could see.
const BREAK_EPSILON_PX: f32 = 0.05;
/// Keeps text and its corresponding layout from getting out of sync. /// Keeps text and its corresponding layout from getting out of sync.
pub struct TextBuffer { pub struct TextBuffer {
text: String, text: String,
@@ -183,6 +176,23 @@ impl TextBuffer {
self.layout_key.as_ref()?.max_width self.layout_key.as_ref()?.max_width
} }
/// Widths covered by the current line breaks, including a wider shaping
/// retained when a later draw requested a narrower box.
pub fn width_holds(&self) -> crate::Holds {
let Some(width) = self.wrap_width() else {
return crate::Holds::ANY;
};
let width = Px::from_f32(width);
let soft_wrapped = self.layout.lines().any(|line| {
matches!(
line.break_reason(),
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
)
});
let upper = if soft_wrapped { width } else { Px::MAX };
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
}
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height()) Vec2::new(self.layout.width(), self.layout.height())
} }
@@ -200,15 +210,19 @@ impl TextBuffer {
// A greedy break at one width is the same break at every width down // A greedy break at one width is the same break at every width down
// to the longest line it produced: each line still fits, and none can // to the longest line it produced: each line still fits, and none can
// take a word that would not fit in the wider box. So the layout in // take a word that would not fit in the wider box. So the layout in
// hand already answers, and re-breaking would only be a chance to // hand already answers, and re-breaking would only be work.
// disagree with itself -- which is what happens when a parent offers //
// a child the length that child just reported, and the two land // At the longest line exactly, with no margin below it. A narrower
// either side of a float. // width really does break differently, so answering one from the
// break in hand is how a warm tree keeps lines a cold tree would
// never produce. The margin was here because a text reports the
// width it used and a parent hands that back; the report is the step
// at or above its longest line now, so what comes back fits.
if let Some(key) = &self.layout_key if let Some(key) = &self.layout_key
&& key.attrs == *attrs && key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width) && let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at && want <= broke_at
&& want + BREAK_EPSILON_PX >= self.layout.width() && want >= self.layout.width()
{ {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits); diag::bump(Counter::TextShapeHits);
+2 -1
View File
@@ -106,7 +106,8 @@ impl UiRenderNode {
self.active.push(i); self.active.push(i);
for change in draws.apply_free() { for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives { for primitive in &mut inst.primitives {
let h = &mut primitive.handle;
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
+8 -4
View File
@@ -35,6 +35,10 @@ struct MoveOffset {
// belongs to the pixel above it. Flooring the product instead drops a pixel // belongs to the pixel above it. Flooring the product instead drops a pixel
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of // wherever a fraction divides a window exactly: a fifth of 1920 comes out of
// `REL_STEP` as 383.99998, and five tabs each lose their last column. // `REL_STEP` as 383.99998, and five tabs each lose their last column.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
fn snap_floor(v: vec2<f32>) -> vec2<f32> { fn snap_floor(v: vec2<f32>) -> vec2<f32> {
return floor(v + PX_STEP * 0.5); return floor(v + PX_STEP * 0.5);
} }
@@ -147,8 +151,8 @@ fn vs_main(
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel); let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_px = vec2(r.x.end.px, r.y.end.px); let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px); let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px); let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
let size = bot_right - top_left; let size = bot_right - top_left;
let uv = vec2<f32>( let uv = vec2<f32>(
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
let br = vec2(m.x.end.rel, m.y.end.rel); let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px); let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px); let top_left = snap_floor(tl * window.dim + tl_px);
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px); let bot_right = snap_floor(br * window.dim + br_px);
let pos = in.clip_position.xy; let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0; return color * 0.0;
+61 -29
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, Size,
UiRegion, WidgetId, TextureHandle, UiRegion, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,20 +9,36 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// The box its drawing is in, in `parent_move`'s coordinates. /// Its frame in `parent_move`'s coordinates: what a fraction it declares
pub region: UiRegion, /// or reports is a fraction of, composed. Everything it draws sits inside
/// The box its parent first asked about it in, as a part of the box the /// this by way of `extent`.
/// parent was itself asked in. Any later box it was given was decided pub frame_abs: UiRegion,
/// knowing its answer, so this is where a question about it is asked /// Where its drawing goes, in the frame's own coordinates.
/// again -- and it is kept relative so that it follows the parent's. pub extent: UiRegion,
pub offer: UiRegion, /// That frame in its parent's frame coordinates, before composition:
/// What it answered there: the size and what that held for. /// forwarded whole by a transparent container, narrowed by a declared
pub answer: (Size, [Holds; 2]), /// length. Its length is the same on every ask, which is what
/// What the widget said it used of its box, the last time it drew. /// a local redraw relies on to ask its parent's own question again.
pub frame: UiRegion,
/// What of its parent's extent the drawing was given, and what it was
/// given at the parent's first ask of it -- the question a cold layout
/// asks. A part is a length from the extent's start, so an extent that
/// moved re-places every child by re-adding that start.
pub place: [Place; 2],
pub offer_place: [Place; 2],
/// The box that ask gave it, in its frame's coordinates. Kept rather
/// than worked out again from where its parent's own box is now: a
/// parent drawn again in the box its own answer chose gives its children
/// boxes it never measured anything in, and the measurement this widget
/// answered is the one its parent's layout was built on.
pub offer_part: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// What the widget said it used of its frame, the last time it drew.
pub size: Size, pub size: Size,
/// The pixel lengths of `region`, per axis, that its drawing and `size` /// The frame and extent reads that this drawing holds for.
/// hold for. pub holds: LayoutHolds,
pub holds: [Holds; 2],
pub drawn: bool, pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a /// How far down the tree it was drawn, the root being 1. Carried down a
@@ -30,35 +46,51 @@ pub struct ActiveData {
/// widget a frame visits and cannot drift while one is being drawn. /// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize, pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, /// Its primitives, each keeping the box it was written in -- in this
/// widget's extent coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>, pub size_deps: Vec<WidgetId>,
/// The movable region its primitives are positioned through: its own when /// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's. /// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
/// The declared lengths whoever drew this widget resolved into its box. /// The declared lengths whoever drew this widget resolved into its frame.
/// A change to one moves a box this widget cannot fix by drawing again, /// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so. /// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2], pub declared: [Option<LayoutLen>; 2],
/// The alignment its parent asked it with. A local redraw repeats that /// Its alignment when it was last drawn, which a change to the property
/// question, including an override chosen by a container. /// is found against.
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.
pub own_align: RegionAlign, pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses. /// The movable region whose coordinates `frame_abs` uses.
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
pub mask: MaskIdx, pub mask: MaskIdx,
/// That inherited one. The two differ exactly where the widget set a
/// mask of its own, which is the one it owns and the one a move rewrites
/// -- and the one a redraw of it must not be handed back, since setting
/// a mask asserts there is none.
pub parent_mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
impl ActiveData { impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths. /// What it answered when its parent measured it, where it has been
pub fn holds_at(&self, px: crate::PxVec2) -> bool { /// measured at all. Not `size`, which is what its last drawing reported:
self.holds[0].contains(px.x) && self.holds[1].contains(px.y) /// a drawing made in the box that answer chose is answering a different
/// question.
pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size)
}
/// Whether what it answered still stands for a frame of these pixel
/// lengths. The answer was given in the box its parent first asked
/// about, which is what it is checked against -- `holds` on the record
/// is about the box the answer then chose.
pub fn answers_at(&self, px: crate::PxVec2, part: UiRegion) -> bool {
self.answer
.is_some_and(|(_, holds)| holds.contains(px, part))
} }
} }
+56 -54
View File
@@ -1,4 +1,4 @@
use crate::{Len, Px, REL_SHIFT, Rel, fixed::div_toward, fixed::narrow}; use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for: /// The lengths of a box, in pixels, that one drawing of a widget holds for:
@@ -10,9 +10,9 @@ use std::ops::RangeInclusive;
/// ///
/// The ends are lengths on the grid rather than floats with a tolerance /// The ends are lengths on the grid rather than floats with a tolerance
/// around them: a box offered back at the length a widget reported comes back /// around them: a box offered back at the length a widget reported comes back
/// as the same number, so a range means what it says. What widening there is /// as the same number, so a range means what it says. The one place a range
/// belongs to [`Self::through`], which has a rounding to undo, and is derived /// is wider than the length it came from is [`Self::through`], and what it is
/// from that rounding rather than chosen. /// wider by is the floor that inverting a fraction undoes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holds { pub struct Holds {
pub lo: Px, pub lo: Px,
@@ -41,51 +41,32 @@ impl Holds {
} }
/// What a box has to be for a part of it, `len` of the box long, to stay /// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range. A part with no relative extent is a fixed length: it /// in this range: the exact preimage of `px + floor(rel * box)`, which is
/// was drawn at that length and any box keeps it there. /// the one way a box in pixels is reached. A part with no relative extent
/// is a fixed length -- it was drawn at that length and any box keeps it
/// there.
/// ///
/// The way in is `px + rel * box` taken to the nearest step, so a part /// The answer is an interval even where this range is a single length,
/// of exactly `lo` came from anything within half a step of it and the /// because the multiply on the way in drops to the step below and many
/// answer is an interval even where this range is one length. Inverting /// boxes therefore give one length. That is a floor rather than an
/// the length alone instead gives a point that need not even contain the /// allowance: inverting it is two divisions and nothing else, and the
/// box the part was drawn in, which is a range excluding the drawing it /// whole of a box maps back to itself.
/// was made for.
pub const fn through(self, len: Len) -> Self { pub const fn through(self, len: 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; let rel = len.rel.raw() as i64;
if rel == 0 { if rel == 0 {
return Self::ANY; return Self::ANY;
} }
// In half steps, and no more than the arithmetic needs: too wide a
// range admits reusing a drawing where it does not hold, and too
// narrow a one leaves out the box a drawing was made in, which the
// `Holds` assertion in `draw_at` catches. `ROUTES` is at that floor
// -- two half steps fires it -- and tightening both ends moved not
// one of the rig's work counters, so the slack is not buying reuse.
//
// `ROUTES` covers a box composed down the chain against the same box
// measured against the window: two routes to one number, each
// rounding where the other does not. `way_in` covers the multiply
// this inverts, which drops a step and only ever downward, so it
// belongs at the top of the range alone -- and the whole of a box has
// no multiply in it, however many pixels were added to it, since
// multiplying by one is exact and taking the pixels off again is too.
// Allowing for it there anyway compounded, a step a level down a
// chain of widgets each taking the whole of its parent.
//
// Shifted by half of what a `Rel` counts in, to divide by the
// fraction: exact until the division takes it back to the grid.
const ROUTES: i64 = 3;
let px = len.px.raw() as i64; let px = len.px.raw() as i64;
let half_rel = REL_SHIFT - 1; // `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
let way_in = match rel == Rel::ONE.raw() as i64 { // `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
true => 0, let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
false => 2, let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
}; // Dividing by a negative fraction turns the ends around, so which
let lo = ((self.lo.raw() as i64 - px) * 2 - ROUTES) << half_rel; // bound each comes from is decided before dividing rather than by
let hi = ((self.hi.raw() as i64 - px) * 2 + ROUTES + way_in) << half_rel; // taking the min and max of four divisions.
// 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.
match rel > 0 { match rel > 0 {
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)), true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)), false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
@@ -114,6 +95,16 @@ mod tests {
use super::*; use super::*;
use crate::Rel; use crate::Rel;
#[test]
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
for px in [-8, 0, 8] {
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
assert_eq!(Holds::ANY.through(len), Holds::ANY);
}
}
}
#[test] #[test]
fn through_reverses_a_range_for_a_negative_fraction() { fn through_reverses_a_range_for_a_negative_fraction() {
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20. // `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
@@ -136,26 +127,37 @@ mod tests {
} }
/// A widget handed the whole of its parent's box, with or without pixels /// A widget handed the whole of its parent's box, with or without pixels
/// taken off it, brings no multiply of its own: only the two routes to /// taken off it, has no fraction to invert: multiplying by one is exact
/// the same length are left to allow for, and not a rounding that did /// and taking the pixels off again is too, so the box maps back to
/// not happen. Widening for it as well grew the interval a level at a /// itself. Allowing for anything here compounded a step a level down a
/// time down a chain of them. Three half steps come back as one whole /// chain of widgets each taking the whole of its parent.
/// one, since dividing by a whole box is dividing by one.
#[test] #[test]
fn the_whole_of_a_box_widens_by_the_routes_alone() { fn the_whole_of_a_box_maps_back_to_itself() {
let at = Px::from_int(956); let at = Px::from_int(956);
let one_step = |len: Px| Holds { assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
lo: len - Px::STEP,
hi: len + Px::STEP,
};
assert_eq!(Holds::at(at).through(Len::FULL), one_step(at));
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8)); let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
assert_eq!( assert_eq!(
Holds::at(at).through(less_eight), Holds::at(at).through(less_eight),
one_step(at + Px::from_int(8)) 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 /// 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. /// for on the way in belongs at the top of the range and not the bottom.
#[test] #[test]
+63
View File
@@ -0,0 +1,63 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on: the pixel lengths of its
/// frame and of its own box that its drawing and its answer hold for, and
/// the symbolic length of its own box where it read one.
///
/// The symbolic length is a pin rather than a range: a container places its
/// children as lengths of its frame measured from where its own box starts,
/// so what it draws turns on that box's length and on nothing about where it
/// is. It reaches the parent only where the box it pinned is the parent's
/// own; anywhere else the parent chose that length itself, and a widget
/// pinned this way is checked when it is re-placed.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub frame: [Holds; 2],
pub extent: [Holds; 2],
pub extent_len: [Option<Len>; 2],
}
impl LayoutHolds {
pub const ANY: Self = Self {
frame: [Holds::ANY; 2],
extent: [Holds::ANY; 2],
extent_len: [None; 2],
};
pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY;
for n in 0..2 {
result.frame[n] = self.frame[n].and(other.frame[n]);
result.extent[n] = self.extent[n].and(other.extent[n]);
debug_assert!(
self.extent_len[n].is_none()
|| other.extent_len[n].is_none()
|| self.extent_len[n] == other.extent_len[n]
);
result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]);
}
result
}
pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| {
self.frame[n].lo <= other.frame[n].lo
&& self.frame[n].hi >= other.frame[n].hi
&& self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi
&& self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len))
})
}
pub fn contains(self, px: PxVec2, extent: UiRegion) -> bool {
AXES.into_iter().all(|axis| {
let n = axis as usize;
let len = extent.axis(axis).len();
self.frame[n].contains(px.axis(axis))
&& self.extent[n].contains(len.to_px(px.axis(axis)))
&& self.extent_len[n].is_none_or(|pinned| pinned == len)
})
}
}
+16 -6
View File
@@ -11,12 +11,16 @@ pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod holds; mod holds;
mod layout_holds;
mod painter; mod painter;
mod place;
mod render_state; mod render_state;
pub use active::*; pub use active::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use place::*;
pub use render_state::*; pub use render_state::*;
#[derive(Default)] #[derive(Default)]
@@ -68,17 +72,24 @@ impl Moves {
} }
} }
/// Composes a region held in `idx`'s coordinates down the chain, which is /// The same walk the vertex shader does, in the same `Len` the shader is
/// the same walk the vertex shader does. /// handed, for asking where a drawing will actually land -- hit testing,
/// and nothing layout decides on. Layout threads its lengths down the
/// draw instead, so no box it compares is composed back up this chain.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion { pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local; let mut region = local;
self.walk(idx, |entry| region = region.within(entry));
region
}
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
let mut at = idx; let mut at = idx;
for _ in 0..CHAIN_LIMIT { for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE { if at == MoveIdx::NONE {
return region; return;
} }
let entry = self.arena[at.idx()]; let entry = &self.arena[at.idx()];
region = region.within(&entry.region); step(&entry.region);
at = entry.parent; at = entry.parent;
} }
debug_assert!( debug_assert!(
@@ -86,7 +97,6 @@ impl Moves {
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \ "a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth" and the shader stops at the same depth"
); );
region
} }
/// How many slots a region in `idx` is composed through, which is what /// How many slots a region in `idx` is composed through, which is what
+404 -212
View File
@@ -1,12 +1,12 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget, Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, RenderedText,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
WidgetId, Widgets, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::render_state::DrawInfo,
}; };
@@ -17,27 +17,57 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's box, in the coordinates of `move_idx`. /// What a fraction this widget declares or reports is a fraction of, in
pub(super) region: UiRegion, /// the coordinates of `move_idx`: forwarded from its parent unchanged
/// through a span, a stack or a scroll, and narrowed only by what was
/// decided above it -- a declared length, or the root. Its length
/// is the same on every ask of the widget, which is what keeps a fraction
/// under it from being resolved twice.
pub(super) frame: UiRegion,
/// Where this widget's drawing goes, in the frame's own coordinates.
/// Everything it writes is in these coordinates, and its children are
/// placed as parts of it.
pub(super) extent: UiRegion,
/// The extent's symbolic length where this draw read it, which makes the
/// drawing one that holds for that length alone -- the way reading a
/// length in pixels makes it hold for that number of pixels.
pub(super) extent_len: [Option<Len>; 2],
/// Symbolic box lengths read only to compute the answer. A container can
/// replace the provisional drawings used for that answer with drawings
/// in decided boxes, so this contract is independent of the final one.
pub(super) answer_extent_len: [Option<Len>; 2],
/// The frame in pixels, which its children's frames are a length of:
/// threaded down rather than composed back up the chain, so every length
/// in layout is one multiply from its parent's and [`Holds::through`]
/// inverts exactly that.
pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<UiRegion>,
/// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked in /// The children asked about so far, so the first place each was asked in
/// is the one recorded as its offer. /// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>, pub(super) offered: Vec<WidgetId>,
/// The box this widget was first asked about in, in pixels. /// Whether this draw is at the place its parent first asked about, which
pub(super) offered_px: PxVec2, /// makes the questions it asks the ones a cold layout asks and their
/// Whether this draw is in that box, which makes the questions it asks /// answers the ones to keep.
/// the ones a cold layout asks and their answers the ones to keep.
pub(super) at_offer: bool, pub(super) at_offer: bool,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of its box in pixels, per axis: every /// What this draw itself read of its frame in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise. /// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2], pub(super) frame_own: [Holds; 2],
/// What the children it asked about and drew keep it to. /// The same for its own box.
pub(super) under: [Holds; 2], pub(super) extent_own: [Holds; 2],
/// Pixel-box dependencies used only to compute the answer. These do not
/// constrain a retained drawing placed inside that answer.
pub(super) answer_extent_own: [Holds; 2],
/// The final drawing kept for each child. Asking one child again replaces
/// its provisional drawing and therefore replaces this contract too.
pub(super) under: Vec<(WidgetId, LayoutHolds)>,
/// The movable region this widget's primitives are positioned through: /// The movable region this widget's primitives are positioned through:
/// its own when opted in, otherwise the nearest ancestor's. /// its own when opted in, otherwise the nearest ancestor's.
pub(super) move_idx: MoveIdx, pub(super) move_idx: MoveIdx,
@@ -57,6 +87,24 @@ impl<'a> Painter<'a> {
/// Takes the kind, for a caller writing many of one primitive. /// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) { fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
self.write_resolved(kind, primitive, region, self.resolve(region));
}
/// A box in this widget's extent coordinates, composed into the
/// coordinates its move slot is in: through the extent, then through the
/// frame the extent is a part of. The same two steps a recomposition
/// replays, so a moved drawing lands where a cold one does.
fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent).within(&self.frame)
}
fn write_resolved<P: Primitive>(
&mut self,
kind: PrimitiveKind<P>,
primitive: P,
region: UiRegion,
resolved: UiRegion,
) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites); diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
@@ -65,15 +113,15 @@ impl<'a> Painter<'a> {
kind, kind,
id: self.id, id: self.id,
primitive, primitive,
region, region: resolved,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx, move_idx: self.move_idx,
}, },
); );
self.push_primitive(h); self.push_primitive(RetainedPrimitive { handle: h, region });
} }
fn push_primitive(&mut self, h: PrimitiveHandle) { fn push_primitive(&mut self, h: RetainedPrimitive) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -81,90 +129,70 @@ impl<'a> Painter<'a> {
self.primitives.push(h); self.primitives.push(h);
} }
/// Writes a primitive to be rendered /// Writes a primitive over the whole of this widget's own box.
pub fn primitive(&mut self, primitive: impl PrimitiveLike) { pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region) self.primitive_at(primitive, UiRegion::FULL)
} }
/// Writes a primitive in a part of this widget's own box, in that box's
/// coordinates.
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region)); self.primitive_at(primitive, region);
} }
/// Sets a mask, in this widget's own box's coordinates.
pub fn set_mask(&mut self, region: UiRegion) { pub fn set_mask(&mut self, region: UiRegion) {
self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
let resolved = self.resolve(region);
let move_idx = self.move_idx;
self.mask = self.rsc.ui_mut().masks.push(Mask { self.mask = self.rsc.ui_mut().masks.push(Mask {
region, region: resolved,
move_idx: self.move_idx, move_idx,
}); });
} }
/// Draws a widget within this widget's region. /// Draws a widget in the whole of this widget's own box, with the frame
/// forwarded unchanged: what a container that is only a wrapper around
/// one child wants, and what every transparent container passes for the
/// frame.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_within(id, UiRegion::FULL) self.widget_at(id, UiRegion::FULL, [Place::Within(Part::All); 2])
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// Draws a child, saying what its fractions are of and where its drawing
/// it resolves into its box. Reading them depends on nothing -- the box /// goes.
/// that comes of them is kept on the child, and `redraw` compares it ///
/// there. /// `frame` is that reference, in this widget's own frame coordinates:
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] { /// [`UiRegion::FULL`] forwards this widget's frame, which is what a
declared_lens(self.rsc.widgets(), id.id()) /// container that only divides room passes, so a fraction under it means
} /// the same wherever it sits and however deeply it is nested. Narrowing
/// it is for what is decided from above, and a declared length narrows
/// Takes back a child that was drawn only to find out how long it is. /// it here.
/// Its drawing is dropped and it is not one of this widget's children ///
/// this frame; what it answered is still something this widget asked. /// `place` is where the drawing goes, per axis, as a part of this
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) { /// widget's extent: see [`Place`]. A narrowed frame is its own extent,
self.children.retain(|child| *child != id.id()); /// since the narrowing is what said where the drawing goes.
self.state.undraw_rec(id.id(), self.rsc); pub fn widget_at<'s, W: ?Sized>(
}
/// 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.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, frame: UiRegion,
) -> DrawResult<'s, 'a, W> { place: [Place; 2],
self.widget_at(id, region, None)
}
/// 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>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align: RegionAlign,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, Some(align))
}
fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align_override: Option<RegionAlign>,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id())); let align = self.rsc.widgets().alignment(id.id());
// Composing `FULL` through a box is not quite the identity in f32, let (local, extent) = frame_and_extent(
// so a child with nothing declared keeps the box it would have had. frame,
let local = match declared.iter().any(Option::is_some) { part_of(self.extent, place),
true => declared_box(region, declared, align), narrowed_by(declared, frame),
false => region, align,
}; );
let within = match local == UiRegion::FULL { let within = match local == UiRegion::FULL {
true => self.region, true => self.frame,
false => local.within(&self.region), false => local.within(&self.frame),
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
if region_node { if region_node {
@@ -176,17 +204,20 @@ impl<'a> Painter<'a> {
self.children.push(id.id()); self.children.push(id.id());
} }
let first_ask = self.offer(id.id()); let first_ask = self.offer(id.id());
let offer = match first_ask { let offer_place = if first_ask {
true => local, place
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer), } else {
self.state
.active
.get(&id.id())
.map_or(place, |a| a.offer_place)
}; };
let answers_offer = self.at_offer && local == offer; let px = local.size().to_px(self.px);
// The answer and what it holds for, both about the box asked in. The // The answer and what it holds for, both about the place asked in.
// child's record may say something else once its drawing has been // 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. // placed: a drawing made again in its placed box holds for that box.
let (size, holds) = self.state.draw_inner( let (size, answer_holds, holds) = self.state.draw_inner(
id.id(), id.id(),
within,
DrawInfo { DrawInfo {
layer: self.layer, layer: self.layer,
parent: Some(self.id), parent: Some(self.id),
@@ -194,28 +225,47 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
offer, frame: local,
offered_px: self.px_within_offer(offer), frame_abs: within,
align: align_override, part: extent,
place,
offer_place,
px,
}, },
None, None,
self.rsc, self.rsc,
); );
if answers_offer { let compose = |holds| in_parent(holds, local, extent, place, declared);
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds); let holds = compose(holds);
} match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
// Whatever the child's answer holds for keeps this one to the boxes Some((_, kept)) => *kept = holds,
// that give the child a length inside it. None => self.under.push((id.id(), holds)),
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size, size: in_parent_frame(size, local.size(), declared),
answer_holds: compose(answer_holds),
} }
} }
/// Takes back a child that was drawn only to find out how long it is.
/// Its drawing is dropped and it is not one of this widget's children
/// 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.under.retain(|(child, _)| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// What a widget's rules declare its lengths to be, which whoever draws
/// it resolves into its frame. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it
/// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
declared_lens(self.rsc.widgets(), id.id())
}
/// What a child says its length is without being drawn, if it can say. /// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size. /// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
@@ -244,46 +294,11 @@ 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>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
) -> Option<LayoutLen> {
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;
}
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
}
let px = self.state.px_of(self.move_idx, within);
let (size, holds) =
self.state
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits);
self.depend_on(child);
if first_ask {
let active = self.state.active.get_mut(&child.id()).unwrap();
active.answer = (size, holds);
}
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
Some(size.axis(axis))
}
/// Whether this is the first box a child is asked about in during a draw /// Whether this is the first box a child is asked about in during a draw
/// that is itself in the box it was asked in -- the question a cold /// that is itself the one its parent measured -- the question a cold
/// layout asks, whose answer is the one to keep. /// layout asks, whose answer is the one to keep. A drawing made again in
/// a box chosen from an answer asks about that box instead, and what it
/// hears back is not a measurement of anything.
fn offer(&mut self, child: WidgetId) -> bool { fn offer(&mut self, child: WidgetId) -> bool {
if !self.at_offer || self.offered.contains(&child) { if !self.at_offer || self.offered.contains(&child) {
return false; return false;
@@ -292,15 +307,6 @@ impl<'a> Painter<'a> {
true true
} }
/// The pixel size of a part of the box this widget was asked in.
fn px_within_offer(&self, local: UiRegion) -> PxVec2 {
let size = local.size();
PxVec2::new(
size.x.to_px(self.offered_px.x),
size.y.to_px(self.offered_px.y),
)
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) { if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id()); self.size_deps.push(child.id());
@@ -319,11 +325,15 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// Writes glyphs in the selected frame or extent coordinates.
// TODO: merge the text methods into the primitive ones. // TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
// Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs composing through the extent.
let resolved = self.resolve(origin);
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let mut region = origin; let place = |mut region: UiRegion| {
region.x.end = region.x.start; region.x.end = region.x.start;
region.y.end = region.y.start; region.y.end = region.y.start;
let mut region = region.offset(UiVec2::from_px(glyph.offset)); let mut region = region.offset(UiVec2::from_px(glyph.offset));
@@ -333,7 +343,9 @@ impl<'a> Painter<'a> {
); );
region.x.end = region.x.start.offset(size.x); region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y); region.y.end = region.y.start.offset(size.y);
self.write( region
};
self.write_resolved(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
uv_min: glyph.entry.uv_min, uv_min: glyph.entry.uv_min,
@@ -342,15 +354,44 @@ impl<'a> Painter<'a> {
color: text.color, color: text.color,
flags: glyph.entry.flags(), flags: glyph.entry.flags(),
}, },
region, place(origin),
place(resolved),
); );
} }
} }
/// This widget's box, in the coordinates its own primitives are written /// The symbolic length of this widget's own box along one axis, in the
/// in -- so a region composed `within` it may be drawn directly. /// lengths of its frame that it places its children in. Reading it pins
pub fn region(&self) -> UiRegion { /// the drawing to that length -- and to nothing about where the box
self.region /// starts, which is what lets a container move without being drawn
/// again. One axis at a time, because a container that divides one axis
/// holds for any length of the other.
pub fn extent_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len();
self.extent_len[axis as usize] = Some(len);
self.answer_extent_len[axis as usize] = Some(len);
len
}
/// The symbolic length used to compute this widget's answer, where the
/// final drawing itself is rebuilt without depending on that length.
pub fn answer_extent_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len();
self.answer_extent_len[axis as usize] = Some(len);
len
}
/// Says that the final drawing uses a symbolic length already read for
/// the answer.
pub fn drawing_uses_extent_len(&mut self, axis: Axis, len: Len) {
debug_assert_eq!(self.extent.axis(axis).len(), len);
self.extent_len[axis as usize] = Some(len);
}
/// A part of this widget's box, expressed in its frame coordinates so it
/// can be used as a child frame decided here.
pub fn extent_part(&self, axis: Axis, part: Part) -> UiSpan {
part.of(*self.extent.axis(axis))
} }
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
@@ -378,49 +419,83 @@ impl<'a> Painter<'a> {
.is_some() .is_some()
} }
/// The part of this widget's box that something of `size` takes, at the /// This widget's own box in pixels. Reading it makes the drawing one
/// near edge. A container that reports one child's size gives every child /// that holds for this box only, until `holds` says how far it goes.
/// 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])
}
/// 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.
pub fn px_size(&mut self) -> PxVec2 { pub fn px_size(&mut self) -> PxVec2 {
let px = self.state.px_of(self.move_idx, self.region); PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
if *own == Holds::ANY {
*own = Holds::at(len);
}
}
px
} }
/// One axis of this widget's box in pixels. Prefer this to /// This widget's own box in pixels, used only to compute its answer.
pub fn answer_px_size(&mut self) -> PxVec2 {
PxVec2::new(
self.answer_px_len(Axis::X),
self.answer_px_len(Axis::Y),
)
}
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px { pub fn px_len(&mut self, axis: Axis) -> Px {
let len = self.state.px_of(self.move_idx, self.region).axis(axis); let part = self.extent.axis(axis).len();
let own = &mut self.own[axis as usize]; let len = part.to_px(self.px.axis(axis));
let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
} }
len len
} }
/// The lengths of this widget's box on `axis` that what it is drawing /// One pixel length used only to compute this widget's answer. The final
/// drawing may be retained when that answer is placed in another box.
pub fn answer_px_len(&mut self, axis: Axis) -> Px {
let len = self.extent.axis(axis).len().to_px(self.px.axis(axis));
let own = &mut self.answer_extent_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// The lengths of this widget's own box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets /// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its /// of the box, and the same reported size. A widget that read its length
/// length in pixels holds for that one alone until it says otherwise. /// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) { pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let part = self.extent.axis(axis).len();
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)), holds.contains(part.to_px(self.px.axis(axis))),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box", "'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(), self.label(),
self.id self.id
); );
self.own[axis as usize] = holds; self.extent_own[axis as usize] = holds;
}
/// One axis of this widget's frame in pixels -- what a fraction of its
/// area resolves against, and so what a container divides among its
/// children. Its own box is a part of this one.
pub fn frame_px_len(&mut self, axis: Axis) -> Px {
let len = self.px.axis(axis);
let own = &mut self.frame_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// [`Self::holds`] stated about the frame rather than about this
/// widget's own box, for a container whose drawing turns on what its
/// fractions are of rather than on the part of it it took.
pub fn frame_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its frame",
self.label(),
self.id
);
self.frame_own[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -463,6 +538,7 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>, painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>, child: &'p StrongWidget<W>,
size: Size, size: Size,
answer_holds: LayoutHolds,
} }
impl<W: ?Sized> DrawResult<'_, '_, W> { impl<W: ?Sized> DrawResult<'_, '_, W> {
@@ -473,6 +549,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::size_read(self.child.id(), self.painter.id, self.size); diag::size_read(self.child.id(), self.painter.id, self.size);
} }
self.painter.depend_on(self.child); self.painter.depend_on(self.child);
self.painter.answer_under = self.painter.answer_under.and(self.answer_holds);
self.size self.size
} }
@@ -506,6 +583,72 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// What a child depends on, said about the boxes the widget that drew it
/// has rather than the ones the child was given.
///
/// `frame` is the child's frame in this widget's frame coordinates and
/// `extent` the box it was given, in the child's own frame coordinates. Both
/// reach it as one length, so what it holds for maps back through that
/// length exactly -- and where the box it was given is this widget's own,
/// what it says about that box is what this widget can say about its own.
pub(crate) fn in_parent(
holds: LayoutHolds,
frame: UiRegion,
extent: UiRegion,
place: [Place; 2],
declared: [Option<LayoutLen>; 2],
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
let frame_len = frame.axis(axis).len();
result.frame[n] = holds.frame[n].through(frame_len);
match (place[n].part(), declared[n]) {
// Its box is this widget's own, or a part of it in that box's
// own lengths: so what it holds for is a range on this widget's
// own box, which is what lets that box move without a redraw. A
// length it pinned is this widget's length wherever the part is
// the whole of it, and pins the same way.
(Part::All, None) if *frame.axis(axis) == UiSpan::FULL => {
result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n];
}
// Its box is a part of this widget's own box, in that box's own
// lengths, so what it holds for maps back through that part into
// a range on this widget's box.
(Part::Of(span), None) => {
result.extent[n] = holds.extent[n].through(span.len());
}
// Its box is a part of this widget's frame: a length of the
// frame is all that reaches it, so what it holds for is a range
// on the frame and none of it on this widget's own box.
_ => {
result.frame[n] = result.frame[n].and(
holds.extent[n]
.through(extent.axis(axis).len())
.through(frame_len),
);
}
}
}
result
}
/// A child's answer as lengths of the parent's own region. A widget reports
/// a fraction of its own region, and `of` is that region as a length of this
/// one. Pixels come through untouched, being that many pixels wherever they
/// end up. A declared axis is already the parent's: it resolved the rule in
/// its own region, and the rule is what the report says.
fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis));
}
}
size
}
/// What a widget declares a length of its box to be. `leftover` is not one: a /// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one, /// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead. /// so it passes up in the size instead.
@@ -526,52 +669,101 @@ 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 /// Whether what a widget reported along an axis is the whole of the box it
/// the box it was asked in that its alignment says. An axis reported as a /// is in rather than a part to be placed inside it. A share fills, because a
/// share fills, because a share is a length only to whoever divides one, and /// share is a length only to whoever divides one, and whoever did is the one
/// whoever did is the one that handed down this box. A declared axis is /// that handed down this box. A declared axis does too: the rule already gave
/// left alone too: `declared_box` already placed it, in the parent's box, /// the region its length, and the rule's length is what the widget reports
/// and the rule's length is what the widget reports there. /// there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// Where a widget's drawing goes inside the part its parent gave it: what
/// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it.
/// ///
/// A reported fraction is a fraction of the box the widget drew in, where a /// The length it reported is a length of its frame, and the part is one too,
/// declared one is a fraction of the box its parent handed down -- a span /// so this takes one from the other rather than composing it into the part.
/// reporting `rel(1.0)` means all of what it was given, whatever that was a /// That is what makes a fraction the same fraction wherever the part it is
/// fraction of. So this scales by the box rather than composing into it. /// placed in sits and however long it is -- the fraction is resolved once,
pub(crate) fn placed_box( /// here, against the frame it was reported of.
region: UiRegion, pub(crate) fn placed_extent(
part: UiRegion,
size: Size, size: Size,
align: RegionAlign,
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
fill: [bool; 2],
align: RegionAlign,
) -> UiRegion { ) -> UiRegion {
let mut placed = region; let mut placed = part;
for (axis, declared) in AXES.into_iter().zip(declared) { for axis in AXES {
let n = axis as usize;
let reported = size.axis(axis); let reported = size.axis(axis);
if reported.leftover != Weight::ZERO || declared.is_some() { if fills(reported, declared[n], fill[n]) {
continue; continue;
} }
let len = Len::from_parts(reported.rel, reported.px);
let span = placed.axis_mut(axis); let span = placed.axis_mut(axis);
let len = span.len().scale(reported.rel) + Len::from_parts(Rel::ZERO, reported.px);
span.start += (span.len() - len).scale(align.axis(axis).rel()); span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len; span.end = span.start + len;
} }
placed placed
} }
/// Takes a widget's declared lengths in the box `region` is given in, since a /// The part of a widget's own box a `place` names, in the coordinates that
/// fraction of a length means a fraction of that one, and puts what is left /// box is in.
/// over on the side its alignment says. A caller that already reserved the pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion {
/// space hands back the same length, so this is the identity for it. let mut part = extent;
pub(crate) fn declared_box( for axis in AXES {
mut region: UiRegion, *part.axis_mut(axis) = place[axis as usize].part().of(*extent.axis(axis));
declared: [Option<LayoutLen>; 2],
align: RegionAlign,
) -> UiRegion {
for (axis, len) in AXES.into_iter().zip(declared) {
let Some(len) = len else { 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());
span.end = span.start + len;
} }
region part
}
/// The length a rule gives a child's frame, per axis: a fraction in it is a
/// fraction of the frame the child was given, which is the one length the
/// rule can mean.
pub(crate) fn narrowed_by(declared: [Option<LayoutLen>; 2], frame: UiRegion) -> [Option<Len>; 2] {
AXES.map(|axis| {
declared[axis as usize]
.map(|len| Len::from_parts(len.rel, len.px).within_len(frame.axis(axis).len()))
})
}
/// The frame a child is asked in and the box its drawing goes in, both in
/// the coordinates of the widget asking.
///
/// `frame` is what the caller said the child's fractions are of, and `part`
/// what of the caller's own box the drawing takes. `narrow` is the length a
/// declared rule gives the frame, which makes the frame the box the drawing
/// goes in -- a rule is what decided where it goes, and there is nothing
/// left to place inside it. A caller that narrowed the frame itself said the
/// same thing.
///
/// The length is the caller's to supply so that a widget asked again gets
/// the frame it already has rather than a second resolution of its rule.
pub(crate) fn frame_and_extent(
mut frame: UiRegion,
part: UiRegion,
narrow: [Option<Len>; 2],
align: RegionAlign,
) -> (UiRegion, UiRegion) {
let mut extent = part;
for (axis, narrow) in AXES.into_iter().zip(narrow) {
let span = frame.axis_mut(axis);
let narrowed = match narrow {
Some(len) => {
let slot = part.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*span = UiSpan::new(start, start + len);
true
}
None => *span != UiSpan::FULL,
};
if narrowed {
*extent.axis_mut(axis) = UiSpan::FULL;
}
}
(frame, extent)
} }
+61
View File
@@ -0,0 +1,61 @@
use crate::{PrimitiveHandle, UiRegion, UiSpan};
/// What of a widget's own box a child is given, along one axis.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Part {
/// The whole of it.
All,
/// Frame lengths from where the box starts, which is what a container
/// dividing room among its children speaks: a child's report is a length
/// of the frame, so the cursor that sums those reports is one too. A
/// moved box re-places every child by re-adding its start, exactly.
From(UiSpan),
/// A part of the box in its own coordinates, which is what a container
/// that insets one speaks: taking eleven pixels off the end needs no
/// length, where saying the same thing in frame lengths would make the
/// container read its own box -- and a box chosen from its own answer
/// then feeds back into the answer.
Of(UiSpan),
}
impl Part {
/// Where it lands in the coordinates `extent` is in.
pub(crate) fn of(self, extent: UiSpan) -> UiSpan {
match self {
Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Of(span) => span.within(&extent),
}
}
}
/// Where a child goes along one axis, as a part of this widget's box.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Place {
/// The child's answer, aligned inside the part by the child's alignment.
Within(Part),
/// Exactly the part; the answer is not placed inside it again.
Fill(Part),
}
impl Place {
pub(crate) fn part(self) -> Part {
match self {
Self::Within(part) | Self::Fill(part) => part,
}
}
/// Whether the part is the drawing's box outright, rather than the box
/// the answer is placed inside.
pub(crate) fn fills(self) -> bool {
matches!(self, Self::Fill(_))
}
}
/// A primitive as it was written: its box in the widget's own box's
/// coordinates, which is what a move of that box re-composes from.
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: UiRegion,
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,4 +1,4 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SlotId { pub struct SlotId {
idx: u32, idx: u32,
genr: u32, genr: u32,
+5 -1
View File
@@ -20,10 +20,14 @@ impl DefaultAppState for Client {
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
( (
// The square is one widget and the two shares of the row it
// sits centred in are another: a length is a property of a
// widget, so `.width` here would overwrite the `.sized`.
rrect rrect
.color(Color::RED) .color(Color::RED)
.sized((100, 100)) .sized((100, 100))
.center() .center()
.wrapper()
.width(leftover(2)), .width(leftover(2)),
( (
rrect.color(Color::ORANGE), rrect.color(Color::ORANGE),
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(rsc);
let main = WidgetPtr::new().add(rsc); let main = Wrapper::new().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new()))); let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| { let mut switch_button = |color, to: WeakWidget, label| {
+7 -3
View File
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
.pad(16) .pad(16)
.background(panel()); .background(panel());
// Each one takes the whole width, because `text_align` puts the
// glyphs somewhere in the box the text is given and a text that
// reports the width of its own glyphs is given exactly that.
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
let aligned = ( let aligned = (
wtext("left").size(24).text_align(Align::LEFT), label("left", Align::LEFT),
wtext("centred").size(24).text_align(Align::CENTER), label("centred", Align::H_CENTER),
wtext("right").size(24).text_align(Align::RIGHT), label("right", Align::RIGHT),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.gap(8) .gap(8)
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height)); render.resize((size.width, size.height), rsc.widgets_mut());
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
+3 -3
View File
@@ -144,9 +144,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test // bound that comes with `SyncSender` is far past anything a test
// leaves unread. // leaves unread.
let (send, updates) = sync_channel(1024); let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send))); let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize(size); render.resize(size, rsc.widgets_mut());
Self { Self {
rsc, rsc,
render, render,
@@ -161,7 +161,7 @@ impl Harness {
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size); self.render.resize(size, self.rsc.widgets_mut());
} }
/// Changes a length rule after the fact, the way `.width()` sets one. /// Changes a length rule after the fact, the way `.width()` sets one.
+9 -7
View File
@@ -117,16 +117,18 @@ pub struct Branch {
impl Widget for Branch { impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL; let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
top.y.end = top.y.start.offset(Px::from_int(40)); let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let mut below = UiRegion::FULL; let below = Place::Within(Part::Of(UiSpan::new(cut, Len::FULL)));
below.y.start = below.y.start.offset(Px::from_int(40)); let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) { match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below), true => painter.widget_at(&self.wide, UiRegion::FULL, place),
false => painter.widget_within(&self.narrow, below), false => painter.widget_at(&self.narrow, UiRegion::FULL, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
+1 -1
View File
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(UiRegion::FULL);
painter.widget(&self.inner); painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll` // What it occupies is its box, on both axes, for the reason `Scroll`
// reports the same: it clips what is inside to that box, so it can // reports the same: it clips what is inside to that box, so it can
+2 -2
View File
@@ -1,15 +1,15 @@
mod image; mod image;
mod mask; mod mask;
mod position; mod position;
mod ptr;
mod rect; mod rect;
mod text; mod text;
mod trait_fns; mod trait_fns;
mod wrapper;
pub use image::*; pub use image::*;
pub use mask::*; pub use mask::*;
pub use position::*; pub use position::*;
pub use ptr::*;
pub use rect::*; pub use rect::*;
pub use text::*; pub use text::*;
pub use trait_fns::*; pub use trait_fns::*;
pub use wrapper::*;
+9 -2
View File
@@ -7,7 +7,14 @@ pub struct Offset {
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let region = UiRegion::FULL.offset(self.amt); // The whole of this widget's box, moved: the frame passes through, so
painter.widget_within(&self.inner, region).size() // what the child declares or reports means the same as it would
// without the offset.
let moved = |len: Len, amt: Len| Place::Within(Part::From(UiSpan::new(amt, len + amt)));
let place = [
moved(painter.extent_len(Axis::X), self.amt.x),
moved(painter.extent_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, UiRegion::FULL, place).size()
} }
} }
+24 -5
View File
@@ -13,9 +13,24 @@ impl Widget for Pad {
// it; where the box is bigger -- a share of a row, a rule over this // 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 // 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. // edge pinned it to a corner it had not asked for.
let inner = painter //
.widget_within(&self.inner, self.padding.region()) // The padding goes around what it pads: the frame passes through, so
.size(); // the inner's fractions mean what they would without it, and only
// the box it draws in is moved in by the pixels. Said as a part of
// this widget's own box in that box's own lengths, so nothing here
// reads how long the box is -- and a box chosen from this widget's
// own answer therefore does not feed back into that answer.
let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead),
Len::from_parts(Rel::ONE, -trail),
)))
};
let place = [
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
let inner = painter.widget_at(&self.inner, UiRegion::FULL, place).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
@@ -53,14 +68,18 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
pub fn region(&self) -> UiRegion { /// `region` less this padding on each side.
let mut region = UiRegion::FULL; pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left; region.x.start.px += self.left;
region.y.start.px += self.top; region.y.start.px += self.top;
region.x.end.px -= self.right; region.x.end.px -= self.right;
region.y.end.px -= self.bottom; region.y.end.px -= self.bottom;
region region
} }
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = Px::from_num(amt);
Self { Self {
+22 -19
View File
@@ -12,15 +12,14 @@ pub struct Scroll {
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let container_len = painter.px_len(self.axis); let container_len = painter.px_len(self.axis);
// Draw in the whole container only when its scrolling-axis length is // Measured in the whole viewport, then drawn at the scrolled offset.
// not already known, then draw it at the scrolled offset. let whole = UiRegion::FULL;
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) { let answer_len = painter
Some(len) => len, .widget_at(&self.inner, whole, [Place::Fill(Part::All); 2])
None => painter.widget(&self.inner).size().axis(self.axis), .len(self.axis);
}; let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len);
let content = answer_len.apply_leftover();
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(container_len); self.content_len = fixed.max(container_len);
if self.snap_end { if self.snap_end {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
@@ -33,9 +32,9 @@ impl Widget for Scroll {
// the drawing holds for that length alone. One scrolled part way sits // the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to // where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length. // the end, it moves with every length.
let fixed_len = content.rel == Rel::ZERO; let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO;
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, self.content_len..=Px::MAX); painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end { } else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt; let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left); painter.holds(self.axis, Px::MIN..=left);
@@ -46,7 +45,7 @@ impl Widget for Scroll {
// have placed the whole scroll in a box longer than it. // have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(Px::ZERO); let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel()); let anchor = slack.mul(align.rel());
let mut region = UiRegion::FULL; let mut content = UiSpan::FULL;
// Content that fills the viewport and has not been scrolled is the // Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. Writing the same box as // viewport, and is handed back as it came. Writing the same box as
// its own length in pixels is the same box in another form, and the // its own length in pixels is the same box in another form, and the
@@ -55,15 +54,19 @@ impl Widget for Scroll {
// each part of it. // each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO; let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
if moved || self.content_len != self.container_len { if moved || self.content_len != self.container_len {
let offset = UiVec2::from_axis( let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
self.axis, content = UiSpan::new(start, start.offset(self.content_len));
Len::from_parts(Rel::ZERO, anchor - self.amt),
Len::ZERO,
);
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 frame, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is drawn is the
// content box, scrolled.
painter.widget_at(
&self.inner,
whole,
self.axis
.pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)),
);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
+67 -53
View File
@@ -10,20 +10,30 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// A length for every child before their final boxes are chosen: from // The row: this span's own box, as a length of the frame its children
// a hint where one exists, and from drawing otherwise. // are laid out against. Its start is nothing's business -- a slot is
// a length from it -- so what this reads is the length alone.
let far = painter.answer_extent_len(axis);
let measure_along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = Place::Within(Part::All);
// A length for every child before their final slots are chosen. The
// frame passes through unchanged, so `rel(0.5)` is half the area this
// span was given whatever else is in it and wherever this child sits
// among them; what it is drawn in is the room left from the cursor,
// because a text has to wrap at the width actually there.
let mut cursor = Len::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let mut span = UiSpan::new(cursor, Len::rel_max()); let room = Place::Fill(Part::From(measure_along(cursor, far)));
if self.dir.sign == Sign::Neg { let len = painter
span.flip(); .widget_at(child, UiRegion::FULL, axis.pair(room, across))
} .len(axis);
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),
};
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
lens.push(len); lens.push(len);
@@ -40,45 +50,41 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
let fixed_total = Len::from_parts(total.rel, total.px);
// What is left for the shares to divide: the row less everything
// fixed, as a length of the frame rather than a number of pixels.
let room = far - fixed_total;
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. The room to // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// divide is `len * fixed - total.px`, and the length where it runs // itself, and answered back through the same expression, so the
// out is exactly the box a parent sizing itself from this answer // boundary is the drawing's own and not a second way of finding it:
// hands back -- which is why this used to need a margin either side // the three cases a rounded division needed -- the fixed parts
// of the boundary, and why it does not now: that box and this sum are // growing slower than the box, faster, or exactly with it -- are the
// whole counts of the same step, and both routes to it land on the // sign of `room.rel`, which `through` already reads. What the
// same count. What the generated oracle checks is the consequence, // generated oracle checks is the consequence, since which children
// since which children exist at all turns on this. // exist at all turns on this.
let fixed = Rel::ONE - total.rel;
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
let current = painter.px_len(axis); shares = room.to_px(painter.frame_px_len(axis)) > Px::ZERO;
let holds = if fixed > Rel::ZERO { let holds = match shares {
// The box length the fixed parts alone fill. true => Holds::from(Px::STEP..=Px::MAX),
let full = total.px.div(fixed); false => Holds::from(Px::MIN..=Px::ZERO),
shares = current > full;
match shares {
true => Holds::from(full.next_up()..=Px::MAX),
false => Holds::from(Px::MIN..=full),
}
} else if fixed < Rel::ZERO {
// The relative parts grow faster than the box does, so here
// a shorter box is the one that leaves room.
let full = total.px.div(fixed);
shares = current < full;
match shares {
true => Holds::from(Px::MIN..=full.next_down()),
false => Holds::from(full..=Px::MAX),
}
} else {
// The relative parts take exactly the box, whatever it is, so
// the only room is what negative pixels leave.
shares = total.px < Px::ZERO;
Holds::ANY
}; };
painter.holds(axis, holds); painter.frame_holds(axis, holds.through(room));
} }
if shares {
painter.drawing_uses_extent_len(axis, far);
}
let drawing_far = match shares {
true => far,
false => fixed_total,
};
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(drawing_far - to, drawing_far - from),
};
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them // rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. A rule // answers nothing and makes its size depend on theirs for it. A rule
@@ -93,7 +99,6 @@ impl Widget for Span {
// row. // row.
let mut fixed = Len::rel_min(); let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO; 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 start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for (child, len) in self.children.iter().zip(&lens) {
@@ -106,20 +111,29 @@ impl Widget for Span {
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let mut span = UiSpan::FULL; let from = start;
span.start = start;
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
taken += len.leftover; taken += len.leftover;
} }
fixed.px += len.px; fixed.px += len.px;
fixed.rel += len.rel; fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room); start = shared(fixed, taken, total.leftover, room);
span.end = start; // Along the row the span says where the child goes, and that slot
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); // is the drawing's box outright rather than something to place an
if self.dir.sign == Sign::Neg { // answer inside again.
region.flip(axis); let span = along(from, start);
} let (frame, slot) = match len.leftover > Weight::ZERO && shares {
let placed = painter.widget_within(child, region); true => (
UiRegion::from_axis(
axis,
painter.extent_part(axis, Part::From(span)),
UiSpan::FULL,
),
Place::Fill(Part::All),
),
false => (UiRegion::FULL, Place::Fill(Part::From(span))),
};
let placed = painter.widget_at(child, frame, axis.pair(slot, across));
if shrinks { if shrinks {
let used = placed.len(!axis); let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
+17 -13
View File
@@ -13,31 +13,35 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
}; };
// Whichever child sizes the stack decides the box every child gets. // Whichever child sizes the stack is given the stack's whole box --
// The stack reports that size, so a child given a longer box would // the stack is the length that child asked for, so placing that
// draw outside what the stack says it occupies. // answer inside the box it decided would apply it twice.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse // On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained // rather than a second drawing of it somewhere else: a retained
// drawing belongs to the layer it was made on. // drawing belongs to the layer it was made on.
Some((i, child)) => { Some((i, child)) => {
painter.child_layer_at(i); painter.child_layer_at(i);
painter.widget(child).size() painter
.widget_at(child, UiRegion::FULL, [Place::Fill(Part::All); 2])
.size()
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i); painter.child_layer_at(i);
// The sizing child placed its own content in the box its answer let place = [Axis::X, Axis::Y].map(|axis| {
// decided, and this box was derived from that answer, so applying let len = size.axis(axis);
// its alignment again here would place it twice. Every other let part = match len.leftover > Weight::ZERO {
// child is handed a box that owes nothing to its own answer, and true => Part::All,
// where it sits in one bigger than itself is its own business. false => Part::From(UiSpan::new(Len::ZERO, Len::from_parts(len.rel, len.px))),
match sizing == Some(i) {
true => painter.widget_aligned(child, region, RegionAlign::NEAR),
false => painter.widget_within(child, region),
}; };
Place::Within(part)
});
painter.widget_at(child, UiRegion::FULL, place);
} }
size size
} }
+11 -11
View File
@@ -50,15 +50,11 @@ impl TextView {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X)); let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
// The shaper measures in floats, which is where a glyph advance comes // The shaper measures in floats, which is where a glyph advance comes
// from; what it answers goes back on the grid. // from; what it answers goes back on the grid.
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32)); painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
// A greedy break is the same break at every width from its longest if width.is_some() {
// line up to the one it was made at: each line still fits, and none painter.holds(Axis::X, self.buf.width_holds());
// could take a word that did not fit in the wider box. A line too
// long to fit at all says nothing about narrower boxes.
if let Some(width) = width {
painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width);
} }
text self.buf.rendered().expect("render_text placed the glyphs")
} }
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
@@ -78,9 +74,13 @@ impl TextView {
let tex = self.render(painter); let tex = self.render(painter);
let region = tex.size.align(align); let region = tex.size.align(align);
let size = Size::px(tex.size); // The step at or above what the shaper measured, so a parent that
let within = region.within(&painter.region()); // hands back the length this reports hands back a box the longest
painter.glyphs(tex, within); // line fits in. Rounded to the nearest step it is half the time a
// hair under that line, and the break made in it is not the break a
// cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
painter.glyphs(tex, region);
(region, size) (region, size)
} }
+7 -3
View File
@@ -134,9 +134,13 @@ widget_trait! {
|state| self.add(state) |state| self.add(state)
} }
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) { // Named for the type it makes rather than as `wrapped`, which would read
let id = self.add_strong(state); // as the text setting. `widget_trait!` takes no attributes, so what it is
state.ui_mut().widgets[ptr].inner = Some(id); // for is on `Wrapper` itself.
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|state| Wrapper {
inner: Some(self.add_strong(state)),
}
} }
} }
+12 -4
View File
@@ -1,11 +1,19 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::Unsize; use std::marker::Unsize;
pub struct WidgetPtr { /// One widget in a box of its own, doing as little as possible on the way:
/// it draws its child in the whole of its box and reports back what the child
/// said. It exists because a length and an alignment are properties of one
/// widget, so a widget cannot both be 100 wide and take two shares of a row
/// -- the two lengths need two widgets, and this is the smaller one.
///
/// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for.
pub struct Wrapper {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
} }
impl Widget for WidgetPtr { impl Widget for Wrapper {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
match &self.inner { match &self.inner {
Some(id) => painter.widget(id).size(), Some(id) => painter.widget(id).size(),
@@ -14,7 +22,7 @@ impl Widget for WidgetPtr {
} }
} }
impl WidgetPtr { impl Wrapper {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -35,7 +43,7 @@ impl WidgetPtr {
} }
} }
impl Default for WidgetPtr { impl Default for Wrapper {
fn default() -> Self { fn default() -> Self {
Self::empty() Self::empty()
} }
+9 -7
View File
@@ -21,16 +21,18 @@ struct BranchesOnMeasurement {
impl Widget for BranchesOnMeasurement { impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL; let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
top.y.end = top.y.start.offset(Px::from_int(40)); let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut)));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let mut below = UiRegion::FULL; let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
below.y.start = below.y.start.offset(Px::from_int(40)); let place = [Place::Within(Part::All), below];
match px > Px::from_f32(self.threshold) { match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below), true => painter.widget_at(&self.wide, UiRegion::FULL, place),
false => painter.widget_within(&self.narrow, below), false => painter.widget_at(&self.narrow, UiRegion::FULL, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
+130 -10
View File
@@ -20,6 +20,88 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
assert_corners!(h, right, (100, 0), (400, 200)); assert_corners!(h, right, (100, 0), (400, 200));
} }
/// A span places each child in the room left after the one before, because a
/// text has to wrap at the width actually there, but the child's region is
/// the whole row. So two children asking for half each take the whole row
/// between them, however much of it was left when each was asked, and a third
/// overflows -- and a span passes its own region on unchanged, so a child of
/// a nested span asking for half asks for half of the same row.
#[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported, and its own child
// asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100));
}
/// The same fraction either way round: after a 100 px child in a 400 px row,
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
/// drew half of what it was offered and reported that. Half the row, not half
/// of the 300 px left of it.
#[test]
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
let mut declaring = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(declaring, declared, (100, 0), (300, 100));
let mut reporting = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(reporting, reported, (100, 0), (300, 100));
}
/// What the fraction a child reports is of and what box it is offered are
/// two different lengths, and only the first is the whole row: a text still
/// wraps at the room actually left after its neighbour, so the same
/// paragraph is taller where less of the row is left for it.
#[test]
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
let paragraph = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer.";
let height_after = |head_width: i32| {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
let region = h.region(&text).unwrap();
(region.bot_right.y - region.top_left.y).to_f32()
};
let (crowded, whole_row) = (height_after(300), height_after(0));
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// The same reading through a pad: padding goes around what it pads and
/// does not narrow what a fraction under it is a fraction of, so half of the
/// window plus the padding is what the pad takes and where the next child
/// starts.
#[test]
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_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, inner, (10, 10), (210, 90));
assert_corners!(h, padded, (0, 0), (220, 100));
assert_corners!(h, tail, (220, 0), (320, 100));
}
#[test] #[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() { fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -225,21 +307,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
h.set_root((bar, buried).span(Dir::RIGHT)); h.set_root((bar, buried).span(Dir::RIGHT));
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1, "only the root region"); assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
h.rsc.widgets_mut().set_region_node(buried, true); h.rsc.widgets_mut().set_region_node(buried, true);
h.frame(); h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!( assert_eq!(
h.render.moves.depth(move_idx), h.render.moves.depth(move_idx),
2, 1,
"the opted-in widget's region and the root region" "the opted-in widget's region alone"
); );
h.rsc.widgets_mut().set_region_node(buried, false); h.rsc.widgets_mut().set_region_node(buried, false);
h.frame(); h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1); assert_eq!(h.render.moves.depth(move_idx), 0);
} }
/// A span that sizes from its children passes their `leftover` weight up /// A span that sizes from its children passes their `leftover` weight up
@@ -341,16 +423,16 @@ fn a_row_of_equal_shares_fills_it_exactly() {
} }
} }
/// Where the shader puts an edge: the two parts of a scalar are floored /// Where the shader puts an edge: the fraction resolved against the window
/// apart, so a fraction and a pixel offset snap independently, and each is /// plus the pixel offset, taken to the boundary it composes to within half
/// taken to the boundary it composes to within half a step of. Kept in step /// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
/// with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region); let drawn = active.extent.within(&active.frame_abs);
let region = h.render.moves.resolve(active.parent_move, drawn);
let dim = h.size().axis(axis); let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor(); let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: 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); let span = region.axis(axis);
(edge(span.start), edge(span.end)) (edge(span.start), edge(span.end))
} }
@@ -620,3 +702,41 @@ fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
} }
} }
} }
#[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));
}
+778 -24
View File
@@ -12,6 +12,7 @@ struct Counted {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
size: Size, size: Size,
reads_box: bool, reads_box: bool,
reads_answer_box: bool,
} }
impl Widget for Counted { impl Widget for Counted {
@@ -19,6 +20,8 @@ impl Widget for Counted {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
if self.reads_box { if self.reads_box {
painter.px_size(); painter.px_size();
} else if self.reads_answer_box {
painter.answer_px_size();
} }
self.size self.size
} }
@@ -38,11 +41,18 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
draws: draws.clone(), draws: draws.clone(),
size, size,
reads_box, reads_box,
reads_answer_box: false,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
(id, Counts(draws)) (id, Counts(draws))
} }
fn answer_counted(h: &mut Harness, size: Size) -> (WeakWidget<Counted>, Counts) {
let (id, draws) = counted(h, size, false);
h.rsc[id].reads_answer_box = true;
(id, draws)
}
struct Layered { struct Layered {
children: [StrongWidget<Rect>; 2], children: [StrongWidget<Rect>; 2],
_revision: usize, _revision: usize,
@@ -156,16 +166,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT)); h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1); assert_eq!(told_draws.get(), 1);
// Reading its box makes its drawing hold for the measuring box alone, // Its final slot is a parent decision, so it is evaluated there after
// and it reports less than that box: so it is drawn again in the box its // the provisional ask established its length.
// answer places it in, and once more in the final box the span chooses. assert_eq!(asked_draws.get(), 2);
// A widget that says what it holds for, as text does, skips the middle
// one.
assert_eq!(
asked_draws.get(),
3,
"drawn to be measured, in its placed box, then in its final box"
);
} }
#[test] #[test]
@@ -220,9 +223,12 @@ struct FromHint {
impl Widget for FromHint { impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap(); let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL; let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
region.y.end = region.y.start.offset(len.px); painter.widget_at(
painter.widget_within(&self.inner, region); &self.inner,
UiRegion::FULL,
[Place::Within(Part::All), Place::Within(Part::From(top))],
);
Size::LEFTOVER Size::LEFTOVER
} }
} }
@@ -244,7 +250,7 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
assert_corners!(h, inner, (0, 0), (400, 120)); assert_corners!(h, inner, (0, 0), (400, 120));
} }
/// Reads its box's size, which nothing but its own draw can put right. /// Reads its box's size to compute its answer.
struct ReadsBox { struct ReadsBox {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
@@ -252,26 +258,36 @@ struct ReadsBox {
impl Widget for ReadsBox { impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::from_px(painter.px_size().div_int(4)) Size::from_px(painter.answer_px_size().div_int(4))
} }
} }
/// Reads its box across one axis only, so its drawing holds for a taller /// Reads its box across one axis only, so its drawing holds for a taller
/// box on its own and only a wider one is worth a draw. /// box on its own and only a wider one is worth a draw.
/// ///
/// Both of these report a quarter of what they read, without saying that the /// Both report a quarter of what they read. Their empty drawings are
/// drawing holds there too, so each length they are asked at costs two draws: /// independent of that read, so a changed question costs one draw.
/// one to answer, and one in the quarter-sized box that answer places them
/// in. The counts below are in those pairs.
struct ReadsWidth { struct ReadsWidth {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
struct ReadsDrawingWidth {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsDrawingWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.px_len(Axis::X);
Size::LEFTOVER
}
}
impl Widget for ReadsWidth { impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::from_px(PxVec2::new( Size::from_px(PxVec2::new(
painter.px_len(Axis::X).div_int(4), painter.answer_px_len(Axis::X).div_int(4),
Px::from_int(20), Px::from_int(20),
)) ))
} }
@@ -312,6 +328,52 @@ fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0)); assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
} }
/// A row places its children as lengths from where its own box starts, so a
/// child that grew moves the ones after it and nothing else: each of them is
/// the same box in a new place, which the retained drawing follows without
/// being made again. Both kinds of length: one the row resolves from a rule,
/// and one it takes from what the child reported.
#[test]
fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
for declared in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::RED).width(50).add(&mut h.rsc);
let ruled = Rc::new(Cell::new(0));
let second = Counted {
draws: ruled.clone(),
size: Size::LEFTOVER,
reads_box: false,
reads_answer_box: false,
};
let second = match declared {
true => second.width(rel(0.25)).add(&mut h.rsc),
false => second.width(60).add(&mut h.rsc),
};
let (third, reported) = counted(&mut h, Size::from((70, 20)), false);
h.set_root((first, second, third).span(Dir::RIGHT).width(rel(1.0)));
let (was_ruled, was_reported) = (ruled.get(), reported.get());
// A quarter of the row is a quarter of the row, wherever it sits in
// it and whatever the first child takes.
let width = match declared {
true => 100,
false => 60,
};
assert_corners!(h, second, (50, 0), (50 + width, 200));
h.set_len(first, Axis::X, 80);
h.frame();
assert_eq!(ruled.get(), was_ruled, "the ruled child was drawn again");
assert_eq!(
reported.get(),
was_reported,
"the reported child was drawn again"
);
assert_corners!(h, second, (80, 0), (80 + width, 200));
assert_corners!(h, third, (80 + width, 90), (150 + width, 110));
}
}
/// The output is the root of the box chain, so a resize is a box that changed /// The output is the root of the box chain, so a resize is a box that changed
/// length like any other -- there is not a second rule for the window. A /// length like any other -- there is not a second rule for the window. A
/// drawing that holds for one length is drawn again whichever box moved. /// drawing that holds for one length is drawn again whichever box moved.
@@ -343,7 +405,7 @@ fn a_resize_redraws_what_read_its_box() {
h.resize((800, 100)); h.resize((800, 100));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2); assert_eq!(draws.get(), settled + 1);
} }
#[test] #[test]
@@ -363,7 +425,7 @@ fn a_resize_only_redraws_read_axes() {
h.resize((800, 300)); h.resize((800, 300));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2, "width changes its answer"); assert_eq!(draws.get(), settled + 1, "width changes its answer");
} }
/// A window is measured onto the grid like everything else, so a resize too /// A window is measured onto the grid like everything else, so a resize too
@@ -390,7 +452,7 @@ fn a_resize_within_one_step_is_not_a_resize() {
h.resize((400.0 + step, 200.0)); h.resize((400.0 + step, 200.0));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2); assert_eq!(draws.get(), settled + 1);
} }
/// The same for a box that changes because a sibling did: what is compared /// The same for a box that changes because a sibling did: what is compared
@@ -459,7 +521,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// Every wrapper up to the outer pad read the size below it, so the outer // Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same // pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf. // size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true); let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), false);
let padded = leaf.pad(10).add(&mut h.rsc); let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12)); h.set_root((padded, below).span(Dir::DOWN).pad(12));
@@ -613,3 +675,695 @@ fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
assert_ne!(layer(front.id()), layer(background.id())); assert_ne!(layer(front.id()), layer(background.id()));
assert_eq!(draws.get(), 1); assert_eq!(draws.get(), 1);
} }
/// A widget's own mask is not the one it inherited, and a redraw of it
/// inherits the second: handing back the first is handing it its own mask to
/// set a second time, which `set_mask` asserts against.
#[test]
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = inner.masked().add(&mut h.rsc);
let other = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((other, masked).span(Dir::RIGHT));
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_corners!(h, inner, (100, 0), (400, 200));
}
/// The two spans a subtree changes hands between, and the branch that is not
/// in the tree yet -- kept alive by the test until it is.
struct Handover {
leaf: WidgetId,
first: WeakWidget<Span>,
second: WeakWidget<Span>,
root: WeakWidget<Span>,
spare: StrongWidget,
}
/// A subtree that changes hands while its box does not move, so nothing about
/// reusing its drawing says it changed parents. `deeper` puts a span between
/// the root and `second`, so it changes depth by changing hands as well.
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
let leaf = rect(Color::RED).add(&mut h.rsc);
let sized = leaf.width(width).add(&mut h.rsc);
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
let first = Span {
children: match moved {
true => Vec::new(),
false => vec![holder.add_strong(&mut h.rsc)],
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let second = Span {
children: match moved {
true => vec![holder.add_strong(&mut h.rsc)],
false => Vec::new(),
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let branch = match deeper {
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
false => second.add_strong(&mut h.rsc),
};
let (in_tree, spare) = match moved {
true => (branch, first.add_strong(&mut h.rsc)),
false => (first.add_strong(&mut h.rsc), branch),
};
let root = Span {
children: vec![in_tree],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
Handover {
leaf: sized.id(),
first,
second,
root,
spare,
}
}
/// Moves the subtree and swaps the branch it sits in for the one it left.
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
let holder = h.rsc[tree.first].children.remove(0);
h.rsc[tree.second].children.push(holder);
h.rsc[tree.root].children.clear();
h.rsc[tree.root].children.push(tree.spare);
h.frame();
tree.leaf
}
#[test]
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, false, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, false, 40.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it left still listed it and undrew it"
);
}
#[test]
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, true, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
// After it has changed hands, so what has to reach the new parent is a
// change made under the subtree it now holds.
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
warm.frame();
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, true, 90.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it moved to is the one the change has to reach"
);
}
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
let instance = &h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx];
h.render
.moves
.resolve(instance.move_idx, instance.region)
.to_px(h.render.output_size())
})
.collect()
}
#[test]
fn 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_at(
&self.child,
UiRegion::FULL,
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
);
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(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 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, 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,
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(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 = ReadsDrawingWidth {
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, (0, 0), (800, 200));
}
#[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_boxes_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,
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(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)
};
// The same box in three places. A pad places its child as lengths of
// its own box measured from where that box starts, so moving it is
// nothing to the pad -- where changing its length is a different
// question, and does draw it again.
let at = |start: f32| {
let span = |start: Len| UiSpan::new(start, start + Len::rel(0.4));
UiRegion::new(span(Len::rel(start) + Len::px(3.125)), span(Len::px(11.25)))
};
let mut warm = Harness::new((403, 211));
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
for start in [0.13, -0.17, 0.31] {
let extent = at(start);
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.answer_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,
[
Place::Fill(Part::From(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
))),
Place::Fill(Part::From(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_at(
&self.child,
UiRegion::FULL,
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
)
.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,
[
Place::Fill(Part::From(self.extent.x)),
Place::Fill(Part::From(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, _) = match fractional {
true => counted(h, size, false),
false => answer_counted(h, size),
};
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));
}
}
}
}
+64 -1
View File
@@ -60,6 +60,69 @@ fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
assert_corners!(h, top, (0, 0), (400, 200)); assert_corners!(h, top, (0, 0), (400, 200));
} }
#[test]
fn fixed_content_and_a_share_fill_one_viewport() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED)
.width(LayoutLen {
px: Px::from_int(600),
rel: Rel::ZERO,
leftover: Weight::ONE,
})
.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (0, 0), (900, 100));
}
#[test]
fn fixed_content_wider_than_the_viewport_still_scrolls() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED).width(1200).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (-300, 0), (900, 100));
}
#[test]
fn a_lone_share_fills_without_scrolling() {
let mut h = Harness::new((900, 100));
let content = rect(Color::RED).width(LayoutLen::LEFTOVER).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
assert_corners!(h, content, (0, 0), (900, 100));
}
#[test]
fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
fn plant(h: &mut Harness) -> (WidgetId, WidgetId) {
let fixed = rect(Color::RED).width(600).add(&mut h.rsc);
let text = 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)
.width(LayoutLen::LEFTOVER)
.add(&mut h.rsc);
let content = (fixed, text).span(Dir::RIGHT).add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
h.set_root(scroll);
(text.id(), content.id())
}
let mut warm = Harness::new((900, 300));
let (text, content) = plant(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(text);
warm.frame();
let mut cold = Harness::new((900, 300));
let (cold_text, cold_content) = plant(&mut cold);
assert_eq!(warm.region(&text), cold.region(&cold_text));
assert_eq!(warm.region(&content), cold.region(&cold_content));
}
/// A widget that clips to its box may not report more than the box: its /// A widget that clips to its box may not report more than the box: its
/// parent would place the part it cut off, and the framework would put a /// parent would place the part it cut off, and the framework would put a
/// drawing longer than its box somewhere. `Masked` is the second of these /// drawing longer than its box somewhere. `Masked` is the second of these
@@ -71,7 +134,7 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
impl Widget for Clipper { impl Widget for Clipper {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(UiRegion::FULL);
painter.widget(&self.0).size() painter.widget(&self.0).size()
} }
} }
+402 -10
View File
@@ -3,13 +3,190 @@
//! frame that had not settled: a wrapping text shaped at a width it was //! frame that had not settled: a wrapping text shaped at a width it was
//! measured in rather than the one it was given. The rest are a widget //! measured in rather than the one it was given. The rest are a widget
//! measured again in a box its own answer had decided, where the old answer //! measured again in a box its own answer had decided, where the old answer
//! is a fixed point whatever the content now says. The last two are neither: //! is a fixed point whatever the content now says. The last three are
//! one box length, composed two ways, landing either side of the boundary //! neither: one box length, composed two ways, landing either side of the
//! that decided whether a child was drawn at all, and one box as long as the //! boundary that decided whether a child was drawn at all, and two boxes
//! box a widget was offered but somewhere else. //! reached through a region node's own entry rather than through the offer
//! that node was given. The last is a wrapping text handed back the width
//! it measured, rounded to a step below the line it measured there.
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch;
fn assert_same_regions(
warm: &Harness,
warm_ids: &[WidgetId],
cold: &Harness,
cold_ids: &[WidgetId],
) {
let mut wrong = Vec::new();
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Ten widgets, shrunk from seed 2 at depth 5. The stack is as tall as its
/// first child, so its other children belong in that one-line box. A cold
/// layout used to keep the span's answer from the larger measuring box while
/// a repaint asked it in the stack's final box.
fn plant_stack_in_its_sizing_childs_box(h: &mut Harness) -> Vec<WidgetId> {
let sizing = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let filler = rect(Color::CYAN.alpha(252)).add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let span = (filler, plain).span(Dir::DOWN).add(&mut h.rsc);
let pad = Pad {
padding: Padding::ZERO,
inner: span.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let probe = rect(Color::RED).add(&mut h.rsc);
let wide = rect(Color::YELLOW.alpha(252)).add(&mut h.rsc);
let narrow = rect(Color::RED).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: 55.0,
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![
sizing.add_strong(&mut h.rsc),
pad.add_strong(&mut h.rsc),
branch.add_strong(&mut h.rsc),
],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_size_rules(stack.id(), Some(LayoutLen::LEFTOVER), None);
h.set_root(stack);
vec![
sizing.id(),
filler.id(),
plain.id(),
span.id(),
pad.id(),
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
stack.id(),
]
}
#[test]
fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
let mut warm = Harness::new((900, 1200));
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_stack_in_its_sizing_childs_box(&mut cold);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Ten widgets, shrunk from seed 108 at depth 5. The nested reverse spans
/// evaluate the branch in successively narrower boxes. The answer from the
/// final, decided box must be the one retained after every span is reordered.
fn plant_branch_in_nested_reverse_spans(
h: &mut Harness,
reordered: bool,
) -> (Vec<WidgetId>, [WeakWidget<Span>; 3]) {
let pair = |first: StrongWidget, second: StrongWidget| match reordered {
true => vec![second, first],
false => vec![first, second],
};
let probe = rect(Color::RED.alpha(63)).add(&mut h.rsc);
let wide = rect(Color::RED).add(&mut h.rsc);
let narrow = wtext(PARAGRAPH).size(16).wrap(true).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: 483.0,
}
.add(&mut h.rsc);
let wrapped = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let down = Span {
children: pair(
branch.add_strong(&mut h.rsc),
wrapped.add_strong(&mut h.rsc),
),
dir: Dir::DOWN,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let inner_filler = rect(Color::CYAN.alpha(63)).add(&mut h.rsc);
let inner = Span {
children: pair(
down.add_strong(&mut h.rsc),
inner_filler.add_strong(&mut h.rsc),
),
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
let outer_filler = rect(Color::GREEN.alpha(63)).add(&mut h.rsc);
let outer = Span {
children: pair(
inner.add_strong(&mut h.rsc),
outer_filler.add_strong(&mut h.rsc),
),
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
h.set_root(outer);
(
vec![
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
wrapped.id(),
down.id(),
inner_filler.id(),
inner.id(),
outer_filler.id(),
outer.id(),
],
[down, inner, outer],
)
}
#[test]
fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
let mut warm = Harness::new((900, 1200));
let (ids, spans) = plant_branch_in_nested_reverse_spans(&mut warm, false);
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_branch_in_nested_reverse_spans(&mut cold, true);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is /// the tree changes -- every widget is marked for redraw and the frame is
@@ -428,12 +605,12 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()] 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 /// A local redraw asks a dirty widget in the box its parent gave it, and only
/// then again in the box its parent chose from that answer. Skipping the /// where that box is as long as the one it was offered; anything else is a
/// second ask because the two boxes are the same *length* left this inner /// question its parent has to ask. This inner scroll's offer is the outer
/// scroll, which owns a region node, drawn at its offer. The offer is the /// scroll's whole viewport and the box it was given is 24px shorter -- 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 what
/// height of the sized child the outer scroll snaps to the end of -- so the /// 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. /// inner scroll and its text stayed 24px too low.
#[test] #[test]
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
@@ -454,3 +631,218 @@ fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
} }
assert!(wrong.is_empty(), "{}", wrong.join("\n")); 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));
}
+7 -4
View File
@@ -5,11 +5,11 @@
//! cargo test --release --features layout-diagnostics \ //! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture //! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! Uninstrumented hardware totals for one phase: //! Build the uninstrumented test with `cargo test --release --test
//! layout_diagnostics --no-run`, then run the emitted executable directly:
//! //!
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \ //! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
//! -e cycles:u,instructions:u cargo test --release \ //! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
//! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or //! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and //! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
@@ -134,6 +134,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
{ {
let diagnostics = iris::core::layout_diagnostics::take(); let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames)); print!("{}", diagnostics.per_frame(frames));
for event in diagnostics.traces() {
println!(" {event:?}");
}
for callsite in diagnostics.hot_text().iter().take(3) { for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new(); let mut ancestry = Vec::new();
let mut id = Some(callsite.id); let mut id = Some(callsite.id);
+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 INNER: (f32, f32) = (640.0, 900.0);
const STILL: (f32, f32) = (900.0, 1200.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: /// 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 /// 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. /// adding one is not the same as adding three.
@@ -391,20 +384,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label 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 /// 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, /// 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. /// 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() { for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
drawn += got.is_some() as usize; drawn += got.is_some() as usize;
if same_region(got, want) { if got == want {
continue; continue;
} }
// Where two trees disagree is rarely where the cause is, so the // Where two trees disagree is rarely where the cause is, so the