Compare commits

..
Author SHA1 Message Date
iris-ai 58ce74dd7d One argument says where a child goes and what its fractions are of
`Place` was a product written as a sum -- a `Part` and a fill flag -- and
`Part` named three operations the geometry already had, under words that did
not match them. `Of` was `UiSpan::within`, `From` was `UiSpan::shift`, and
`Sized` was `placement`'s own body with the length given rather than
reported. Both enums are gone.

`PlaceDescAxis` says one axis, named after the operation it performs:
`within`, `shifted`, `sized`, and `WHOLE`. What is optional is a builder --
`fills` and `rel_base` -- so a caller writes only what it decided, and the
rel base it does not write follows the constructor: a span composed into the
caller's box narrows it, a span along a cursor does not, a decided length is
it. That was the one rule a caller could get wrong with nothing failing.

`PlaceDesc` says both axes with named fields, so `axis`, `axis_mut` and
`from_axis` work the way they do on every other pair here, and the joint
work -- resolving a region, reading the fill flags -- is written once rather
than per axis. `widget_at` and `place_at` take `impl Into<PlaceDesc>`, so a
wrapper passes a `UiRegion` and says nothing else. `widget_within` and
`ActiveData::narrow_rel_base` are deleted; `asked` and `placed` carry the
rel base their ask stated.

Cold layout is byte-identical to `84dad21`.
2026-09-19 17:59:26 -04:00
iris-ai c55be21761 Put place before the rel base, and say what a Place decides
An argument that is usually `None` goes last. So `widget_at` and `place_at`
take the place first, and `narrow_rel_base` after it.

`Place` and `Part` also now say which of the two boxes they decide, since
that is the question a caller has to answer to pick between them: `Part` is
the child's region, said as a part of the caller's own, and `Within`/`Fill`
is what becomes of the placement in it.
2026-09-19 17:16:41 -04:00
iris-ai beb138632a Say narrow_rel_base, and let a container pass None for it
`narrow` said what the argument did to a value it never named, so a reader
had to go and find out which value. It is `narrow_rel_base`, and the
resolved one stays the bare `rel_base` -- which is also the only one in
`Painter`, `Placing` and `LayoutHolds`, where there is nothing to tell it
apart from.

It takes `impl Into<Option<[Option<Len>; 2]>>`, so a container that does not
narrow anything writes `None` rather than `[None; 2]`, and `Span` builds the
one case that does with a `then` instead of a mutable array.

Cold layout is byte-identical to `84dad21`.
2026-09-19 16:55:48 -04:00
iris-ai aeb60e50f5 Say rel base, and give containers back a box to hand over
`frame` named a length, not a rectangle, which was the one word in the
layout vocabulary that lied about its own shape. It is `rel_base`: what a
fraction a widget declares or reports is a fraction of.

Three API changes with it, all for containers that do one simple thing:

- `widget_within(id, region)` returns, taking a box in the widget's own
  coordinates and deriving the child's rel base from it. `Offset` and `Pad`
  are one call each again. `Offset` also stops reading `region_len`, which
  pinned its drawing to a box length it does not care about.
- `place_at` takes the rel base, returns the answer, and asks the child
  where there is no answer to re-express. Which of the two happens is the
  painter's to work out, so `Span`'s second pass is one call and its
  `drawn_across` bookkeeping is gone.
- `Part::All` is a `Part::WHOLE` constant rather than a variant, since it
  was exactly `Of(UiSpan::FULL)` and bought a separate arm in two matches.
  Measured at 0.07% of instructions retired against 0.04% run-to-run noise.

Cold layout is byte-identical to `84dad21` over 400 depth-5 trees.
2026-09-19 16:33:49 -04:00
iris-ai a904cf4f36 TODO: transforms on a move entry, for stretch and rotation 2026-09-19 15:07:51 -04:00
iris-ai 5642f2010a Say region and placement, not extent
The split box was named `region` and `placement` on 2026-09-17; `frame`
came back as a length and survived, `extent` did not. It stayed as the
name for both halves, distinguished only by prose: `draw_at` bound the
caller's `part` to a parameter called `extent`, and `ActiveData` held two
`UiRegion`s that `draw_at` wrote `part: extent` from.

The box a parent asks a widget in is now the region, and where its
drawing ends up is its placement. `Painter`'s four holds accumulators
become the one `LayoutHolds` they were assembled into, which also drops
the name mapping between them.

The cold dump of 400 depth-5 trees is byte-identical across the change.
2026-09-19 14:49:56 -04:00
iris-ai 84dad211f5 Avoid repeated plan generation and unused diagnostics in layout fuzzers 2026-09-19 13:43:11 -04:00
iris-ai add6774980 Keep retained masks and reparented drawings alive, and advance collapsed slots 2026-09-19 13:43:11 -04:00
iris-ai cadfba05dd Keep only what a room drawing is still needed for
A span measuring a child in the room kept its whole `Size`, of which the
along axis is already in `lens` and only the across one is read again when
the drawing is placed. Keep that length alone, which also retires the
rebinding of the match's result and the one in the placing loop. The
placement comment already says what becomes of a drawing made in the room,
so the measuring pass no longer says it a second time.
2026-09-19 02:51:08 -04:00
iris-ai 38b3a81053 Pin a frame by the fraction the child declared
`size_hint` resolves a child's hint against the asking widget's frame and
pins that frame, so a later draw cannot reuse a resolution made against a
different one. It asked the *resolved* hint whether it still had a fraction,
which is false whenever the frame is itself pixels -- a slot of a row, or the
box a stack's sizing child decided -- and the pin was dropped there. Ask the
declared hint, which is what made this draw depend on the frame, and what
`ruled` in `render_state` already asks for a rule.

No generated tree distinguishes the two: the fuzzer grows no `rel` rules, and
a frame that changes almost always changes a box the other pins catch. Kept
for the reason the `frame_len` pin beside it is kept -- "these two
invalidations always coincide" is an assumption nothing states.
2026-09-19 02:51:08 -04:00
iris-ai f6242aa33c Take a span child's length from its hint, and ask it once in its slot
A share child was drawn in the measuring room and again in its slot, and
one record holding two questions made every local change under it defer
to the span. Where a rule or a hint gives the length along the span, the
first ask answers nothing the rule does not, so the child is asked once,
in its slot; the widgets that always report the whole of their box now
say so. A hint with a fraction resolves against the frame and pins it.

The dump rig prints every cold layout so a change to it shows in a diff.
2026-09-19 02:09:41 -04:00
iris-ai a888717ee9 Say window where these comments still say frame
Lengths became lengths of the window when the frame did, and `Part::From`'s
own documentation still described its spans as frame lengths -- which is
what the scroll above read them as.
2026-09-19 01:23:34 -04:00
iris-ai e8a5792dcb Place a scroll's fitting content in the viewport, not in the window
A scroll that has not been scrolled and whose content fits asked for its
content box as `Part::From(UiSpan::FULL)`. A `Part::From` span is in window
lengths, so `rel(1.0)` in one is the whole window rather than the whole box,
and the content landed in a window-tall box anchored at the viewport's
start -- 50 px low for a 300 px viewport in a 400 px window.

Saying the whole of the box as `Part::All` is the one expression that cannot
mean anything else, and it is also the place the child was already asked in,
so the placement becomes a no-op.
2026-09-19 01:23:34 -04:00
iris-aiandClaude Opus 5 a30971e4c5 Call the record's boxes what they are
The offer names are from the protocol before this one, where a widget was
drawn twice and the record had to say which drawing was the question. It
is asked once now, so offer_part is the part it was asked in, offer_place
the place it was asked at, and place where its drawing was put: part,
asked and placed. LayoutHolds::frame is a range on the window since the
frame became a length of one, and the frame's own entry is the frame_len
pin beside it, so it is window; Painter::frame_own goes with it.
answers_at had one caller and said less than the line that replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:40:23 -04:00
iris-aiandClaude Opus 5 adbedaf264 Say what the fuzzer's branching widget branched on
Branch reads a measurement in pixels and draws a different subtree either
side of a threshold, and it left that read as a pin on the window, so
every one of them redrew on every resize: at depth 8 that was seed 1's
resize going from 40 widget draws to 131 and seed 13's from nothing to
828. It now states the range it actually branched on, the way Span states
the one that decides whether its shares have room. A fixture that redraws
everything on a resize cannot tell a change that reuses well from one
that does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:24:10 -04:00
iris-aiandClaude Opus 5 23523eea29 Take a window read where a length is resolved against it
A widget that resolves a window length in pixels depends on that window
wherever the length is a fraction of it, and nothing was recording that:
Painter::to_px replaces window_px_len and pins the window it read, while
a length that is only pixels is that many pixels in any window and pins
nothing. Span still states the range it actually branched on, which
replaces the pin with something wider.

Scroll is where it showed: its content's answer is a window length now,
so a viewport whose own box does not change with the window -- 40 px of
a branch's box -- kept an end-snapped offset from the window before.
Seed 942 at depth 6 under resize, pinned as
unsettled::resizing_under_a_short_scroll_snaps_its_window_tall_content_again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:21:11 -04:00
iris-aiandClaude Opus 5 1512d8418b Make the frame a length of the window and the box a region
There is one coordinate unit, the window. Every box in the tree is a
region in window units and a widget's frame is a length in the same
units, which is only what fractions resolve against, so the box need not
be the frame and padding can take from both without either becoming the
other. A region node's entry is a translation -- a rel 1 region anchored
where its box starts -- rather than a box, so nothing composes a frame
back up a chain and a node that moves is one entry write.

Padding is then an inset of both: its pixels come off the frame, so
rel(1.0) under it fills the padded widget rather than overflowing it,
and off the box, so what is drawn sits inside. A length a container
decides for a child's frame is a length of the window like everything
else here -- a row's slot, padding's frame less its pixels, or the box a
stack's sizing child decided, which arrives as Part::Sized -- because a
slot of a row is not a fraction of anything the row can name, the same
reason a node entry is a translation. A declaration is a fraction of
whichever of those reached it, and is the only one that also places the
box.

Frame validity is a pin beside the box's, not a range: a range of window
pixels cannot say which frame an answer is a fraction of, since two
frames are different lengths at the same window size. A widget pins its
frame by reading it or by being answered with it under a fractional
rule, and the pin composes up wherever a length of this frame is what
reached the child.

Also here, because the diagnosis needed them: the shrinker reports the
shrunk tree's own divergence with each level's frame, ask, box and size
warm against cold, and there is a size-resize case -- a change and then a
resize, the order that shows an answer kept as a fraction of the wrong
length, which every other case compares at the window it was made at.

Three defects the reports found, each pinned: a rule changed over two
pads relocated the column under them instead of dividing it again (seed
59, depth 5, resize-size), a share inside padding had the padding taken
off twice, and a root resolved its own rule twice.

fmt and clippy clean with and without layout-diagnostics, 121 suite, 20
core, 11 generated, the 400-seed depth-5 shrinker over all sixteen cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:14:46 -04:00
iris-aiandClaude Fable 5.1 0ef87ebfcf Defer a twice-asked widget's local redraw to its parent
A span asks a share child twice in one draw: in the room, whose answer
its slots rest on, and in the decided slot, whose cross-axis answer it
reads. The record keeps only the second question, so a local redraw that
found that answer unchanged never told the row that the first had --
seed 946 at depth 6, where emptying a fixed-height column turns it from
a share into a fixed width as wide as the row. A widget its parent asked
more than once in one draw now defers to that parent, like one whose
declared length changed. Pinned as
unsettled::emptying_a_column_the_row_asked_twice_asks_the_row_again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:29:58 -04:00
iris-aiandClaude Fable 5.1 3091fb86df Ask each child once and place its answer by re-expression
A widget draws in the box it is asked in and its answer is placed inside
that box by re-expressing the drawing; nothing is drawn again in a box an
answer chose. The offer machinery, whose job was to tell a measuring draw
from a placing one, goes with the placing draw. A span measures each child
from its cursor and moves fixed children to their slots with place_at; a
share child is asked once more in its decided slot with its frame narrowed
to it. A stack asks non-sizing children in the box its sizing child
decided. A scroll asks its content once and moves it to the scrolled
offset. A local redraw asks the retained question again and puts the
answer back where the parent placed it.

A symbolic length a child pinned composes through Part::Of exactly where
the part is the whole box less pixels, and pins the parent's own length
otherwise; dropping it let a pad reuse a drawing across a narrowed frame
of the same pixel length (shrinker seeds 60, 248 and 384 at depth 5).

Suite 114/114 including the two decided-box pins, fast oracle 11/11,
shrinker 400 seeds at depth 5 over all fifteen cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:20:57 -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
34 changed files with 3258 additions and 1066 deletions

No files matched your search

+10
View File
@@ -14,3 +14,13 @@ WidgetRef<W> or smth instead of Id
vecs for each widget type? vecs for each widget type?
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..?? POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
transforms on a move entry (scale + rotation)
an entry is a translation today; composing through one scales the rel
part and passes px through untouched, so fixed-size content and glyphs
do not follow a shortened entry
want a real transform per entry, resolved in resolve_move the way the
translation already is, so a whole subtree transforms with one buffer
write and no redraw
wanted for compose-style stretch at the end of a scroll area, and for
rotation generally
+7 -5
View File
@@ -32,7 +32,6 @@ pub(crate) enum Counter {
SizeReads, SizeReads,
HintHits, HintHits,
HintMisses, HintMisses,
RetainedSizeHits,
ReuseAttempts, ReuseAttempts,
ReuseExact, ReuseExact,
ReuseMoved, ReuseMoved,
@@ -42,7 +41,6 @@ pub(crate) enum Counter {
ReuseOutside, ReuseOutside,
ReuseWrongLayer, ReuseWrongLayer,
ReuseWrongNode, ReuseWrongNode,
PlaceRedraws,
QueuePops, QueuePops,
DepthReads, DepthReads,
LocalRedraws, LocalRedraws,
@@ -54,10 +52,13 @@ pub(crate) enum Counter {
TextShapes, TextShapes,
TextBreaks, TextBreaks,
GlyphPlacements, GlyphPlacements,
OutsidePinnedLen,
OutsideRelBase,
OutsideRegion,
} }
impl Counter { impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1; const COUNT: usize = Self::OutsideRegion as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"updates", "updates",
@@ -67,7 +68,6 @@ impl Counter {
"draw-result size reads", "draw-result size reads",
"hint hits", "hint hits",
"hint misses", "hint misses",
"retained size hits",
"reuse attempts", "reuse attempts",
"reuse exact", "reuse exact",
"reuse moved", "reuse moved",
@@ -77,7 +77,6 @@ impl Counter {
"reuse: outside what it holds for", "reuse: outside what it holds for",
"reuse: another layer", "reuse: another layer",
"reuse: region-node choice changed", "reuse: region-node choice changed",
"placed by redrawing",
"redraw queue pops", "redraw queue pops",
"depth reads", "depth reads",
"local redraws", "local redraws",
@@ -89,6 +88,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 rel base",
"reuse outside: a region length",
]; ];
} }
+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;
+17
View File
@@ -176,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())
} }
+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;
+42 -32
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
UiRegion, UiVec2, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,29 +9,35 @@ 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. /// Where its drawing goes, in its region node's coordinates.
pub placement: UiRegion,
/// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window.
pub rel_base: UiVec2,
/// Where its drawing was put, and where it was asked. The two differ
/// where a container asks in one place and puts the answer in another --
/// a row measures from its cursor and puts the child in its slot. Each
/// carries the rel base that ask stated, so asking again from either is
/// the same question it was.
pub placed: PlaceDesc,
pub asked: PlaceDesc,
/// The box it was asked in, in the parent's region-node coordinates: the
/// box its drawing was made in and the one its contract is about. Its
/// drawing is placed elsewhere by re-expression, never by asking again.
pub region: UiRegion, pub region: UiRegion,
/// The box its parent gave it, in the same coordinates: what it was /// The measured answer and its dependencies. A hint-only dependency or
/// asked about, before its own answer placed its drawing inside it. /// a widget first encountered during placement has no measurement yet.
/// `region` is that placement, and a local redraw asks here. pub answer: Option<(Size, LayoutHolds)>,
pub given: UiRegion, /// Asked more than once in its parent's last draw -- measured in one box
/// The same box as lengths of its parent's box, which is the one route /// and then asked in the one the parent decided. The parent's layout
/// to a box in pixels: a draw threads these down a level at a time, and /// rests on the first answer and its drawing on the last, so only the
/// [`crate::UiRenderState::redraw`] takes the same steps back up. /// parent can ask either again.
pub given_len: UiVec2, pub re_asked: bool,
/// The lengths of the box its parent first asked about it in, as /// What the widget reported, in window-unit lengths.
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
/// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it.
pub offer_len: UiVec2,
/// What it answered there: the size and what that held for.
pub answer: (Size, [Holds; 2]),
/// What the widget said it used of its box, the last time it drew.
pub size: Size, pub size: Size,
/// The pixel lengths of `region`, per axis, that its drawing and `size` /// The window and region reads that this drawing holds for, and the
/// hold for. /// rel base and region it pinned.
pub holds: [Holds; 2], pub holds: LayoutHolds,
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
@@ -39,24 +45,26 @@ 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 placement coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>,
/// An owned mask holds one reference independently of its primitives.
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 rel base.
/// 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 axes along which its parent chose its box from its own answer,
/// so a local redraw asks the question its parent asked.
pub decided: [bool; 2],
/// Its alignment when it was last drawn, which a change to the property /// Its alignment when it was last drawn, which a change to the property
/// is found against. /// is found against.
pub own_align: RegionAlign, pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses. /// The movable region whose coordinates its placement is in when this
/// widget does not own a region node.
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it /// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it. /// inherited from whoever drew it.
@@ -70,8 +78,10 @@ pub struct ActiveData {
} }
impl ActiveData { impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths. /// What it answered when its parent asked, where it has been asked at
pub fn holds_at(&self, px: crate::PxVec2) -> bool { /// all. Not `size`, which is what its last drawing reported: a drawing
self.holds[0].contains(px.x) && self.holds[1].contains(px.y) /// re-expressed in the box that answer chose is not a second answer.
pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size)
} }
} }
+13
View File
@@ -52,6 +52,9 @@ impl Holds {
/// allowance: inverting it is two divisions and nothing else, and the /// allowance: inverting it is two divisions and nothing else, and the
/// whole of a box maps back to itself. /// whole of a box maps back to itself.
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;
@@ -92,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.
+79
View File
@@ -0,0 +1,79 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on: the window lengths its reads
/// hold for, the pixel lengths of its own box, and the symbolic lengths of
/// that box and of its rel base where either one is what it was expressed in.
///
/// The symbolic lengths are pins rather than ranges: a container places its
/// children as lengths of its rel base measured from where its own box starts,
/// so what it draws turns on that box's length and on nothing about where it
/// is. A box pin 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.
///
/// A rel base pin says the answer or the drawing is a fraction of the rel base,
/// which is a different length wherever the rel base is a different one -- at
/// the same window size, so no range of window pixels can say it. A length
/// of the rel base that is only pixels is not one: it is that many pixels
/// whatever the rel base turns out to be.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub window: [Holds; 2],
pub rel_base: [Option<Len>; 2],
pub region: [Holds; 2],
pub region_len: [Option<Len>; 2],
}
impl LayoutHolds {
pub const ANY: Self = Self {
window: [Holds::ANY; 2],
rel_base: [None; 2],
region: [Holds::ANY; 2],
region_len: [None; 2],
};
pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY;
for n in 0..2 {
result.window[n] = self.window[n].and(other.window[n]);
result.region[n] = self.region[n].and(other.region[n]);
debug_assert!(
self.region_len[n].is_none()
|| other.region_len[n].is_none()
|| self.region_len[n] == other.region_len[n]
);
debug_assert!(
self.rel_base[n].is_none()
|| other.rel_base[n].is_none()
|| self.rel_base[n] == other.rel_base[n]
);
result.region_len[n] = self.region_len[n].or(other.region_len[n]);
result.rel_base[n] = self.rel_base[n].or(other.rel_base[n]);
}
result
}
pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| {
self.window[n].lo <= other.window[n].lo
&& self.window[n].hi >= other.window[n].hi
&& self.region[n].lo <= other.region[n].lo
&& self.region[n].hi >= other.region[n].hi
&& self.region_len[n].is_none_or(|len| other.region_len[n] == Some(len))
&& self.rel_base[n].is_none_or(|len| other.rel_base[n] == Some(len))
})
}
pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
AXES.into_iter().all(|axis| {
let n = axis as usize;
let len = region.axis(axis).len();
self.window[n].contains(window.axis(axis))
&& self.rel_base[n].is_none_or(|pinned| pinned == rel_base.axis(axis))
&& self.region[n].contains(len.to_px(window.axis(axis)))
&& self.region_len[n].is_none_or(|pinned| pinned == len)
})
}
}
+4
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)]
+409 -296
View File
@@ -1,14 +1,14 @@
#[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, RenderedText, Size, StrongWidget, Axis, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
WidgetId, Widgets, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::render_state::{DrawInfo, Placing},
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y]; const AXES: [Axis; 2] = [Axis::X, Axis::Y];
@@ -17,34 +17,37 @@ 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`. /// This widget's rel base, per axis: a length of the window, and what a
/// fraction it or anything under it declares or reports is a fraction
/// of. A length rather than a box, so padding can take from both the
/// rel base and the box without either becoming the other.
pub(super) rel_base: UiVec2,
/// The box this widget was asked in, in its region node's coordinates:
/// what it draws in, and what its children's places are parts of.
pub(super) region: UiRegion, pub(super) region: UiRegion,
/// That box in pixels, which its children's are a length of: threaded /// The window in pixels. Frames and boxes become pixels against this one
/// down from the box this widget was given rather than composed back up /// unit, regardless of region-node boundaries.
/// the chain, so every length in layout is one multiply from its pub(super) window: PxVec2,
/// 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>,
/// The previous drawing's owned mask, available for this draw to reclaim.
pub(super) mask_slot: Option<MaskIdx>,
/// 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
/// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>,
/// The lengths of the box this widget was first asked about in, in
/// pixels. Its children's offers are a fraction of it.
pub(super) offered_px: PxVec2,
/// Whether this draw is in a box of those lengths, which makes the
/// questions it asks the ones a cold layout asks and their answers the
/// ones to keep.
pub(super) at_offer: bool,
/// The children whose size this widget read while drawing. /// 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 reads, as against what its children's drawings
/// length until it reads one, then that one, unless it says otherwise. /// hold for: every window and every length of its own region until it
pub(super) own: [Holds; 2], /// reads one, then that one unless it says otherwise, and the rel base or
/// What the children it asked about and drew keep it to. /// region length it read symbolically, each of which makes the drawing
pub(super) under: [Holds; 2], /// hold for that length alone.
pub(super) own: LayoutHolds,
/// What each child's drawing depends on. Asking a child again replaces
/// its drawing, so it replaces this too rather than narrowing it.
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,
@@ -64,6 +67,22 @@ 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 region, composed into its region node's
/// coordinates.
fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.region)
}
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(
@@ -72,152 +91,115 @@ 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:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
} }
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);
self.mask = self.rsc.ui_mut().masks.push(Mask { let resolved = self.resolve(region);
region, let move_idx = self.move_idx;
move_idx: self.move_idx, let mask = Mask {
}); region: resolved,
move_idx,
};
let masks = &mut self.rsc.ui_mut().masks;
self.mask = match self.mask_slot.take() {
Some(idx) => {
*masks.get_mut(idx) = mask;
idx
}
None => {
let idx = masks.push(mask);
// The owner keeps the slot alive even with no primitives.
masks.push_ref(idx);
idx
}
};
} }
/// Draws a widget within this widget's region. /// Draws a widget in the whole of this widget's own box, with the rel
/// base forwarded unchanged: what a container that is only a wrapper
/// around one child wants.
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)
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// Resolves what the place says about the child's rel base into a length,
/// it resolves into its box. Reading them depends on nothing -- the box /// where that is this widget's own narrowed the way the region is. An
/// that comes of them is kept on the child, and `redraw` compares it /// axis the region leaves whole is not read at all, so a wrapper that
/// there. /// only moves its child does not pin its drawing to a rel base.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] { fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
declared_lens(self.rsc.widgets(), id.id()) for axis in AXES {
if let Some(span) = place.axis(axis).narrows_rel_base() {
let len = span.len();
let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis)));
*place.axis_mut(axis) = place.axis(axis).with_rel_base(stated);
}
}
place
} }
/// Takes back a child that was drawn only to find out how long it is. /// Asks a child, saying what its fractions are of and where it is asked.
/// 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.state.undraw_rec(id.id(), self.rsc);
}
/// Draws a widget somewhere within this one. `region` is in this widget's
/// own coordinates, and the child's declared lengths are still to be
/// taken from it. Where the child's drawing sits inside what it is given
/// is the child's alignment, applied where the child is drawn, so a
/// container positions a child either by handing it a box of exactly its
/// length or by leaving it room and letting its alignment decide.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, region.size(), [false; 2])
}
/// Draws a widget in `region`, saying what the answer means.
/// ///
/// `reports_of` is what a fraction the child reports is a fraction of, as /// `place` says where the child goes and what its fractions are of:
/// lengths of this widget's own box. It is the box the child was given /// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which
/// wherever that box is the child's whole area -- a pad's inset, a stack /// is a box of this widget's own with the answer placed inside it.
/// child, a scroll's content -- and a span passes its own extent along
/// the row instead: it offers each child the room left from its cursor,
/// because a text has to wrap at the width actually there, while
/// `rel(0.5)` still means half the span wherever the child sits in it.
/// ///
/// A `decided` axis is one where this box was chosen from the widget's /// The child draws once, in the region that comes of it, and its answer
/// own answer. On those the answer is not placed inside the box again: it /// is placed inside that region by re-expressing the drawing. Nothing is
/// already is the box, and a fraction taken of it a second time would /// drawn again in a box an answer chose; a container that puts the
/// shrink it twice. A container uses that where it hands back exactly /// answer somewhere else says so with [`Self::place_at`].
/// what a child asked for -- a span placing a child at the length it
/// reported, a scroll giving its content the content's own length.
pub fn widget_at<'s, W: ?Sized>( pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, place: impl Into<PlaceDesc>,
reports_of: UiVec2,
decided: [bool; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
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 = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
// A rule this box was already chosen from is not resolved into it a let (rel_base, region) =
// second time. The box is that rule's length already, so resolving rel_base_and_region(self.region, self.rel_base, place, declared, align);
// it again takes the fraction twice -- a widget declaring half of a
// stack, in the stack its own answer made half a row, is a quarter
// of the row. Pixels survive it, being the same length wherever they
// are taken from, which is why only a share ever shrank.
let resolve = AXES.map(|axis| match decided[axis as usize] {
true => None,
false => declared[axis as usize],
});
// Composing `FULL` through a box is not quite the identity in f32,
// so a child with nothing declared keeps the box it would have had.
let local = match resolve.iter().any(Option::is_some) {
true => declared_box(region, resolve, align),
false => region,
};
let within = match local == UiRegion::FULL {
true => self.region,
false => local.within(&self.region),
};
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
if region_node { if region_node {
diag::bump(Counter::RegionNodeDraws); diag::bump(Counter::RegionNodeDraws);
diag::region_node(id.id(), self.id, within); diag::region_node(id.id(), self.id, region);
} }
// A child listed twice would be moved twice. // A child listed twice would be moved twice.
if !self.children.contains(&id.id()) { let re_asked = self.children.contains(&id.id());
if !re_asked {
self.children.push(id.id()); self.children.push(id.id());
} }
let first_ask = self.offer(id.id()); let px = rel_base.to_px(self.window);
let given_len = local.size(); let (size, answer_holds, holds) = self.state.draw_inner(
let offer_len = match first_ask {
true => given_len,
false => self
.state
.active
.get(&id.id())
.map_or(given_len, |a| a.offer_len),
};
let px = given_len.to_px(self.px);
let offered_px = offer_len.to_px(self.offered_px);
// Whether this ask is the child's offer question, which is a question
// about lengths: the same lengths somewhere else is the same question.
let answers_offer = self.at_offer && px == offered_px;
// The answer and what it holds for, both about the box asked in. The
// child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box.
let (size, holds) = self.state.draw_inner(
id.id(), id.id(),
within,
DrawInfo { DrawInfo {
layer: self.layer, layer: self.layer,
parent: Some(self.id), parent: Some(self.id),
@@ -225,32 +207,99 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
given_len, rel_base,
offer_len, region,
placed: place,
asked: place,
re_asked,
px, px,
offered_px,
decided,
}, },
None, None,
self.rsc, self.rsc,
); );
if answers_offer { let holds = self.in_parent(holds, region, place, declared);
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds); let answer_holds = self.in_parent(answer_holds, region, place, declared);
} 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: in_parent_frame(size, reports_of, declared), size,
answer_holds,
} }
} }
/// What a child says its length is without being drawn, if it can say. /// Takes back a child that was drawn only to find out how long it is.
/// Asking counts as reading its size. /// 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);
}
/// Puts a child in `place` of this widget's box, where that box is the
/// answer the child already gave: the drawing is re-expressed there
/// rather than made again -- what a row does once it knows every slot,
/// having measured each child from its cursor.
///
/// A child this draw has not asked about, and one whose rel base this
/// narrows, is asked here instead: there is no answer to re-express, or
/// the question has changed. So a container that places every child the
/// same way says it once, and which of the two happens is this widget's
/// business rather than the caller's.
pub fn place_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> {
let place = self.state_rel_base(place.into());
let states_rel_base = AXES
.iter()
.any(|&axis| place.axis(axis).stated_rel_base().is_some());
if states_rel_base || !self.children.contains(&id.id()) {
return self.widget_at(id, place);
}
let at = self.placing();
self.state.place_in(id.id(), &at, place, self.rsc);
let active = &self.state.active[&id.id()];
let size = active.measured().unwrap_or(active.size);
DrawResult {
child: id,
painter: self,
size,
// Read where it was asked; moving it is not a second answer.
answer_holds: LayoutHolds::ANY,
}
}
/// This widget as the thing its children are placed within.
fn placing(&self) -> Placing {
Placing {
id: self.id,
region: self.region,
rel_base: self.rel_base,
window: self.window,
depth: self.depth,
move_idx: self.move_idx,
mask: self.mask,
}
}
/// What a widget's rules declare its lengths to be, which whoever draws
/// it resolves into its rel base. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it
/// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
declared_lens(self.rsc.widgets(), id.id())
}
/// What a child says its length is without being drawn, if it can say,
/// as the length its draw would report: a fraction in it is resolved
/// against this widget's rel base, which is the rel base a child asked with
/// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let widgets = self.rsc.widgets(); let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the // A rule is the answer where there is one: it wins over whatever the
@@ -260,70 +309,27 @@ impl<'a> Painter<'a> {
.get_dyn(id.id()) .get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis)) .and_then(|widget| widget.size_hint(axis))
}); });
let rel_base = self.rel_base.axis(axis);
let resolved = hint.map(|hint| hint.within_len(rel_base));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint); {
match hint { diag::hint_read(id.id(), self.id, axis, resolved);
Some(hint) => { diag::bump(match resolved {
#[cfg(feature = "layout-diagnostics")] Some(_) => Counter::HintHits,
diag::bump(Counter::HintHits); None => Counter::HintMisses,
self.depend_on(id); });
Some(hint) }
} if let Some(hint) = hint {
None => { self.depend_on(id);
#[cfg(feature = "layout-diagnostics")] // Resolving a fraction against this rel base makes this draw a
diag::bump(Counter::HintMisses); // function of the rel base's length. The fraction to ask about is
None // the child's own: resolved against a rel base of pixels, none is
// left to see it by.
if hint.rel != Rel::ZERO {
self.own.rel_base[axis as usize] = Some(rel_base);
} }
} }
} resolved
/// 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. `reports_of` is what a fraction in the
/// answer is a fraction of, as it is for [`Self::widget_at`].
pub fn known_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
reports_of: UiVec2,
) -> Option<LayoutLen> {
let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align);
let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer_len = local.size();
}
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
}
let px = local.size().to_px(self.px);
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(in_parent_frame(size, reports_of, declared).axis(axis))
}
/// 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
/// layout asks, whose answer is the one to keep.
fn offer(&mut self, child: WidgetId) -> bool {
if !self.at_offer || self.offered.contains(&child) {
return false;
}
self.offered.push(child);
true
} }
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
@@ -344,21 +350,27 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// Writes glyphs in the selected rel base or region 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 region.
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));
let size = PxVec2::new( let size = PxVec2::new(
Px::from_int(glyph.entry.width as i32), Px::from_int(glyph.entry.width as i32),
Px::from_int(glyph.entry.height as i32), Px::from_int(glyph.entry.height as i32),
); );
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,
@@ -367,15 +379,33 @@ 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 rel base 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 region_len(&mut self, axis: Axis) -> Len {
let len = self.region.axis(axis).len();
self.own.region_len[axis as usize] = Some(len);
len
}
/// This widget's rel base along one axis: what a fraction it or anything
/// under it declares or reports is a fraction of. A container reads it
/// to hand a length of it down -- padding, which takes its pixels off.
/// Reading it pins the drawing to that rel base, the way
/// [`Self::region_len`] pins it to the box.
pub fn rel_base(&mut self, axis: Axis) -> Len {
let len = self.rel_base.axis(axis);
self.own.rel_base[axis as usize] = Some(len);
len
} }
/// 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
@@ -403,41 +433,68 @@ impl<'a> Painter<'a> {
.is_some() .is_some()
} }
/// This widget's box in pixels. Reading it makes the drawing one that /// This widget's own box in pixels. Reading it makes the drawing one
/// holds for this box only, until `holds` says how far it goes. /// that holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 { pub fn px_size(&mut self) -> PxVec2 {
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) { PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
if *own == Holds::ANY {
*own = Holds::at(len);
}
}
self.px
} }
/// One axis of this widget's box in pixels. Prefer this to /// 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.px.axis(axis); let len = self.region.axis(axis).len();
let own = &mut self.own[axis as usize]; let px = len.to_px(self.window.axis(axis));
let own = &mut self.own.region[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(px);
} }
len px
} }
/// The lengths of this widget's box on `axis` that what it is drawing /// 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 len = self.region.axis(axis).len();
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.px.axis(axis)), holds.contains(len.to_px(self.window.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.own.region[axis as usize] = holds;
}
/// A window length in pixels, which is what every length in layout is
/// measured in. Reading one pins the drawing to this window wherever the
/// length is a fraction of it; one that is only pixels is that many
/// pixels in any window and pins nothing.
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let window = self.window.axis(axis);
if len.rel != Rel::ZERO {
let own = &mut self.own.window[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(window);
}
}
len.to_px(window)
}
/// The windows this drawing holds for, stated rather than taken: a
/// container that branched on a length in pixels says which side of the
/// boundary it was on, which is wider than the one window reading that
/// length pins, and replaces it.
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.window.axis(axis)),
"'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(),
self.id
);
self.own.window[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -480,6 +537,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> {
@@ -490,6 +548,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
} }
@@ -523,22 +582,67 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// A child's answer as lengths of the parent's own box. A widget reports a /// Moves what a child depends on into this widget's own terms: this
/// fraction, and `reports_of` is the length that fraction is of: the box the /// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// child was given wherever that is the child's whole area, and the parent's /// only what the child was asked with.
/// own extent wherever the box is a positional remainder, as a span's is impl Painter<'_> {
/// after an earlier child. Pixels come through untouched either way, being /// Window ranges are already about the one unit and combine directly.
/// that many pixels wherever they end up. A declared axis is already the /// A rel base pin becomes this widget's own rel base wherever a length of it
/// parent's: it resolved the rule in its own box, and the rule is what the /// is what reached the child; where only pixels did, no length of this
/// report says. /// rel base can change the child's and the pin stops here.
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size { ///
let mut size = size; /// A child's validity maps back through the part of this widget's box,
for (axis, declared) in AXES.into_iter().zip(declared) { /// where the box the child was asked in is that part; a declared length
if declared.is_none() { /// places the box inside the part instead, and then only that length
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis)); /// reaches the child. A narrowed rel base is not one of these: it decides
/// what fractions under the child mean and leaves the box the part it
/// was given.
fn in_parent(
&self,
holds: LayoutHolds,
region: UiRegion,
place: PlaceDesc,
declared: [Option<LayoutLen>; 2],
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
// Every read became pixels against the window, so a range on
// it is already in this widget's terms.
result.window[n] = holds.window[n];
let at = *place.axis(axis);
let reaches = at.stated_rel_base().is_none()
&& !at.is_sized()
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO);
result.rel_base[n] = holds.rel_base[n].and(reaches.then(|| self.rel_base.axis(axis)));
match (at.within_span(), declared[n].is_some()) {
// 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. A length it pinned
// is this widget's length less the part's pixels where the
// part is the whole of the box less pixels, which is the one
// shape that inverts exactly; any other part pins this
// widget's own length.
(Some(span), false) => {
let part_len = span.len();
result.region[n] = holds.region[n].through(part_len);
result.region_len[n] = holds.region_len[n].map(|pinned| match part_len.rel {
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
_ => self.region.axis(axis).len(),
});
}
// Its box is a length this widget decided, from its own
// rel base or from a sibling's answer: no length of this
// widget's box reaches it, so what it holds for is a range
// on the window and none of it on that box.
_ => {
result.window[n] =
result.window[n].and(holds.region[n].through(region.axis(axis).len()));
}
}
} }
result
} }
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
@@ -564,71 +668,80 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
/// Whether what a widget reported along an axis is the whole of the box it /// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. A share fills, because a /// is in rather than a part to be placed inside it. A share fills, because a
/// share is a length only to whoever divides one, and whoever did is the one /// share is a length only to whoever divides one, and whoever did is the one
/// that handed down this box. A declared axis does too: `declared_box` /// that handed down this box. A declared axis does too: the rule already gave
/// already placed it, in the parent's box, and the rule's length is what the /// the region its length, and the rule's length is what the widget reports
/// widget reports there. And an axis the parent decided from the answer is /// there. And an axis the parent decided from the answer is
/// the answer already. /// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool { pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided reported.leftover != Weight::ZERO || declared.is_some() || decided
} }
/// What of the box it was given a widget's drawing occupies, as lengths of /// Where a widget's drawing goes inside the part its parent gave it: what
/// that box: the size it reported wherever that is a part to be placed, and /// it reported, on the side of the part its alignment says, and the whole
/// the whole of the box wherever the answer fills it. /// 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 rel base, 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 is a length of the box rather than a length composed /// placed in sits and however long it is -- the fraction is resolved once,
/// into it, and a box in pixels is this step from the given box's pixels. /// here, against the rel base it was reported of.
pub(crate) fn placed_lens( pub(crate) fn placement(
region: UiRegion,
size: Size, size: Size,
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
decided: [bool; 2], place: PlaceDesc,
) -> UiVec2 { align: RegionAlign,
let mut lens = UiVec2::FULL_SIZE; ) -> UiRegion {
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
let reported = size.axis(axis);
if !fills(reported, declared, decided) {
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
}
}
lens
}
/// Where that drawing sits: those lengths taken of the box the widget was
/// asked in, on the side of it that the widget's alignment says.
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
let mut placed = region; let mut placed = region;
for axis in AXES { for axis in AXES {
// The whole of the box is already where it sits, and the arithmetic let n = axis as usize;
// below is the identity for it. let reported = size.axis(axis);
if lens.axis(axis) == Len::FULL { if fills(reported, declared[n], place.axis(axis).does_fill()) {
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 = lens.axis(axis).within_len(span.len());
span.start += (span.len() - len).scale(align.axis(axis).rel()); span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len; span.end = span.start + len;
} }
placed placed
} }
/// Takes a widget's declared lengths in the box `region` is given in, since a /// The rel base length and the box a child is asked in, in the coordinates the
/// fraction of a length means a fraction of that one, and puts what is left /// widget asking draws in.
/// over on the side its alignment says. A caller that already reserved the ///
/// space hands back the same length, so this is the identity for it. /// `own` is that widget's own box, and `place` what of it the child is
pub(crate) fn declared_box( /// given, including any rel base it states -- a row's slot, or padding's rel
mut region: UiRegion, /// base less its pixels. That is a window length, like every other length
/// here, since a slot of a row is not a fraction of anything the row can
/// name. The child's declaration is a fraction of whichever reached it, and
/// is the only one that also places the box: a box the caller decided is
/// what `place` names.
pub(crate) fn rel_base_and_region(
own: UiRegion,
parent_rel_base: UiVec2,
place: PlaceDesc,
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
align: RegionAlign, align: RegionAlign,
) -> UiRegion { ) -> (UiVec2, UiRegion) {
for (axis, len) in AXES.into_iter().zip(declared) { let given = place.of(own, align);
let Some(len) = len else { continue }; let mut rel_base = parent_rel_base;
let span = region.axis_mut(axis); let mut region = given;
let len = Len::from_parts(len.rel, len.px); for axis in AXES {
span.start += (span.len() - len).scale(align.axis(axis).rel()); let n = axis as usize;
span.end = span.start + len; let base = place
.axis(axis)
.stated_rel_base()
.unwrap_or_else(|| parent_rel_base.axis(axis));
let len = declared[n]
.map(|len| Len::from_parts(len.rel, len.px).within_len(base))
.unwrap_or(base);
*rel_base.axis_mut(axis) = len;
if declared[n].is_some() {
let slot = given.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*region.axis_mut(axis) = UiSpan::new(start, start + len);
}
} }
region (rel_base, region)
} }
+249
View File
@@ -0,0 +1,249 @@
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
/// How a child's region along one axis comes from the region of the widget
/// asking, and what its fractions are of.
///
/// The three ways of saying a region are the three the geometry already has:
/// a span composed into the caller's box, a span shifted to where that box
/// starts, and a length placed in it by alignment. Which one is meant cannot
/// be read off the numbers, since two of them take the same span and apply
/// it differently, so it is said here.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDescAxis {
span: PlaceSpan,
fills: bool,
rel_base: RelBase,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PlaceSpan {
Within(UiSpan),
Shifted(UiSpan),
Sized(Len),
}
/// What a child's fractions are of, where the caller has not named a length.
#[derive(Clone, Copy, Debug, PartialEq)]
enum RelBase {
/// The caller's own, unchanged.
Inherit,
/// The caller's own, narrowed the way the region is.
WithRegion,
/// This length of the window.
Len(Len),
}
impl PlaceDescAxis {
/// The whole of the caller's box.
pub const WHOLE: Self = Self::within(UiSpan::FULL);
/// `span` composed into the caller's own box, so it moves and scales
/// with it: [`UiSpan::within`], which is what a container that insets
/// one speaks. Taking eleven pixels off the end needs no length, where
/// saying the same thing in window lengths would make the container read
/// its own box -- and a box chosen from its own answer then feeds back
/// into the answer.
///
/// The child's rel base is narrowed the same way, so padding takes its
/// pixels off both and `rel(1)` under it fills the caller rather than
/// overflowing it.
pub const fn within(span: UiSpan) -> Self {
Self {
span: PlaceSpan::Within(span),
fills: false,
rel_base: RelBase::WithRegion,
}
}
/// `span` shifted to where the caller's own box starts: window lengths
/// along a cursor, which is what a container dividing room among its
/// children speaks. A child's report is a window length, so the cursor
/// that sums those reports is one too, and a moved box re-places every
/// child by re-adding its start, exactly.
///
/// The child's rel base passes through: how far along the cursor a child
/// sits says nothing about what a fraction under it is of.
pub const fn shifted(span: UiSpan) -> Self {
Self {
span: PlaceSpan::Shifted(span),
fills: false,
rel_base: RelBase::Inherit,
}
}
/// A box this long, placed in the caller's own by the child's alignment:
/// the rule that places an answer, with the length given from above
/// rather than reported. What a stack's sizing child decides for the
/// rest. It is the child's rel base too.
pub const fn sized(len: Len) -> Self {
Self {
span: PlaceSpan::Sized(len),
fills: false,
rel_base: RelBase::Len(len),
}
}
/// This region is the child's placement: its answer is not placed inside
/// it again. A container uses it where it hands back exactly what the
/// child asked for -- a row placing a child at the length it reported.
pub const fn fills(mut self) -> Self {
self.fills = true;
self
}
/// What the child's fractions are of, as a length of the window: a
/// resolved share, or a box a sibling's answer decided.
pub const fn rel_base(mut self, len: Len) -> Self {
self.rel_base = RelBase::Len(len);
self
}
/// Whether the region is the placement outright, rather than a box the
/// answer is placed inside.
pub(crate) const fn does_fill(self) -> bool {
self.fills
}
/// Where it lands in the coordinates `own` is in.
pub(crate) fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan {
match self.span {
PlaceSpan::Within(span) => span.within(&own),
PlaceSpan::Shifted(mut span) => {
span.shift(own.start);
span
}
PlaceSpan::Sized(len) => {
let start = own.start + (own.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
}
}
/// The child's rel base, where this says one outright. `None` forwards
/// the caller's own, and [`RelBase::WithRegion`] is resolved by whoever
/// can read that rel base, so it does not reach here.
pub(crate) const fn stated_rel_base(self) -> Option<Len> {
match self.rel_base {
RelBase::Len(len) => Some(len),
_ => None,
}
}
/// The length this narrows the caller's rel base by, where it does.
/// `None` leaves that rel base alone, and reading it is then a
/// dependency the caller does not take.
pub(crate) const fn narrows_rel_base(self) -> Option<UiSpan> {
match (self.rel_base, self.span) {
(RelBase::WithRegion, PlaceSpan::Within(span)) => Some(span),
_ => None,
}
}
/// The span it composes into the caller's box, where that is what it
/// does: the one case whose validity maps back through the part.
pub(crate) const fn within_span(self) -> Option<UiSpan> {
match self.span {
PlaceSpan::Within(span) => Some(span),
_ => None,
}
}
/// Whether the caller decided this length rather than a place along its
/// own box, which is what stops its length reaching the child at all.
pub(crate) const fn is_sized(self) -> bool {
matches!(self.span, PlaceSpan::Sized(_))
}
/// The same, with its rel base stated outright.
pub(crate) const fn with_rel_base(mut self, len: Option<Len>) -> Self {
self.rel_base = match len {
Some(len) => RelBase::Len(len),
None => RelBase::Inherit,
};
self
}
}
/// Where a child is asked, on both axes. A [`UiRegion`] converts into the
/// common case: that box of the caller's own, the answer placed inside it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDesc {
pub x: PlaceDescAxis,
pub y: PlaceDescAxis,
}
impl PlaceDesc {
/// The whole of the caller's box, on both axes.
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
Self { x, y }
}
/// The same on both axes.
pub const fn splat(place: PlaceDescAxis) -> Self {
Self { x: place, y: place }
}
/// `aligned` on `axis` and `ortho` on the other, which is how a
/// container that divides one axis says what it is doing.
pub fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
Axis::Y => Self::new(ortho, aligned),
}
}
pub const fn axis(&self, axis: Axis) -> &PlaceDescAxis {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut PlaceDescAxis {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
pub const fn fills(self) -> Self {
Self::new(self.x.fills(), self.y.fills())
}
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
*self.axis_mut(axis) = self.axis(axis).rel_base(len);
self
}
/// The box each axis names, in the coordinates `own` is in.
pub(crate) fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
}
}
impl From<UiRegion> for PlaceDesc {
fn from(region: UiRegion) -> Self {
Self::new(
PlaceDescAxis::within(region.x),
PlaceDescAxis::within(region.y),
)
}
}
impl From<PlaceDescAxis> for PlaceDesc {
fn from(place: PlaceDescAxis) -> Self {
Self::splat(place)
}
}
/// 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,
-1
View File
@@ -26,7 +26,6 @@ impl DefaultAppState for State {
.wrap(true) .wrap(true)
.text_align(Align::LEFT) .text_align(Align::LEFT)
.pad(16) .pad(16)
.width(rel(1.0))
.background(panel()); .background(panel());
// Each one takes the whole width, because `text_align` puts the // Each one takes the whole width, because `text_align` puts the
+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.
+25 -9
View File
@@ -117,19 +117,35 @@ 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 = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); .widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top))
.len(Axis::X);
let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X);
// The range it actually branched on, said the way a container says
// one: pinning the window instead would redraw this widget on every
// resize, which is a fixture that never exercises reuse.
let threshold = Px::from_f32(self.threshold);
let holds = match px > threshold {
true => Holds::from(threshold + Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=threshold),
};
painter.window_holds(Axis::X, holds.through(len));
let mut below = UiRegion::FULL; let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y)));
below.y.start = below.y.start.offset(Px::from_int(40)); let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below);
match px > Px::from_f32(self.threshold) { match px > threshold {
true => painter.widget_within(&self.wide, below), true => painter.widget_at(&self.wide, place),
false => painter.widget_within(&self.narrow, below), false => painter.widget_at(&self.narrow, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
pub struct Spanned { pub struct Spanned {
+5 -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
@@ -15,4 +15,8 @@ impl Widget for Masked {
// draw, and the framework would place the drawing it clipped away. // draw, and the framework would place the drawing it clipped away.
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
+3 -2
View File
@@ -7,7 +7,8 @@ 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); painter
painter.widget_within(&self.inner, region).size() .widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
.size()
} }
} }
+13 -5
View File
@@ -13,9 +13,13 @@ 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()) // Padding is an inset of both: it comes off the rel base, so `rel(1)`
.size(); // under it fills this widget rather than overflowing it by the
// padding, and it comes off the box, so what is drawn sits inside.
// The two stay distinct -- the box can be narrower still, where a row
// asked this widget in the room left, and a text wraps at that.
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
@@ -53,14 +57,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 {
+27 -22
View File
@@ -12,16 +12,13 @@ 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 // Asked in the whole viewport, then put at the scrolled offset.
// not already known, then draw it at the scrolled offset. let answer_len = painter
let whole = UiRegion::FULL; .widget_at(&self.inner, PlaceDesc::WHOLE.fills())
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) { .len(self.axis);
Some(len) => len, let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis);
None => painter.widget(&self.inner).size().axis(self.axis),
};
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;
@@ -34,9 +31,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);
@@ -47,7 +44,6 @@ 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;
// 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,22 +51,31 @@ impl Widget for Scroll {
// one centred in `px 900`, since halving a difference is not halving // one centred in `px 900`, since halving a difference is not halving
// 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 { let content = match moved || self.content_len != self.container_len {
let offset = UiVec2::from_axis( true => {
self.axis, let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
Len::from_parts(Rel::ZERO, anchor - self.amt), PlaceDescAxis::shifted(UiSpan::new(start, start.offset(self.content_len)))
Len::ZERO, }
); false => PlaceDescAxis::WHOLE,
region = region.offset(offset); };
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); // The viewport is the inner's rel base, so a fraction it declares or
} // reports is a fraction of what is on screen rather than of the
painter.widget_at(&self.inner, region, region.size(), [true; 2]); // content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there.
painter.place_at(
&self.inner,
PlaceDesc::from_axis(self.axis, content.fills(), PlaceDescAxis::WHOLE.fills()),
);
// 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
// is. // is.
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
impl Scroll { impl Scroll {
+57 -66
View File
@@ -10,25 +10,35 @@ 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 rel base 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.region_len(axis);
let 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 = PlaceDescAxis::WHOLE;
// A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The rel base 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 a drawn child is asked 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 len = match painter.size_hint(child, axis) {
if self.dir.sign == Sign::Neg {
span.flip();
}
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
// Offered the room left from the cursor, because a text has to
// wrap at the width actually there, but reporting a fraction of
// the whole row: `rel(0.5)` is half the span whatever else is in
// it and wherever this child sits among them.
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
Some(len) => len, Some(len) => len,
None => painter None => {
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2]) let room = PlaceDescAxis::shifted(along(cursor, far));
.len(axis), painter
.widget_at(child, PlaceDesc::from_axis(axis, room, across))
.len(axis)
}
}; };
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
@@ -46,43 +56,26 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// What is left for the shares to divide: the row less everything
// fixed, as a length of the rel base rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. The room to // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// divide is `len * fixed - total.px`, and the length where it runs // itself, and answered back through the same expression, so the
// out is exactly the box a parent sizing itself from this answer // boundary is the drawing's own and not a second way of finding it:
// hands back -- which is why this used to need a margin either side // the three cases a rounded division needed -- the fixed parts
// of the boundary, and why it does not now: that box and this sum are // growing slower than the box, faster, or exactly with it -- are the
// whole counts of the same step, and both routes to it land on the // sign of `room.rel`, which `through` already reads. What the
// same count. What the generated oracle checks is the consequence, // generated oracle checks is the consequence, since which children
// since which children exist at all turns on this. // exist at all turns on this.
let fixed = Rel::ONE - total.rel;
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
let current = painter.px_len(axis); shares = painter.to_px(room, 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.window_holds(axis, holds.through(room));
} }
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
@@ -99,10 +92,9 @@ 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) {
// A child asking for nothing but a part of what is left over, // A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for // when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows. // pixels or a fraction keeps those and overflows.
@@ -110,32 +102,31 @@ impl Widget for Span {
{ {
painter.undraw(child); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
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 child's box outright rather than something to place an
if self.dir.sign == Sign::Neg { // answer inside again. A share is decided here and nowhere
region.flip(axis); // else: its slot narrows its rel base, and the child is asked in
// it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here.
let slot = along(from, start);
let slot_place = PlaceDescAxis::shifted(slot).fills();
let mut place = PlaceDesc::from_axis(axis, slot_place, across);
if len.leftover > Weight::ZERO && shares {
place = place.rel_base(axis, slot.len());
} }
// Along the row this box is the child's own answer, so the answer let used = painter.place_at(child, place).len(!axis);
// is not placed in it again; across it the child sits where its
// alignment says.
let placed = painter.widget_at(
child,
region,
UiVec2::FULL_SIZE,
[axis == Axis::X, axis == Axis::Y],
);
if shrinks { if shrinks {
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
// span's own eventual width admits multiple fixed points. // span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too; // A scalable child therefore makes Children scalable too;
+30 -25
View File
@@ -13,45 +13,50 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
}; };
// Every child gets the whole of this stack's box, the sizing one // Whichever child sizes the stack is given the stack's whole box --
// included, and the stack is then handed a box of the length that // the stack is the length that child asked for, so placing that
// child asked for. Not the part of the box that length takes: the // answer inside the box it decided would apply it twice.
// stack's own box becomes that length, and taking the fraction of it
// again is the fraction twice -- a child asking for half of a stack
// that is already half a row would have a quarter of the row.
//
// It cannot be told apart by asking whether this box is the answer
// yet, either. A drawing has to be a function of the box alone, since
// moving the stack into the box it asked for reuses the drawing by
// scaling it, and a drawing made a fraction of one box is right in
// any other. So: fractions of this box throughout, and the move is
// the whole of the difference.
let region = UiRegion::FULL;
// Whichever child sizes the stack is asked here and not again below,
// on the layer it ends up on: a retained drawing belongs to the layer
// it was made on, so measuring it anywhere else costs a second
// drawing of it. Its box is its own answer, so the answer is not
// placed inside it again.
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
// rather than a second drawing of it somewhere else: a retained
// 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 painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
.widget_at(child, region, region.size(), [true; 2])
.size()
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
// Every other child gets the box the sizing child decided: the
// stack is that length, so that is the box they are asked in, and a
// fraction under them is a fraction of it. A share leaves the axis
// to whoever gave the stack its box. Where a child sits in a box
// bigger than itself is its own business.
let on = |axis| {
let len = size.axis(axis);
match len.leftover == Weight::ZERO {
true => PlaceDescAxis::sized(Len::from_parts(len.rel, len.px)).fills(),
false => PlaceDescAxis::WHOLE,
}
};
let place = PlaceDesc::new(on(Axis::X), on(Axis::Y));
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) { if sizing == Some(i) {
continue; continue;
} }
painter.child_layer_at(i); painter.child_layer_at(i);
// A box that owes nothing to this child's own answer: where it painter.widget_at(child, place);
// sits in one bigger than itself is its own business.
painter.widget_within(child, region);
} }
size size
} }
/// Without a sizing child a stack is whatever box it is given, which it
/// can say without drawing anything.
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
match self.size {
StackSize::Default => Some(LayoutLen::LEFTOVER),
StackSize::Child(_) => None,
}
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
+5 -15
View File
@@ -50,20 +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.
//
// The step at or above that longest line rather than the nearest
// one, since the shaper measures in floats: the nearest step is
// under the line half the time, and a range starting there admits a
// box the line does not fit in, where the break is not this one.
if let Some(width) = width {
painter.holds(Axis::X, Px::ceil_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> {
@@ -89,8 +80,7 @@ impl TextView {
// hair under that line, and the break made in it is not the break a // hair under that line, and the break made in it is not the break a
// cold layout makes there. // cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size)); let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
let within = region.within(&painter.region()); painter.glyphs(tex, region);
painter.glyphs(tex, within);
(region, size) (region, size)
} }
+10 -8
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 = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); .widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top))
.len(Axis::X);
let px = painter.to_px(measured.apply_leftover(), Axis::X);
let mut below = UiRegion::FULL; let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y)));
below.y.start = below.y.start.offset(Px::from_int(40)); let place = PlaceDesc::new(PlaceDescAxis::WHOLE, 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, place),
false => painter.widget_within(&self.narrow, below), false => painter.widget_at(&self.narrow, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
+173 -38
View File
@@ -20,11 +20,12 @@ 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 offers each child the room left after the one before, because a /// 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 reads what the child /// text has to wrap at the width actually there, but the child's region is
/// reports as a fraction of the whole row. So two children asking for half /// the whole row. So two children asking for half each take the whole row
/// each take the whole row between them, however much of it was left when /// between them, however much of it was left when each was asked, and a third
/// each was asked, and a third overflows. /// 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] #[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() { fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100)); let mut h = Harness::new((400, 100));
@@ -34,10 +35,10 @@ fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let tail = rect(Color::BLUE).width(100).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))); h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported and drawn there // The nested span is placed at the length it reported, and its own child
// once more; half of that final box is what its own child takes. // asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100)); assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (300, 100)); assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100)); assert_corners!(h, tail, (400, 0), (500, 100));
} }
@@ -82,36 +83,10 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
assert!(crowded > whole_row, "{crowded} against {whole_row}"); assert!(crowded > whole_row, "{crowded} against {whole_row}");
} }
/// A stack takes its size from one child and gives every child that size, so /// Padding is an inset: it narrows the frame a fraction resolves against and
/// a child asking for half of it is asking for half of what it is itself the /// adds itself back to the padded widget's reported length.
/// size of. Once the stack has been placed at the length it reported that
/// length is the box, and taking the fraction of it again takes it twice:
/// half a row became a quarter, and a further stack around it a further half.
/// Nothing pinned it because a pixel is the same length wherever it is taken
/// from, so only a share ever shrank -- and warm and cold shrink alike, so no
/// oracle saw it either.
#[test] #[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() { fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
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));
}
/// The same reading through a pad: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two.
#[test]
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
let mut h = Harness::new((400, 100)); let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc); 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 padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
@@ -120,10 +95,85 @@ fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
// placed inside it by its own alignment, which is not what is under test. // 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))); h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (210, 100)); assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100)); assert_corners!(h, tail, (210, 0), (310, 100));
} }
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.";
/// The worked example of what padding insets: in a 900 px row after a 24 px
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
/// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there.
#[test]
fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let fill_width = h.region(&fill).unwrap().size().x;
assert_eq!(fill_width, Px::from_int(868));
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
let asked = active.region.x.len().to_px(window);
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844));
}
/// The other way round: a share inside padding. A slot is a length of the
/// row, which is already the padded width, so what the span decided reaches
/// the child as it stands -- taking the padding off a second time would make
/// `rel(1.0)` in the slot shorter than the slot.
#[test]
fn a_share_inside_padding_fills_the_slot_it_was_given() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let first = Span {
children: vec![fill.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.width(leftover(1))
.add(&mut h.rsc);
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row.pad(16));
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
}
/// The same padding in a share instead: the slot is 450, so both the
/// fraction and the wrap are the slot less the padding, and the two agree.
#[test]
fn padding_narrows_both_rel_base_and_box_inside_a_share() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
let mut h = Harness::new((900, 200));
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
}
#[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));
@@ -450,7 +500,7 @@ fn a_row_of_equal_shares_fills_it_exactly() {
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`. /// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region); let region = h.render.moves.resolve(active.move_idx, active.placement);
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 + s.px.to_f32()); let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
@@ -723,3 +773,88 @@ 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));
}
/// The root's frame is the window and its rule is a fraction of that, which
/// is one resolution and not two: nothing above it narrowed anything.
#[test]
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
let mut h = Harness::new((900, 200));
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
}
#[test]
fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
for dir in [Dir::RIGHT, Dir::LEFT, Dir::DOWN, Dir::UP] {
for collapsed in [1, 2] {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).add(&mut h.rsc);
h.set_len(head, dir.axis, 200);
let tail = rect(Color::BLUE).add(&mut h.rsc);
let tail_len = 200 - 10 * (collapsed + 1);
h.set_len(tail, dir.axis, tail_len);
let mut children: Vec<StrongWidget> = vec![head.add_strong(&mut h.rsc)];
let mut shares = Vec::new();
for _ in 0..collapsed {
let share = rect(Color::GREEN).add(&mut h.rsc);
shares.push(share);
children.push(share.add_strong(&mut h.rsc));
}
children.push(tail.add_strong(&mut h.rsc));
h.set_root(Span {
children,
dir,
gap: Px::from_int(10),
});
for share in shares {
assert!(h.region(&share).is_none());
}
let region = h.region(&tail).unwrap();
let (from, to) = match dir.sign {
Sign::Pos => (400 - tail_len, 400),
Sign::Neg => (0, tail_len),
};
assert_eq!(region.top_left.axis(dir.axis), Px::from_int(from));
assert_eq!(region.bot_right.axis(dir.axis), Px::from_int(to));
}
}
}
+880 -20
View File
@@ -156,16 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT)); 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, // Asked once, from the cursor; its slot is its answer and the drawing is
// and it reports less than that box: so it is drawn again in the box its // moved there.
// answer places it in, and once more in the final box the span chooses. assert_eq!(asked_draws.get(), 1);
// 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 +213,11 @@ 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,
PlaceDesc::new(PlaceDescAxis::WHOLE, PlaceDescAxis::shifted(top)),
);
Size::LEFTOVER Size::LEFTOVER
} }
} }
@@ -259,10 +254,9 @@ impl Widget for ReadsBox {
/// 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 of these report a quarter of what they read. The quarter-sized box
/// drawing holds there too, so each length they are asked at costs two draws: /// the answer places them in is not a question: the drawing is moved there,
/// one to answer, and one in the quarter-sized box that answer places them /// so each length they are asked at costs one draw.
/// in. The counts below are in those pairs.
struct ReadsWidth { struct ReadsWidth {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
@@ -312,6 +306,51 @@ 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,
};
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 +382,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 +402,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 +429,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
@@ -628,3 +667,824 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
h.frame(); h.frame();
assert_corners!(h, inner, (100, 0), (400, 200)); 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_region_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,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x),
PlaceDescAxis::shifted(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,
frame: UiRegion,
region: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x).fills(),
PlaceDescAxis::shifted(self.region.y).fills(),
)
.rel_base(Axis::X, self.frame.x.len()),
);
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),
frame: UiRegion::FULL,
region: 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].frame.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
h.rsc[root].region = UiRegion::new(
UiSpan::new(Len::rel(start), Len::rel(end)),
UiSpan::new(Len::px(7.25), Len::rel(end)),
);
h.frame();
assert_eq!(draws.get(), before);
let retained = primitive_bounds(&h, text.id());
assert!(!retained.is_empty());
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
h.frame();
assert!(draws.get() > before);
assert_eq!(retained, primitive_bounds(&h, text.id()));
}
}
}
#[test]
fn resizing_does_not_remeasure_a_fixed_stack_for_its_unmeasured_overlay() {
let mut h = Harness::new((400, 200));
let (sizing, _) = counted(&mut h, Size::from((100, 80)), false);
let (overlay, draws) = counted(&mut h, Size::LEFTOVER, true);
h.set_root((sizing, overlay).stack().size(StackSize::Child(0)));
let settled = draws.get();
h.resize((800, 300));
h.frame();
assert_eq!(draws.get(), settled);
assert_corners!(h, overlay, (350, 110), (450, 190));
}
struct Unmeasured {
child: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Unmeasured {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.child);
Size::LEFTOVER
}
}
#[test]
fn a_declared_size_change_stops_at_an_independent_parent() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::RED).width(100).add(&mut h.rsc);
let parent = Unmeasured {
child: leaf.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add_strong(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
h.set_root(Unmeasured {
child: parent,
draws: draws.clone(),
});
let settled = draws.get();
h.set_len(leaf, Axis::X, 150);
h.frame();
assert_corners!(h, leaf, (125, 0), (275, 200));
assert_eq!(draws.get(), settled);
}
#[test]
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((leaf,).stack());
let settled = draws.get();
h.resize((800, 200));
h.frame();
assert!(draws.get() > settled);
assert_corners!(h, leaf, (300, 90), (500, 110));
}
#[test]
fn changed_drawing_dependencies_reach_ancestors_without_a_size_change() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
h.set_root(((leaf,).stack(),).stack());
h.rsc[leaf].reads_box = true;
h.frame();
let settled = draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(draws.get(), settled + 1);
assert_corners!(h, leaf, (0, 0), (800, 200));
}
#[test]
fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
let mut h = Harness::new((400, 200));
let (leaf, leaf_draws) = counted(&mut h, Size::LEFTOVER, true);
let draws = Rc::new(Cell::new(0));
let child = leaf.add_strong(&mut h.rsc);
h.set_root(Unmeasured {
child,
draws: draws.clone(),
});
let settled = draws.get();
for reads_box in [false, true, false, true] {
h.rsc[leaf].reads_box = reads_box;
h.frame();
assert_eq!(draws.get(), settled);
}
let settled = leaf_draws.get();
h.resize((800, 200));
h.frame();
assert_eq!(leaf_draws.get(), settled + 1);
}
#[test]
fn padding_and_stack_boxes_follow_the_region_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,
region: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x).fills(),
PlaceDescAxis::shifted(self.region.y).fills(),
),
);
Size::LEFTOVER
}
}
for node in [false, true] {
let plant = |h: &mut Harness, region| {
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, region }.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 region = at(start);
let before = draws.get();
warm.rsc[root].region = region;
warm.frame();
assert_eq!(draws.get(), before);
let mut cold = Harness::new((403, 211));
let (_, other, other_fixed, _) = plant(&mut cold, region);
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_a_childs_region_preserves_the_slot_chosen_from_its_measurement() {
struct Measured;
impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size {
let width = painter.px_len(Axis::X);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
}
}
struct Frame {
child: StrongWidget,
start: f32,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
))
.fills(),
PlaceDescAxis::shifted(UiSpan::FULL).fills(),
),
);
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 changing_regions_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,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x),
PlaceDescAxis::shifted(self.region.y),
),
)
.size()
}
}
struct Frame {
child: StrongWidget,
region: 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,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x).fills(),
PlaceDescAxis::shifted(self.region.y).fills(),
),
)
.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, outer| {
let size = if fractional {
Size {
x: rel(0.5),
y: LayoutLen::px(27),
}
} else {
Size::from((80, 27))
};
let (leaf, _) = counted(h, size, !fractional);
let child = Container {
child: leaf.add_strong(&mut h.rsc),
region,
}
.add_strong(&mut h.rsc);
let answer = Rc::new(Cell::new(Size::ZERO));
let root = Frame {
child,
region: outer,
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 region =
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
warm.rsc[root].region = region;
warm.frame();
let mut cold = Harness::new((403, 211));
let (_, other, other_answer) = plant(&mut cold, region);
assert_eq!(answer.get(), other_answer.get());
assert_eq!(warm.region(&leaf), cold.region(&other));
}
}
}
}
struct OptionalMask {
inner: StrongWidget,
enabled: bool,
}
impl Widget for OptionalMask {
fn draw(&mut self, painter: &mut Painter) -> Size {
if self.enabled {
painter.set_mask(UiRegion::FULL);
}
painter.widget(&self.inner);
Size::LEFTOVER
}
}
fn primitive_masks(h: &Harness, id: WidgetId) -> Vec<MaskIdx> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx]
.mask_idx
})
.collect()
}
#[test]
fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::RED).height(50).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let child = Stretchy {
inner: inner.add_strong(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
let masked = child.masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(masked, node);
h.set_root((first, masked).span(Dir::DOWN));
let mask = h.render.active[&masked.id()].mask;
let settled = draws.get();
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
assert_eq!(h.render.active[&masked.id()].mask, mask);
h.set_len(first, Axis::Y, 10);
h.frame();
let clip = h.rsc.ui().masks[mask.idx()];
let clip = h
.render
.moves
.resolve(clip.move_idx, clip.region)
.to_px(h.render.output_size());
assert_eq!(clip, h.region(&masked).unwrap());
assert_corners!(h, inner, (0, 10), (400, 200));
}
}
#[test]
fn adding_and_removing_a_mask_updates_existing_primitives() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = OptionalMask {
inner: inner.add_strong(&mut h.rsc),
enabled: false,
}
.add(&mut h.rsc);
h.set_root(masked);
for enabled in [true, false, true, false] {
h.rsc[masked].enabled = enabled;
h.frame();
let mask = h.render.active[&masked.id()].mask;
assert_eq!(mask == MaskIdx::NONE, !enabled);
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
}
assert_eq!(h.rsc.ui().masks.len(), 1, "retired slots must be reusable");
}
#[test]
fn an_empty_masks_slot_is_released_when_the_mask_is_removed_or_undrawn() {
let mut h = Harness::new((400, 200));
let (inner, _) = counted(&mut h, Size::LEFTOVER, false);
let masked = OptionalMask {
inner: inner.add_strong(&mut h.rsc),
enabled: true,
}
.add(&mut h.rsc);
let row = (masked,).span(Dir::DOWN).add(&mut h.rsc);
h.set_root(row);
for _ in 0..3 {
h.rsc[masked].enabled = false;
h.frame();
h.rsc[masked].enabled = true;
h.frame();
let child = h.rsc[row].pop().unwrap();
h.frame();
h.rsc[row].push(child);
h.frame();
}
assert_eq!(h.rsc.ui().masks.len(), 1);
}
struct SharedChild(Rc<StrongWidget>);
impl Widget for SharedChild {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(self.0.as_ref()).size()
}
}
struct SwitchParent {
choices: [StrongWidget; 2],
choice: usize,
}
impl Widget for SwitchParent {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget(&self.choices[self.choice]).size()
}
}
#[test]
fn a_redrawn_subtree_is_not_undrawn_by_the_parent_it_left() {
for node in [false, true] {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::RED).width(40).add(&mut h.rsc);
let held: StrongWidget = leaf.add_strong(&mut h.rsc);
let shared = Rc::new(held);
let first = SharedChild(shared.clone()).add_strong(&mut h.rsc);
let second = SharedChild(shared).add_strong(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(&second, node);
let root = SwitchParent {
choices: [first, second],
choice: 0,
}
.add(&mut h.rsc);
h.set_root(root);
let before = h.region(&leaf);
h.rsc[root].choice = 1;
h.frame();
assert_eq!(h.region(&leaf), before);
}
}
+80 -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()
} }
} }
@@ -82,3 +145,19 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
h.set_root(clipper); h.set_root(clipper);
h.frame(); h.frame();
} }
/// Content that fits sits in the viewport, not in a box of the window's
/// length anchored at the viewport's start. `Part::From` takes window
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
/// filling the window would land right.
#[test]
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).height(100).add(&mut h.rsc);
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
h.set_root((head, scroll).span(Dir::DOWN));
assert_corners!(h, scroll, (0, 100), (400, 400));
assert_corners!(h, inner, (0, 225), (400, 275));
}
+453
View File
@@ -14,6 +14,180 @@ use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch; 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
/// taken again -- so no box may move, and a warm frame has to land where a /// taken again -- so no box may move, and a warm frame has to land where a
@@ -559,6 +733,51 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \ 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."; box leaves room for, so a paragraph's height is an answer and not a setting.";
fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec<WidgetId>, WidgetId) {
let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None);
if fixed {
h.rsc.widgets_mut().set_size_rules(
sizing.id(),
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
}
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let pad = Pad {
padding: Padding::ZERO,
inner: text.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.set_root(stack);
(
vec![sizing.id(), text.id(), pad.id(), stack.id()],
sizing.id(),
)
}
#[test]
fn fixing_a_stacks_sizing_child_repositions_its_overlay() {
let mut warm = Harness::new((900, 1200));
let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false);
warm.frame();
warm.rsc.widgets_mut().set_size_rules(
sizing,
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4, /// 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, /// `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 span takes its width from the longest line of the texts in it, and
@@ -614,3 +833,237 @@ fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
assert_eq!(warm.region(&text), cold.region(&cold_text)); 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));
}
/// Nine widgets, shrunk from seed 946 at depth 6. The column is a share of
/// the row while its rect has room to draw and a fixed width once it has
/// not, so the row asks it twice: in the room, where it answers a share,
/// and in its slot, where it answers its text's width. Emptying the column
/// changes only the first answer. A local redraw that asked only the second
/// question kept the row as it was; the column has to defer to the row.
fn plant_column_that_is_a_share_only_while_its_rect_fits(
h: &mut Harness,
emptied: bool,
) -> (Vec<WidgetId>, WeakWidget<Span>, Vec<StrongWidget>) {
let first = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
let second = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let mut spare: Vec<StrongWidget> =
vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)];
let mut children: Vec<StrongWidget> = vec![first.add_strong(&mut h.rsc)];
if !emptied {
children.append(&mut spare);
}
let column = Span {
children,
dir: Dir::DOWN,
gap: Px::ZERO,
}
.height(159)
.add(&mut h.rsc);
let left = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
let right = rect(Color::BLUE.alpha(0)).add(&mut h.rsc);
let row = Span {
children: vec![
left.add_strong(&mut h.rsc),
column.add_strong(&mut h.rsc),
right.add_strong(&mut h.rsc),
],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let end = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
let root = Span {
children: vec![end.add_strong(&mut h.rsc), row.add_strong(&mut h.rsc)],
dir: Dir::LEFT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.set_root(root);
(
vec![
first.id(),
filler.id(),
second.id(),
column.id(),
left.id(),
right.id(),
row.id(),
end.id(),
root.id(),
],
column,
spare,
)
}
#[test]
fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
let mut warm = Harness::new((900, 1200));
let (ids, column, _spare) =
plant_column_that_is_a_share_only_while_its_rect_fits(&mut warm, false);
warm.frame();
// Kept alive: dropping the last share of a widget frees its id.
let _removed: Vec<StrongWidget> = warm.rsc[column].children.drain(1..).collect();
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _, _spare) =
plant_column_that_is_a_share_only_while_its_rect_fits(&mut cold, true);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column
/// divides the box it is given between two shares, so its drawing holds for
/// that box's length alone, and the pads above it pass that dependency up:
/// each one's box is a part of the box it was asked in. Padding narrowing
/// the frame it hands down does not change that, and while it was taken to,
/// changing the rule over the pads relocated the column's drawing into the
/// new box instead of dividing it again.
fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec<WidgetId> {
let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
let bottom = rect(Color::RED).add(&mut h.rsc);
let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
let inner = Pad {
padding: Padding::ZERO,
inner: column.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let outer = Pad {
padding: Padding::ZERO,
inner: inner.add_strong(&mut h.rsc),
}
.height(height)
.add(&mut h.rsc);
let beside = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((outer, beside).span(Dir::RIGHT));
vec![
top.id(),
bottom.id(),
column.id(),
inner.id(),
outer.id(),
beside.id(),
]
}
#[test]
fn changing_a_rule_over_two_pads_divides_the_column_again() {
let mut warm = Harness::new((900, 1200));
let ids = plant_two_shares_under_two_pads(&mut warm, 88.0);
warm.frame();
warm.rsc
.widgets_mut()
.set_size_rules(ids[4], None, Some(LayoutLen::px(105)));
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Six widgets, shrunk from seed 942 at depth 6 (`resize`). A `Branch` asks
/// its probe in the top 40 px of its box and forwards the frame, so the
/// scroll's own box is 40 px tall whatever the window is -- but its content
/// is as tall as the frame, which is the window, and a scroll kept to its
/// end has to be told when that changes. Resolving a length against the
/// window is what reads it, so that is where the dependency is taken.
fn plant_a_window_tall_column_in_a_short_scroll(h: &mut Harness) -> Vec<WidgetId> {
let leaf = rect(Color::RED).add(&mut h.rsc);
let column = Span {
children: vec![leaf.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.height(rel(1.0))
.add(&mut h.rsc);
let scroll = Scroll::new(column.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
let wide = rect(Color::BLUE).add(&mut h.rsc);
let narrow = rect(Color::GREEN).add(&mut h.rsc);
let root = Branch {
probe: scroll.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);
h.set_root(root);
vec![leaf.id(), column.id(), scroll.id(), root.id()]
}
#[test]
fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
let mut warm = Harness::new((1920, 1200));
let ids = plant_a_window_tall_column_in_a_short_scroll(&mut warm);
warm.frame();
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant_a_window_tall_column_in_a_short_scroll(&mut cold);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
+8 -4
View File
@@ -14,7 +14,7 @@
#[path = "scenario/mod.rs"] #[path = "scenario/mod.rs"]
mod scenario; mod scenario;
use iris::random::{Edits, plan}; use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds}; use scenario::{ALL, Case, diverges, env, over_seeds};
/// How deep the generator branches. The generator widens two to four ways per /// How deep the generator branches. The generator widens two to four ways per
@@ -32,8 +32,11 @@ fn depth() -> usize {
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98]; const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
fn check(seed: u64, depth: usize, case: Case) { fn check(seed: u64, depth: usize, case: Case) {
let grown = plan(seed, depth, &Edits::default()); check_plan(&plan(seed, depth, &Edits::default()), seed, depth, case);
if let Some(how) = diverges(&grown, case, seed) { }
fn check_plan(grown: &Plan, seed: u64, depth: usize, case: Case) {
if let Some(how) = diverges(grown, case, seed) {
panic!( panic!(
"seed {seed} at depth {depth} differs after {}: {how}\n\ "seed {seed} at depth {depth} differs after {}: {how}\n\
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \ reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
@@ -119,8 +122,9 @@ fn a_long_run_of_seeds_agrees() {
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(), None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
}; };
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default());
for case in ALL { for case in ALL {
check(seed, depth, case); check_plan(&grown, seed, depth, case);
} }
}); });
} }
+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);
+42
View File
@@ -0,0 +1,42 @@
//! Prints where a cold layout puts every widget of many grown trees, so two
//! commits can be compared on cold layout alone. The warm/cold oracle cannot
//! see a change that moves cold layout, since both of its sides move; this
//! can, by diffing its output across the change:
//!
//! IRIS_DUMP_SEEDS=400 IRIS_DUMP_DEPTH=5 cargo test --release \
//! --test layout_dump -- --ignored --nocapture > /tmp/before.txt
//!
//! then the same after, and `diff` the two. A line is one widget: the seed,
//! its index in creation order, and its box in window pixels, or `-` where
//! it is not drawn.
use iris::harness::Harness;
use iris::random::{Edits, grow};
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
#[test]
#[ignore = "a dump to diff across commits, not a check"]
fn every_cold_layout_is_printed() {
let seeds = env("IRIS_DUMP_SEEDS", 400_u64);
let depth = env("IRIS_DUMP_DEPTH", 5_usize);
let mut out = String::new();
for seed in 1..=seeds {
let mut harness = Harness::new((1920.0, 1200.0));
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
harness.state.root = Some(root);
harness.frame();
for (index, id) in tree.ids.iter().enumerate() {
match harness.region(id) {
Some(region) => out.push_str(&format!("{seed} {index} {region:?}\n")),
None => out.push_str(&format!("{seed} {index} -\n")),
}
}
}
print!("{out}");
}
+59 -32
View File
@@ -42,21 +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 two steps of the grid between the two ways of reaching
/// it. A move, a repaint, a row of shares and every length in pixels land on
/// the same number. What needs the slack is a position: a box centred in a
/// fraction of its parent against the same box centred in its own pixels,
/// and a box re-expressed as a fraction of a parent that changed length.
/// A step is a thousandth of a pixel, where this was a twentieth of one
/// before any of it was on a grid.
///
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
/// stopped being composed: it passes the 100-seed oracle and fails the
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
/// what is left here is the resize path's own rounding rather than a length
/// reached two ways.
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.
@@ -118,6 +103,11 @@ pub enum Case {
/// A resize and then a size change, so a retained answer is asked to /// A resize and then a size change, so a retained answer is asked to
/// survive two different kinds of invalidation in a row. /// survive two different kinds of invalidation in a row.
ResizeSize, ResizeSize,
/// A size change and then a resize, which is the other order and not the
/// same test: a length answered as a fraction of one box and kept as a
/// fraction of another agrees at the size it was changed at and parts
/// from it at every other one.
SizeResize,
/// A few declared sizes. /// A few declared sizes.
Size, Size,
/// Every declared size at once, so every reader of a size has a changed /// Every declared size at once, so every reader of a size has a changed
@@ -134,12 +124,13 @@ pub enum Case {
Shuffle(Shuffle), Shuffle(Shuffle),
} }
pub const ALL: [Case; 15] = [ pub const ALL: [Case; 16] = [
Case::Repaint, Case::Repaint,
Case::RepaintSome, Case::RepaintSome,
Case::Resize, Case::Resize,
Case::ResizeRepaint, Case::ResizeRepaint,
Case::ResizeSize, Case::ResizeSize,
Case::SizeResize,
Case::Size, Case::Size,
Case::EverySize, Case::EverySize,
Case::Align, Case::Align,
@@ -161,6 +152,7 @@ impl Case {
Self::Resize => "resize", Self::Resize => "resize",
Self::ResizeRepaint => "resize-repaint", Self::ResizeRepaint => "resize-repaint",
Self::ResizeSize => "resize-size", Self::ResizeSize => "resize-size",
Self::SizeResize => "size-resize",
Self::Size => "size", Self::Size => "size",
Self::EverySize => "every-size", Self::EverySize => "every-size",
Self::Align => "align", Self::Align => "align",
@@ -185,6 +177,15 @@ impl Case {
_ => (STILL, STILL), _ => (STILL, STILL),
} }
} }
/// The window the warm tree is taken to after the change, where the case
/// is about what the change left behind rather than about the change.
fn then_resize(self) -> Option<(f32, f32)> {
match self {
Self::SizeResize => Some(INNER),
_ => None,
}
}
} }
fn mark(warm: &mut Harness, tree: &Tree, step: usize) { fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
@@ -299,7 +300,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
warm.frame(); warm.frame();
return out; return out;
} }
Case::Size | Case::ResizeSize => Edits { Case::Size | Case::ResizeSize | Case::SizeResize => Edits {
sizes: some_sizes(warm, tree, rng), sizes: some_sizes(warm, tree, rng),
..Default::default() ..Default::default()
}, },
@@ -399,18 +400,15 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label label
} }
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool { /// One widget's layout as it stands: the frame its fractions resolved
match (got, want) { /// against, the box it was asked in, the box its drawing went in, and what
(Some(got), Some(want)) => { /// it reported. In window units, which is what both trees are in.
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS); fn record(id: WidgetId, h: &Harness) -> String {
same(got.top_left.x, want.top_left.x) let active = &h.render.active[&id];
&& same(got.top_left.y, want.top_left.y) format!(
&& same(got.bot_right.x, want.bot_right.x) "rel_base {} region {} placement {} size {}",
&& same(got.bot_right.y, want.bot_right.y) active.rel_base, active.region, active.placement, active.size,
} )
(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
@@ -429,6 +427,17 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
warm.frame(); warm.frame();
} }
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed)); let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
// Whatever the change left, seen at another window: an answer kept as a
// fraction of the wrong length is the same number of pixels where it was
// made and a different one everywhere else.
let end = match case.then_resize() {
Some(after) => {
warm.resize(after);
warm.frame();
after
}
None => end,
};
let mut cold = Harness::new(end); let mut cold = Harness::new(end);
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan); let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
@@ -439,12 +448,19 @@ 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;
} }
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
// Where two trees disagree is rarely where the cause is, so the // Where two trees disagree is rarely where the cause is, so the
// ancestry comes with it, marking the widgets that own a region. // ancestry comes with it, marking the widgets that own a region.
let mut chain = Vec::new(); let mut chain = Vec::new();
let mut records = Vec::new();
let mut at = Some(w); let mut at = Some(w);
while let Some(id) = at { while let Some(id) = at {
let active = &warm.render.active[&id]; let active = &warm.render.active[&id];
@@ -453,11 +469,22 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
false => "*", false => "*",
}; };
chain.push(format!("{}{node}", describe(id, &warm))); chain.push(format!("{}{node}", describe(id, &warm)));
// What each level was asked in on both sides, since the level
// where the two stop agreeing is the one to look at rather than
// the leaf that reported the difference.
let cold_id = places.get(&id).and_then(|&i| cold_tree.ids.get(i));
records.push(format!(
" {}\n warm {}\n cold {}",
describe(id, &warm),
record(id, &warm),
cold_id.map_or("-".into(), |&id| record(id, &cold)),
));
at = active.parent; at = active.parent;
} }
return Some(format!( return Some(format!(
"widget {i}\n warm {got:?}\n cold {want:?}\n {}", "widget {i}\n warm {got:?}\n cold {want:?}\n {}\n{}",
chain.join(" < ") chain.join(" < "),
records.join("\n"),
)); ));
} }
match drawn { match drawn {
+6 -2
View File
@@ -70,10 +70,14 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
over_seeds(seeds, |seed| { over_seeds(seeds, |seed| {
let grown = plan(seed, depth, &Edits::default()); let grown = plan(seed, depth, &Edits::default());
for &case in &cases { for &case in &cases {
let Some(how) = diverges(&grown, case, seed) else { if diverges(&grown, case, seed).is_none() {
continue; continue;
}; }
let small = shrink(grown.clone(), case, seed); let small = shrink(grown.clone(), case, seed);
// Described from the shrunk tree: the grown tree's chain names
// widgets that are no longer there, and the ancestry of the
// failure is what a test is written from.
let how = diverges(&small, case, seed).unwrap_or_default();
println!( println!(
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}", "seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
case.name(), case.name(),