Compare commits

..
Author SHA1 Message Date
iris-ai 750217631d Drop three things nothing reads, and say what a span makes scalable
`Axis::pair` and `RegionAlign::NEAR` arrived on this branch with no caller
and never got one. `Holds::contains` took `&self` where its five siblings on
the same `Copy` pair of pixels take `self`.

The comment beside a span's cross-axis accumulator said a scalable child
"makes Children scalable too"; `Children` names nothing here, and what it
makes scalable is the span.
2026-09-20 01:02:25 -04:00
iris-ai 9b4cc329ce Say the window when the window is why a drawing was refused
`AxisHolds` is four contracts, and `diag::outside` counted three: a refusal
because this window is outside the range the drawing was made for bumped
"reuse outside: a rel base". The two are different questions -- a window
range is pixels, a rel base pin is a window-unit length that an unchanged
window can still change -- so the rig answered "why did that redraw?" with
the wrong one for every resize.
2026-09-20 01:00:13 -04:00
iris-ai 02048eab77 Rebuild a suboptimal swapchain after presenting, not before
`Surface::configure` panics while a texture the surface handed out is still
alive, which wgpu says at both `configure` and `get_current_texture`. The
`Suboptimal` arm configured with the texture it was about to draw with in
hand, so the first suboptimal frame -- a resize or a display change on some
drivers -- takes the app down instead of rebuilding the swapchain.

The texture is good for this frame, so it is drawn with and presented, and
the rebuild happens once `present` has consumed it.
2026-09-20 01:00:05 -04:00
iris-ai d8d51221ee Keep a contract only where it still holds for this widget
`redraw` keeps the narrower of an old and a fresh contract so that widening
and narrowing back do not churn the parent that reads it. The drawing's half
asked whether the old range still covers this window and box before keeping
it; the answer's half did not, so a widget whose answer contract widened in
a frame that also resized the window kept a range the new window is outside.

The parent's next ask then refuses that answer and draws the whole subtree
again -- throwing away the drawing the widget had just made. Cost, not
geometry: the size kept is the size just reported.

`a_contract_this_window_is_outside_is_not_kept` draws the leaf twice before
the change and once after.
2026-09-20 01:00:01 -04:00
iris-aiandClaude Opus 5 781199a7c9 TODO: a prepare stage on Event, for the placeholder in CursorData
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:26:53 -04:00
iris-aiandClaude Opus 5 69ba91588a Run every generated case without the long seed scan
`Case::SizeResize` was in `ALL` and in none of the `case!` invocations, so
a size change followed by a resize -- the order the enum's own comment
argues is not the same test as the other one -- was only ever checked by
the ignored long run. The tests and the list of which cases have one come
from one macro invocation now, and a case missing from it fails a test
rather than going quiet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:20:23 -04:00
iris-aiandClaude Opus 5 8088a1fa59 Say why a reuse was refused, on every path that refuses one
Three of the eight rejections in `try_reuse` were invisible or half-visible
to the diagnostics: a changed inherited mask counted nothing and traced
nothing, an undrawn record traced without counting, and a changed
region-node choice counted without tracing. The mask one is the rejection
this branch's repair was about, so "why did that redraw?" was exactly the
question the rig could not answer.

Adding a counter meant editing a variant list and a name list at the same
index, which renames every total after a slip and says nothing. The two
lists are one declaration now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:20:23 -04:00
iris-aiandClaude Opus 5 713e3e747b Judge a kept contract against the box it was asked in
A local redraw keeps the narrower guarantee its parent holds when the new
drawing covers it, so widening and narrowing back do not churn the parent.
It checked that guarantee against `placement`, where the answer put the
drawing, rather than `region`, the box the drawing was made in and the box
both contracts are about. The two differ on every axis a widget reported
less than it was offered, so any such widget escalated to its parent every
time its contract widened -- which is the churn the retention exists to
avoid. `resize` and `try_reuse` both already ask about `region`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:18:18 -04:00
iris-aiandClaude Opus 5 aea0387567 Stop keeping what nothing reads back
`ActiveData::size_deps` was written on every draw, cleared on every undraw,
and read nowhere: a `Vec<WidgetId>` per active widget for a list only the
`Painter`'s own copy is used from, in `draw_at`, before the record is built.
What it looked like it was for -- reaching a widget whose size was read --
is already done there, by recording whoever asked about a child it did not
draw.

`SizeRule::apply` had no caller and would have been wrong if it found one:
it answers with the rule's own length, where `draw_at` resolves a fraction
against the rel base first. One rule, applied in one place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:18:18 -04:00
iris-ai 1ebd4d3a05 Match a place's own cases instead of asking it five questions
`PlaceSpan` and `RelBase` were private to `place.rs`, so `painter.rs`
reached them through six `pub(crate)` accessors -- `stated_rel_base`,
`narrows_rel_base`, `within_span`, `is_sized`, `does_fill`,
`with_rel_base` -- and `in_parent` re-derived a three-case enum from
five yes/no answers. The two enums are `pub` now and `ui/mod.rs`
re-exports `place` by name rather than by glob, the way it already did
for `painter`, so nothing new leaves the crate and there is no
visibility qualifier to get right. All six accessors are deleted:
`in_parent` matches `(at.span, declared)`, `resolve_rel_base` matches
`(rel_base, span)` and assigns, and the other three read the field.

`!at.is_sized()` was dead. `PlaceSpan::Sized` is built in exactly one
place, `Len::as_desc`, which sets `RelBase::Len(self)` in the same
literal, so `stated_rel_base().is_none()` already excluded it. Deleting
`with_rel_base` removes the only writer that could have separated the
two, so a named length now carries its own base by construction rather
than by habit -- which is what the comment on `RelBase` says.

The four `pub(crate)` methods left in `painter.rs` are inherent methods
on types the crate does export, so hiding the path is not available to
them; they are `pub(super)`, which is the module tree that calls them.

fmt, workspace clippy under `-D warnings` with and without
`layout-diagnostics`, and the workspace tests under both are clean. The
cold dump over 400 depth-5 trees is byte-identical: 34,492 boxes.
2026-09-19 23:51:58 -04:00
iris-ai 7e2b4cd9db Print the box a widget drew in, not the rel base labelled as it
`DrawInfo::px` was the child's rel base in pixels, resolved at all four
construction sites on every draw and read only by three diagnostics --
each of which called it the box: `diag::draw_request`'s `pixel_size`,
printed by `trace_unsettled` as "draw in"; and two `debug_assert`
messages saying "clips to" and "drew in". A rel base and a box differ
wherever a parent hands down part of its own, which is every child of a
span, so all three said something that was not true.

The field is gone and each site reads `region.to_px(window)`, which is
the box it claimed to be printing and costs nothing outside a failing
assert. The trace field is `region_px`. `Placing::window` existed only
to resolve that value and follows it out.

The 23-line counter block inside `try_reuse`'s "outside its range"
branch is `diag::outside`, beside the other diagnostics, so the decision
reads as its six checks.

fmt, workspace clippy under `-D warnings` with and without
`layout-diagnostics`, and the workspace tests under both are clean. The
cold dump over 400 depth-5 trees is byte-identical: 34,492 boxes. The
trace now prints "draw in 189.00x176.00" beside a region 189 by 176.
2026-09-19 23:28:36 -04:00
iris-ai 3da1c71870 Name the values layout carries, and say what a span's slot is
`along` said nothing about what it did. It is `Span::slot` now: the
stretch of the row between two distances from where the span starts
laying out, as a span of its own box, with the mirror for a negative
direction in one place. `far` is `row`, which is what the comment above
it already called it, and `shares` is `has_room` beside the
`any_leftover` it was folded into. `reached` now guards on the leftover
weight it divides by rather than on the numerator that happened to be
zero with it.

The pairs layout returns are named rather than positional: `Answer`
{size, holds} and `Drawn` {answer, drawing_holds} replace
`(Size, LayoutHolds)` and a three-tuple with two `LayoutHolds` in it,
which was the one shape the cold dump exists to catch. `try_reuse`
answers `bool` rather than `Option<()>`, and the four hand-written
copies of `move_idx != parent_move` are `ActiveData::is_region_node`.

`AXES` was declared in three modules; it is `Axis::BOTH`. `rel_min`,
`rel_max` and the unused `select_len` are gone -- `ZERO` and `FULL`
already said those. Three doc comments sat on `impl` blocks instead of
the single method inside them. `reposition` and `redepth` walked their
children by index, looking the parent up again per child; both take the
list and put it back. `Scroll`'s `fixed` and `fixed_len` are
`answer_px` and `answer_is_px`, which says which one is the length.

fmt, workspace clippy under `-D warnings` with and without
`layout-diagnostics`, and the workspace tests are clean. The cold dump
over 400 depth-5 trees is byte-identical to `6c84b6f`: 34,492 boxes,
no seed moved.
2026-09-19 23:24:55 -04:00
iris-aiandClaude Opus 5 6c84b6f2cb Read a slot's ends where they are used, not carry one between children
`start` looked like a third accumulator beside `fixed` and `taken`, carried
across iterations and assigned at three points. It was never independent:
every assignment was `shared(fixed, taken)`, so it was those two read
together. Reading it at each end of a slot instead drops the variable, drops
two of the three calls per child, and leaves the gap added after the last
child deriving nothing -- which was the thing that read as a bug, and is not
one because no end is taken from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:13:52 -04:00
iris-aiandClaude Opus 5 8d2b7a512b Sum a span's cursor by name, and guard a scroll's re-clamp
`cursor` added `px` and `rel` by hand where the placing loop below now says
`fixed += len.without_leftover()` -- the same sum, one of them named. And
`let along = total` shadowed the closure that makes a span along the row,
two meanings for one word in one function; the local said nothing `total`
did not.

A scroll's draw writes `amt` and `snap_end`, so a second draw at another
viewport reads what the first wrote. Warm still matches cold because
re-clamping is idempotent, but nothing said so and nothing checked it: the
seed scans build scrolls and never scroll them. The test scrolls four
distances, one past the end, and widens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:04:39 -04:00
iris-aiandClaude Opus 5 40b89c1f79 Say from_axes, which is what a constructor is called
`per_axis` on `PlaceDesc` and `Declared` builds a pair by asking for each
axis. `from_axis` beside it already names the three-argument form, so the
plural is the one that takes a function, and both follow Rust's convention
for a constructor rather than an invented word.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:53:58 -04:00
iris-aiandClaude Opus 5 4d42f1c8ca Say on_axis for the lift, so it is not indexing's word
`PlaceDescAxis::axis(axis)` shared its name with `PlaceDesc`'s extraction,
which is now `Index<Axis>` and reads `place[axis]`. The two go opposite
directions, so they get different words: `on_axis` pairs with the
`from_axis` it is the shorthand for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:35:22 -04:00
iris-aiandClaude Opus 5 2807a925af Make a declared length one that cannot carry a share
`declared_lens` filtered `leftover` out of both its sources and every
consumer then re-dropped it, so the rule lived in two filters and a comment.
A declaration is a `Len`: `LayoutLen::declared` states the rule once and both
sources go through it, and `Declared` replaces the bare two-element array on
`ActiveData` and in four signatures.

The two sources stay one value deliberately. A rule decides the child's box;
a hint only promises what it will report -- but `size_hint` is by contract an
exact answer with no painter context, and `hints_agree` fails a widget that
draws something else, so narrowing the box to a hint cannot change what is
drawn. Every consumer asks about the length, never which said it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:06:31 -04:00
iris-aiandClaude Opus 5 55df32a33c Say layout's operations by name, and index a pair by its axis
Four rounds over the same idea: an expression that needed a comment to say
what it computed wanted to be a named operation.

The placement description is built by chaining off the value that says it.
`UiSpan::within_desc`/`shifted_desc` and `Len::as_desc` replace the
`PlaceDescAxis::` constructors, `PlaceDescAxis::axis` lifts one axis into a
pair with the whole box across it, and `PlaceDesc::per_axis` covers the case
where the two axes differ. `beside` is dropped: `from_axis` already said it.

Seven module-level functions become methods on the value each took first --
`Widgets::declared_lens`, `LayoutLen::fills`, `PlaceDesc::placement` and
`::rel_base_and_region`, `Size::within_box`, `UiRegion::at_origin` and
`::as_translation`.

`UiSpan::place` is the aligned-placement rule, which was written out three
times; `LayoutLen::without_leftover` is the sibling `apply_leftover` never
had, at six sites; `is_px` and `is_only_leftover` name field comparisons the
surrounding comments had to translate; `Holds::covers` was interval
containment spelled out by hand. A span's `shared` loses the two arguments
that did not vary across its loop.

`LayoutHolds` was four two-element arrays where every other pair here is a
struct of two per-axis values, so nothing it did could be written once.
It becomes `AxisHolds` on `x` and `y`, and `and`, `covers` and `contains`
lose their loops.

Every pair gets `Index<Axis>`/`IndexMut<Axis>` through one macro, and the
eighteen `axis`/`axis_mut` methods go. `const_index` keeps the accessors
usable in const context.

Cold layout is unchanged: `layout_dump` over 400 depth-5 trees is identical
to 58ce74d byte for byte, across all 34,492 boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:56:59 -04:00
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
36 changed files with 2385 additions and 1562 deletions

No files matched your search

+24
View File
@@ -14,3 +14,27 @@ 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
a prepare stage on Event, so Data has no placeholder field
run_sensors builds one CursorData per widget and has to put something in
`sense` before anything knows which sense matched, so it writes
CursorSense::Hovering and says in place that it means nothing;
should_run then clones the whole thing to overwrite that one field
the state is representable only because the type lets the caller say it:
what the caller supplies and what matching adds are two different things
wearing one struct
the awkward part is doing it without the generics getting annoying --
Data<'a> is already a GAT with a default, and splitting it in two adds
another associated type to every Event impl for the sake of one field
(Bryan, 2026-09-20; low priority, he wants a good answer rather than a
quick one)
+102 -98
View File
@@ -15,7 +15,7 @@
//! reuse, size, placement, and text events for one suspicious widget. The //! reuse, size, placement, and text events for one suspicious widget. The
//! selection is a set and survives [`take`] until cleared. //! selection is a set and survives [`take`] until cleared.
use crate::{Axis, LayoutLen, PxVec2, Size, UiRegion, WidgetId}; use crate::{Axis, LayoutHolds, LayoutLen, PxVec2, Size, UiRegion, UiVec2, WidgetId};
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
@@ -23,104 +23,71 @@ use std::{
time::Instant, time::Instant,
}; };
#[derive(Clone, Copy)] /// Declares a counter or timer kind beside the name its report prints. Two
pub(crate) enum Counter { /// lists in the same order was one list too many: a variant inserted without
Updates, /// its label moving with it renames every total after it, and nothing says
DrawRequests, /// so.
WidgetDraws, macro_rules! labelled {
RegionNodeDraws, ($(#[$meta:meta])* $vis:vis enum $Name:ident { $($variant:ident = $label:literal,)* }) => {
SizeReads, $(#[$meta])*
HintHits, #[derive(Clone, Copy)]
HintMisses, $vis enum $Name { $($variant,)* }
RetainedSizeHits,
ReuseAttempts, impl $Name {
ReuseExact, const COUNT: usize = [$($label,)*].len();
ReuseMoved, const NAMES: [&'static str; Self::COUNT] = [$($label,)*];
ReuseDirty, }
ReuseWrongParent, };
ReuseRemapped,
ReuseOutside,
ReuseWrongLayer,
ReuseWrongNode,
PlaceRedraws,
QueuePops,
DepthReads,
LocalRedraws,
SizeChanges,
ReaderEdges,
PrimitiveWrites,
TextRenders,
TextShapeHits,
TextShapes,
TextBreaks,
GlyphPlacements,
OutsidePinnedLen,
OutsideFrame,
OutsideExtent,
} }
impl Counter { labelled! {
const COUNT: usize = Self::OutsideExtent as usize + 1; pub(crate) enum Counter {
Updates = "updates",
const NAMES: [&'static str; Self::COUNT] = [ DrawRequests = "draw requests",
"updates", WidgetDraws = "widget draws",
"draw requests", RegionNodeDraws = "region-node draws",
"widget draws", SizeReads = "draw-result size reads",
"region-node draws", HintHits = "hint hits",
"draw-result size reads", HintMisses = "hint misses",
"hint hits", ReuseAttempts = "reuse attempts",
"hint misses", ReuseExact = "reuse exact",
"retained size hits", ReuseMoved = "reuse moved",
"reuse attempts", ReuseDirty = "reuse: dirty",
"reuse exact", ReuseUndrawn = "reuse: nothing drawn to keep",
"reuse moved", ReuseWrongParent = "reuse: wrong parent",
"reuse: dirty", ReuseRemapped = "reuse remapped",
"reuse: wrong parent", ReuseOutside = "reuse: outside what it holds for",
"reuse remapped", ReuseWrongLayer = "reuse: another layer",
"reuse: outside what it holds for", ReuseWrongNode = "reuse: region-node choice changed",
"reuse: another layer", ReuseWrongMask = "reuse: a different inherited mask",
"reuse: region-node choice changed", QueuePops = "redraw queue pops",
"placed by redrawing", DepthReads = "depth reads",
"redraw queue pops", LocalRedraws = "local redraws",
"depth reads", SizeChanges = "size changes",
"local redraws", ReaderEdges = "reader edges",
"size changes", PrimitiveWrites = "primitive writes",
"reader edges", TextRenders = "text renders",
"primitive writes", TextShapeHits = "text shape hits",
"text renders", TextShapes = "text shapes",
"text shape hits", TextBreaks = "text line breaks",
"text shapes", GlyphPlacements = "glyph placements",
"text line breaks", OutsidePinnedLen = "reuse outside: the length it was pinned to",
"glyph placements", OutsideWindow = "reuse outside: this window",
"reuse outside: the length it was pinned to", OutsideRelBase = "reuse outside: a rel base",
"reuse outside: a frame length", OutsideRegion = "reuse outside: a region length",
"reuse outside: an extent length", }
];
} }
#[derive(Clone, Copy)] labelled! {
pub(crate) enum TimerKind { pub(crate) enum TimerKind {
Update, Update = "update total",
FullLayout, FullLayout = "full layout",
IncrementalLayout, IncrementalLayout = "incremental layout",
TextRender, TextRender = "text render",
TextShape, TextShape = "text shape",
TextBreak, TextBreak = "text line break",
GlyphPlacement, GlyphPlacement = "glyph placement",
} }
impl TimerKind {
const COUNT: usize = Self::GlyphPlacement as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"update total",
"full layout",
"incremental layout",
"text render",
"text shape",
"text line break",
"glyph placement",
];
} }
#[derive(Clone)] #[derive(Clone)]
@@ -255,6 +222,8 @@ pub enum ReuseOutcome {
Dirty, Dirty,
WrongParent, WrongParent,
WrongLayer, WrongLayer,
WrongMask,
WrongNode,
Remapped, Remapped,
Outside, Outside,
Undrawn, Undrawn,
@@ -268,7 +237,7 @@ pub enum TraceEvent {
id: WidgetId, id: WidgetId,
parent: Option<WidgetId>, parent: Option<WidgetId>,
region: UiRegion, region: UiRegion,
pixel_size: PxVec2, region_px: PxVec2,
region_node: bool, region_node: bool,
}, },
Reuse { Reuse {
@@ -364,7 +333,7 @@ pub(crate) fn draw_request(
id: WidgetId, id: WidgetId,
parent: Option<WidgetId>, parent: Option<WidgetId>,
region: UiRegion, region: UiRegion,
pixel_size: PxVec2, region_px: PxVec2,
region_node: bool, region_node: bool,
) { ) {
trace( trace(
@@ -373,7 +342,7 @@ pub(crate) fn draw_request(
id, id,
parent, parent,
region, region,
pixel_size, region_px,
region_node, region_node,
}, },
); );
@@ -383,6 +352,41 @@ pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
trace(id, TraceEvent::Reuse { id, outcome }); trace(id, TraceEvent::Reuse { id, outcome });
} }
/// A drawing that cannot be reused because the box on offer is outside what
/// it holds for, and which of the four contracts said so. They overlap: a
/// drawing can be outside two of them at once, and counting each is what
/// says where a rel base redrawing more than it should is coming from.
pub(crate) fn outside(
id: WidgetId,
holds: LayoutHolds,
region: UiRegion,
rel_base: UiVec2,
window: PxVec2,
) {
for axis in Axis::BOTH {
let holds = holds[axis];
let len = region[axis].len();
let window = window[axis];
if holds.region_len.is_some_and(|pinned| pinned != len) {
bump(Counter::OutsidePinnedLen);
}
if !holds.window.contains(window) {
bump(Counter::OutsideWindow);
}
if holds
.rel_base
.is_some_and(|pinned| pinned != rel_base[axis])
{
bump(Counter::OutsideRelBase);
}
if !holds.region.contains(len.to_px(window)) {
bump(Counter::OutsideRegion);
}
}
bump(Counter::ReuseOutside);
reuse(id, ReuseOutcome::Outside);
}
pub(crate) fn size_reported(id: WidgetId, size: Size) { pub(crate) fn size_reported(id: WidgetId, size: Size) {
trace(id, TraceEvent::SizeReported { id, size }); trace(id, TraceEvent::SizeReported { id, size });
} }
+1
View File
@@ -9,6 +9,7 @@
#![feature(unsize)] #![feature(unsize)]
#![feature(coerce_unsized)] #![feature(coerce_unsized)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#![feature(const_index)]
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
pub mod layout_diagnostics; pub mod layout_diagnostics;
+3 -22
View File
@@ -1,3 +1,4 @@
use crate::util::impl_axis_index;
use crate::{Px, Rel}; use crate::{Px, Rel};
use super::*; use super::*;
@@ -83,28 +84,6 @@ pub struct RegionAlign {
pub y: AxisAlign, pub y: AxisAlign,
} }
impl RegionAlign {
/// Both axes at the near edge: the start of a box in its own orientation.
pub const NEAR: Self = Self {
x: AxisAlign::NEG,
y: AxisAlign::NEG,
};
pub fn axis(&self, axis: Axis) -> AxisAlign {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
impl RegionAlign { impl RegionAlign {
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG); pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG); pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
@@ -231,3 +210,5 @@ impl RegionAlign {
UiVec2::from(self) UiVec2::from(self)
} }
} }
impl_axis_index!(RegionAlign => AxisAlign);
+6 -36
View File
@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::util::impl_axis_index;
use crate::{Fixed, FixedVec2}; use crate::{Fixed, FixedVec2};
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
@@ -8,14 +9,8 @@ pub enum Axis {
} }
impl Axis { impl Axis {
/// A per-axis pair with `aligned` on this axis and `ortho` on the other, /// Both of them, for the layout code that asks the same question of each.
/// which is what `from_axis` does for a vector. pub const BOTH: [Self; 2] = [Self::X, Self::Y];
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 {
@@ -53,20 +48,6 @@ pub enum Sign {
} }
impl<const SHIFT: u32> FixedVec2<SHIFT> { impl<const SHIFT: u32> FixedVec2<SHIFT> {
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self { pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
match axis { match axis {
Axis::X => Self::new(aligned, ortho), Axis::X => Self::new(aligned, ortho),
@@ -76,20 +57,6 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
} }
impl Vec2 { impl Vec2 {
pub fn axis(&self, axis: Axis) -> f32 {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut f32 {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self { pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
Self { Self {
x: match axis { x: match axis {
@@ -148,3 +115,6 @@ impl<T> BothAxis<T> {
} }
} }
} }
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
impl_axis_index!(Vec2 => f32);
+32 -15
View File
@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::util::impl_axis_index;
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op}; use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq)]
@@ -118,20 +119,6 @@ impl Size {
}, },
} }
} }
pub fn axis(&self, axis: Axis) -> LayoutLen {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl LayoutLen { impl LayoutLen {
@@ -158,11 +145,39 @@ impl LayoutLen {
Len::from_parts(self.rel.add(share), self.px) Len::from_parts(self.rel.add(share), self.px)
} }
/// Only pixels: the same number of them whatever box it lands in, and
/// whatever anyone else in the row asks for. A length that is any part
/// of a box or of what is left over is not one.
pub fn is_px(self) -> bool {
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
}
/// Nothing but a claim on what is left over, so there is no length here
/// at all where nothing is.
pub fn is_only_leftover(self) -> bool {
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
}
/// This as a length of a box, where it is one. `leftover` is not: a
/// share of what is left over is a length only to whoever divides one,
/// so it passes up in the reported size instead and is resolved there.
pub fn declared(self) -> Option<Len> {
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
}
/// What this takes whatever is left over: the reading of a length for
/// anyone not dividing a box between siblings, where a share is a claim
/// on someone else's room rather than a length of its own.
/// [`Self::apply_leftover`] is the opposite reading of the same value.
pub const fn without_leftover(self) -> Len {
Len::from_parts(self.rel, self.px)
}
/// This length, given as a part of a box `len` long, as a part of the /// This length, given as a part of a box `len` long, as a part of the
/// box `len` is itself a part of. The share is untouched: it is a claim /// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything. /// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self { pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len); let part = self.without_leftover().within_len(len);
Self { Self {
px: part.px, px: part.px,
rel: part.rel, rel: part.rel,
@@ -236,3 +251,5 @@ impl std::fmt::Display for LayoutLen {
Ok(()) Ok(())
} }
} }
impl_axis_index!(Size => LayoutLen);
+13 -40
View File
@@ -1,3 +1,4 @@
use crate::util::impl_axis_index;
use std::{fmt::Display, marker::Destruct}; use std::{fmt::Display, marker::Destruct};
use super::*; use super::*;
@@ -61,20 +62,6 @@ impl UiVec2 {
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub fn axis(&self, axis: Axis) -> Len {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
/// Resolved against a box of `size`, which is where a fraction stops /// Resolved against a box of `size`, which is where a fraction stops
/// being one and becomes a place. /// being one and becomes a place.
pub fn to_px(&self, size: PxVec2) -> PxVec2 { pub fn to_px(&self, size: PxVec2) -> PxVec2 {
@@ -176,14 +163,6 @@ impl Len {
Self::from_parts(Rel::ZERO, Px::from_f32(px)) Self::from_parts(Rel::ZERO, Px::from_f32(px))
} }
pub const fn rel_min() -> Self {
Self::ZERO
}
pub const fn rel_max() -> Self {
Self::FULL
}
pub const fn max(&self, other: Self) -> Self { pub const fn max(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.max(other.rel), rel: self.rel.max(other.rel),
@@ -226,10 +205,6 @@ impl Len {
}) })
} }
pub fn select_len(&self, len: Len) -> Self {
len.within_len(*self)
}
pub const fn flip(&mut self) { pub const fn flip(&mut self) {
self.rel = Rel::ONE.sub(self.rel); self.rel = Rel::ONE.sub(self.rel);
self.px = self.px.neg(); self.px = self.px.neg();
@@ -294,6 +269,15 @@ impl UiSpan {
} }
} }
/// A box `len` long inside this one, on the side `align` says. Both must
/// be lengths of the same rel base: it subtracts one from the other
/// rather than composing it in, which is what keeps a fraction the same
/// fraction however long this box turns out to be.
pub const fn place(self, len: Len, align: AxisAlign) -> Self {
let start = self.start + (self.len() - len).scale(align.rel());
Self::new(start, start + len)
}
pub const fn len(&self) -> Len { pub const fn len(&self) -> Len {
self.end - self.start self.end - self.start
} }
@@ -348,20 +332,6 @@ impl UiRegion {
y: self.y.within(&parent.y), y: self.y.within(&parent.y),
} }
} }
pub const fn axis(&self, axis: Axis) -> &UiSpan {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut UiSpan {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn flip(&mut self, axis: Axis) { pub const fn flip(&mut self, axis: Axis) {
match axis { match axis {
Axis::X => self.x.flip(), Axis::X => self.x.flip(),
@@ -462,3 +432,6 @@ impl Display for PixelRegion {
write!(f, "{} -> {}", self.top_left, self.bot_right) write!(f, "{} -> {}", self.top_left, self.bot_right)
} }
} }
impl_axis_index!(UiVec2 => Len);
impl_axis_index!(UiRegion => UiSpan);
+49 -46
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, Size, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
TextureHandle, UiRegion, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,35 +9,34 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// Its frame in `parent_move`'s coordinates: what a fraction it declares /// Where its drawing goes, in its region node's coordinates.
/// or reports is a fraction of, composed. Everything it draws sits inside pub placement: UiRegion,
/// this by way of `extent`. /// What a fraction declared or reported under this widget is a fraction
pub frame_abs: UiRegion, /// of, as a length of the window.
/// Where its drawing goes, in the frame's own coordinates. pub rel_base: UiVec2,
pub extent: UiRegion, /// Where its drawing was put, and where it was asked. The two differ
/// That frame in its parent's frame coordinates, before composition: /// where a container asks in one place and puts the answer in another --
/// forwarded whole by a transparent container, narrowed by a declared /// a row measures from its cursor and puts the child in its slot. Each
/// length. Its length is the same on every ask, which is what /// carries the rel base that ask stated, so asking again from either is
/// a local redraw relies on to ask its parent's own question again. /// the same question it was.
pub frame: UiRegion, pub placed: PlaceDesc,
/// What of its parent's extent the drawing was given, and what it was pub asked: PlaceDesc,
/// given at the parent's first ask of it -- the question a cold layout /// The box it was asked in, in the parent's region-node coordinates: the
/// asks. A part is a length from the extent's start, so an extent that /// box its drawing was made in and the one its contract is about. Its
/// moved re-places every child by re-adding that start. /// drawing is placed elsewhere by re-expression, never by asking again.
pub place: [Place; 2], pub region: UiRegion,
pub offer_place: [Place; 2],
/// The box that ask gave it, in its frame's coordinates. Kept rather
/// than worked out again from where its parent's own box is now: a
/// parent drawn again in the box its own answer chose gives its children
/// boxes it never measured anything in, and the measurement this widget
/// answered is the one its parent's layout was built on.
pub offer_part: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or /// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet. /// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>, pub answer: Option<Answer>,
/// What the widget said it used of its frame, the last time it drew. /// Asked more than once in its parent's last draw -- measured in one box
/// and then asked in the one the parent decided. The parent's layout
/// rests on the first answer and its drawing on the last, so only the
/// parent can ask either again.
pub re_asked: bool,
/// What the widget reported, in window-unit lengths.
pub size: Size, pub size: Size,
/// The frame and extent reads that this drawing holds for. /// The window and region reads that this drawing holds for, and the
/// rel base and region it pinned.
pub holds: LayoutHolds, pub holds: LayoutHolds,
pub drawn: bool, pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
@@ -47,23 +46,23 @@ pub struct ActiveData {
pub depth: usize, pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
/// Its primitives, each keeping the box it was written in -- in this /// Its primitives, each keeping the box it was written in -- in this
/// widget's extent coordinates, which is what a move recomposes from. /// widget's placement coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>, pub primitives: Vec<RetainedPrimitive>,
/// An owned mask holds one reference independently of its primitives.
pub mask_region: Option<UiRegion>, pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
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 frame. /// 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: Declared,
/// 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 `frame_abs` 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.
@@ -77,20 +76,24 @@ pub struct ActiveData {
} }
impl ActiveData { impl ActiveData {
/// What it answered when its parent measured it, where it has been /// What it answered when its parent asked, where it has been asked at
/// measured at all. Not `size`, which is what its last drawing reported: /// all. Not `size`, which is what its last drawing reported: a drawing
/// a drawing made in the box that answer chose is answering a different /// re-expressed in the box that answer chose is not a second answer.
/// question.
pub fn measured(&self) -> Option<Size> { pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size) self.answer.map(|answer| answer.size)
} }
/// Whether what it answered still stands for a frame of these pixel /// Whether it owns a region node rather than sharing the one it was drawn
/// lengths. The answer was given in the box its parent first asked /// under, which is what its two move indices being different says.
/// about, which is what it is checked against -- `holds` on the record pub fn is_region_node(&self) -> bool {
/// is about the box the answer then chose. self.move_idx != self.parent_move
pub fn answers_at(&self, px: crate::PxVec2, part: UiRegion) -> bool {
self.answer
.is_some_and(|(_, holds)| holds.contains(px, part))
} }
} }
/// What a widget answered when it was asked: the size it reported, and the
/// boxes and windows that answer holds for.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Answer {
pub size: Size,
pub holds: LayoutHolds,
}
+7 -1
View File
@@ -29,10 +29,16 @@ impl Holds {
Self { lo: len, hi: len } Self { lo: len, hi: len }
} }
pub const fn contains(&self, len: Px) -> bool { pub const fn contains(self, len: Px) -> bool {
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw() len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
} }
/// Every length `other` holds for is one this holds for, so a drawing
/// made under this range is still good wherever `other` is.
pub const fn covers(self, other: Self) -> bool {
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
}
pub const fn and(self, other: Self) -> Self { pub const fn and(self, other: Self) -> Self {
Self { Self {
lo: self.lo.max(other.lo), lo: self.lo.max(other.lo),
+88 -43
View File
@@ -1,63 +1,108 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion}; use crate::util::impl_axis_index;
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2};
const AXES: [Axis; 2] = [Axis::X, Axis::Y]; /// What one evaluation of a widget depends on along one axis: the window
/// lengths its reads hold for, the pixel lengths of its own box, and the
/// What one evaluation of a widget depends on: the pixel lengths of its /// symbolic lengths of that box and of its rel base where either one is what
/// frame and of its own box that its drawing and its answer hold for, and /// it was expressed in.
/// the symbolic length of its own box where it read one.
/// ///
/// The symbolic length is a pin rather than a range: a container places its /// The symbolic lengths are pins rather than ranges: a container places its
/// children as lengths of its frame measured from where its own box starts, /// 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 /// so what it draws turns on that box's length and on nothing about where it
/// is. It reaches the parent only where the box it pinned is the parent's /// is. A box pin reaches the parent only where the box it pinned is the
/// own; anywhere else the parent chose that length itself, and a widget /// parent's own; anywhere else the parent chose that length itself, and a
/// pinned this way is checked when it is re-placed. /// 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 AxisHolds {
pub window: Holds,
pub rel_base: Option<Len>,
pub region: Holds,
pub region_len: Option<Len>,
}
impl AxisHolds {
pub const ANY: Self = Self {
window: Holds::ANY,
rel_base: None,
region: Holds::ANY,
region_len: None,
};
pub fn and(self, other: Self) -> Self {
// Two pins of the same length disagreeing would mean one drawing was
// a fraction of two different lengths at once.
debug_assert!(
self.region_len.is_none()
|| other.region_len.is_none()
|| self.region_len == other.region_len
);
debug_assert!(
self.rel_base.is_none() || other.rel_base.is_none() || self.rel_base == other.rel_base
);
Self {
window: self.window.and(other.window),
rel_base: self.rel_base.or(other.rel_base),
region: self.region.and(other.region),
region_len: self.region_len.or(other.region_len),
}
}
pub fn covers(self, other: Self) -> bool {
self.window.covers(other.window)
&& self.region.covers(other.region)
&& self
.region_len
.is_none_or(|len| other.region_len == Some(len))
&& self.rel_base.is_none_or(|len| other.rel_base == Some(len))
}
/// Whether a widget in a box `len` long, with that rel base, in that
/// window, is one this drawing holds for.
pub fn contains(self, window: Px, rel_base: Len, len: Len) -> bool {
self.window.contains(window)
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
&& self.region.contains(len.to_px(window))
&& self.region_len.is_none_or(|pinned| pinned == len)
}
}
/// [`AxisHolds`] on both axes. Every question asked of it is asked of one
/// axis at a time, since a widget that read one length holds for any length
/// of the other.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds { pub struct LayoutHolds {
pub frame: [Holds; 2], pub x: AxisHolds,
pub extent: [Holds; 2], pub y: AxisHolds,
pub extent_len: [Option<Len>; 2],
} }
impl LayoutHolds { impl LayoutHolds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
frame: [Holds::ANY; 2], x: AxisHolds::ANY,
extent: [Holds::ANY; 2], y: AxisHolds::ANY,
extent_len: [None; 2],
}; };
pub fn and(self, other: Self) -> Self { pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY; Self {
for n in 0..2 { x: self.x.and(other.x),
result.frame[n] = self.frame[n].and(other.frame[n]); y: self.y.and(other.y),
result.extent[n] = self.extent[n].and(other.extent[n]);
debug_assert!(
self.extent_len[n].is_none()
|| other.extent_len[n].is_none()
|| self.extent_len[n] == other.extent_len[n]
);
result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]);
} }
result
} }
pub fn covers(self, other: Self) -> bool { pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| { self.x.covers(other.x) && self.y.covers(other.y)
self.frame[n].lo <= other.frame[n].lo
&& self.frame[n].hi >= other.frame[n].hi
&& self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi
&& self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len))
})
} }
pub fn contains(self, px: PxVec2, extent: UiRegion) -> bool { pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
AXES.into_iter().all(|axis| { Axis::BOTH
let n = axis as usize; .into_iter()
let len = extent.axis(axis).len(); .all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
self.frame[n].contains(px.axis(axis))
&& self.extent[n].contains(len.to_px(px.axis(axis)))
&& self.extent_len[n].is_none_or(|pinned| pinned == len)
})
} }
} }
impl_axis_index!(LayoutHolds => AxisHolds);
+1 -1
View File
@@ -20,7 +20,7 @@ pub use active::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*; pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use place::*; pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive};
pub use render_state::*; pub use render_state::*;
#[derive(Default)] #[derive(Default)]
+364 -383
View File
@@ -1,72 +1,54 @@
#[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, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, RenderedText, Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::{
place::{PlaceSpan, RelBase},
render_state::{DrawInfo, Placing},
},
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// makes your surfaces look pretty /// makes your surfaces look pretty
pub struct Painter<'a> { 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,
/// What a fraction this widget declares or reports is a fraction of, in /// This widget's rel base, per axis: a length of the window, and what a
/// the coordinates of `move_idx`: forwarded from its parent unchanged /// fraction it or anything under it declares or reports is a fraction
/// through a span, a stack or a scroll, and narrowed only by what was /// of. A length rather than a box, so padding can take from both the
/// decided above it -- a declared length, or the root. Its length /// rel base and the box without either becoming the other.
/// is the same on every ask of the widget, which is what keeps a fraction pub(super) rel_base: UiVec2,
/// under it from being resolved twice. /// The box this widget was asked in, in its region node's coordinates:
pub(super) frame: UiRegion, /// what it draws in, and what its children's places are parts of.
/// Where this widget's drawing goes, in the frame's own coordinates. pub(super) region: UiRegion,
/// Everything it writes is in these coordinates, and its children are /// The window in pixels. Frames and boxes become pixels against this one
/// placed as parts of it. /// unit, regardless of region-node boundaries.
pub(super) extent: UiRegion, pub(super) window: PxVec2,
/// The extent's symbolic length where this draw read it, which makes the
/// drawing one that holds for that length alone -- the way reading a
/// length in pixels makes it hold for that number of pixels.
pub(super) extent_len: [Option<Len>; 2],
/// Symbolic box lengths read only to compute the answer. A container can
/// replace the provisional drawings used for that answer with drawings
/// in decided boxes, so this contract is independent of the final one.
pub(super) answer_extent_len: [Option<Len>; 2],
/// The frame in pixels, which its children's frames are a length of:
/// threaded down rather than composed back up the chain, so every length
/// in layout is one multiply from its parent's and [`Holds::through`]
/// inverts exactly that.
pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<RetainedPrimitive>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<UiRegion>, 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. /// Only children whose answers were read constrain this widget's answer.
pub(super) answer_under: LayoutHolds, pub(super) answer_under: LayoutHolds,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first place each was asked in
/// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>,
/// Whether this draw is at the place its parent first asked about, 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 frame 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) frame_own: [Holds; 2], /// reads one, then that one unless it says otherwise, and the rel base or
/// The same for its own box. /// region length it read symbolically, each of which makes the drawing
pub(super) extent_own: [Holds; 2], /// hold for that length alone.
/// Pixel-box dependencies used only to compute the answer. These do not pub(super) own: LayoutHolds,
/// constrain a retained drawing placed inside that answer. /// What each child's drawing depends on. Asking a child again replaces
pub(super) answer_extent_own: [Holds; 2], /// its drawing, so it replaces this too rather than narrowing it.
/// The final drawing kept for each child. Asking one child again replaces
/// its provisional drawing and therefore replaces this contract too.
pub(super) under: Vec<(WidgetId, LayoutHolds)>, 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.
@@ -90,12 +72,10 @@ impl<'a> Painter<'a> {
self.write_resolved(kind, primitive, region, self.resolve(region)); self.write_resolved(kind, primitive, region, self.resolve(region));
} }
/// A box in this widget's extent coordinates, composed into the /// A box in this widget's region, composed into its region node's
/// coordinates its move slot is in: through the extent, then through the /// coordinates.
/// frame the extent is a part of. The same two steps a recomposition
/// replays, so a moved drawing lands where a cold one does.
fn resolve(&self, region: UiRegion) -> UiRegion { fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent).within(&self.frame) region.within(&self.region)
} }
fn write_resolved<P: Primitive>( fn write_resolved<P: Primitive>(
@@ -123,7 +103,6 @@ impl<'a> Painter<'a> {
fn push_primitive(&mut self, h: RetainedPrimitive) { 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);
@@ -148,75 +127,83 @@ impl<'a> Painter<'a> {
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
let resolved = self.resolve(region); let resolved = self.resolve(region);
let move_idx = self.move_idx; let move_idx = self.move_idx;
self.mask = self.rsc.ui_mut().masks.push(Mask { let mask = Mask {
region: resolved, region: resolved,
move_idx, 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 in the whole of this widget's own box, with the frame /// Draws a widget in the whole of this widget's own box, with the rel
/// forwarded unchanged: what a container that is only a wrapper around /// base forwarded unchanged: what a container that is only a wrapper
/// one child wants, and what every transparent container passes for the /// around one child wants.
/// frame.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, UiRegion::FULL, [Place::Within(Part::All); 2]) self.widget_at(id, UiRegion::FULL)
} }
/// Draws a child, saying what its fractions are of and where its drawing /// Resolves what the place says about the child's rel base into a length,
/// goes. /// where that is this widget's own narrowed the way the region is. An
/// axis the region leaves whole is not read at all, so a wrapper that
/// only moves its child does not pin its drawing to a rel base.
fn resolve_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
for axis in Axis::BOTH {
let at = &mut place[axis];
let (RelBase::WithRegion, PlaceSpan::Within(span)) = (at.rel_base, at.span) else {
continue;
};
let len = span.len();
at.rel_base = match len == Len::FULL {
true => RelBase::Inherit,
false => RelBase::Len(len.within_len(self.rel_base(axis))),
};
}
place
}
/// Asks a child, saying what its fractions are of and where it is asked.
/// ///
/// `frame` is that reference, in this widget's own frame coordinates: /// `place` says where the child goes and what its fractions are of:
/// [`UiRegion::FULL`] forwards this widget's frame, which is what a /// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which
/// container that only divides room passes, so a fraction under it means /// is a box of this widget's own with the answer placed inside it.
/// the same wherever it sits and however deeply it is nested. Narrowing
/// it is for what is decided from above, and a declared length narrows
/// it here.
/// ///
/// `place` is where the drawing goes, per axis, as a part of this /// The child draws once, in the region that comes of it, and its answer
/// widget's extent: see [`Place`]. A narrowed frame is its own extent, /// is placed inside that region by re-expressing the drawing. Nothing is
/// since the narrowing is what said where the drawing goes. /// drawn again in a box an answer chose; a container that puts the
/// answer somewhere else says so with [`Self::place_at`].
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>,
frame: UiRegion, place: impl Into<PlaceDesc>,
place: [Place; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let place = self.resolve_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());
let (local, extent) = frame_and_extent( let (rel_base, region) =
frame, place.rel_base_and_region(self.region, self.rel_base, declared, align);
part_of(self.extent, place),
narrowed_by(declared, frame),
align,
);
let within = match local == UiRegion::FULL {
true => self.frame,
false => local.within(&self.frame),
};
#[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 drawn = self.state.draw_inner(
let offer_place = if first_ask {
place
} else {
self.state
.active
.get(&id.id())
.map_or(place, |a| a.offer_place)
};
let px = local.size().to_px(self.px);
// The answer and what it holds for, both about the place 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, answer_holds, holds) = self.state.draw_inner(
id.id(), id.id(),
DrawInfo { DrawInfo {
layer: self.layer, layer: self.layer,
@@ -225,18 +212,17 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
frame: local, rel_base,
frame_abs: within, region,
part: extent, placed: place,
place, asked: place,
offer_place, re_asked,
px,
}, },
None, None,
self.rsc, self.rsc,
); );
let compose = |holds| in_parent(holds, local, extent, place, declared); let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
let holds = compose(holds); let answer_holds = self.in_parent(drawn.answer.holds, region, place, declared);
match self.under.iter_mut().find(|(child, _)| *child == id.id()) { match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
Some((_, kept)) => *kept = holds, Some((_, kept)) => *kept = holds,
None => self.under.push((id.id(), holds)), None => self.under.push((id.id(), holds)),
@@ -244,8 +230,8 @@ impl<'a> Painter<'a> {
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size: in_parent_frame(size, local.size(), declared), size: drawn.answer.size,
answer_holds: compose(answer_holds), answer_holds,
} }
} }
@@ -258,53 +244,95 @@ impl<'a> Painter<'a> {
self.state.undraw_rec(id.id(), self.rsc); self.state.undraw_rec(id.id(), self.rsc);
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// Puts a child in `place` of this widget's box, where that box is the
/// it resolves into its frame. Reading them depends on nothing -- the box /// answer the child already gave: the drawing is re-expressed there
/// that comes of them is kept on the child, and `redraw` compares it /// rather than made again -- what a row does once it knows every slot,
/// there. /// having measured each child from its cursor.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] { ///
declared_lens(self.rsc.widgets(), id.id()) /// 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.resolve_rel_base(place.into());
let states_rel_base = Axis::BOTH
.iter()
.any(|&axis| matches!(place[axis].rel_base, RelBase::Len(_)));
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,
}
} }
/// What a child says its length is without being drawn, if it can say. /// This widget as the thing its children are placed within.
/// Asking counts as reading its size. fn placing(&self) -> Placing {
Placing {
id: self.id,
region: self.region,
rel_base: self.rel_base,
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>) -> Declared {
self.rsc.widgets().declared_lens(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
// widget would draw, so it has to win over what the widget says too. // widget would draw, so it has to win over what the widget says too.
let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| { let hint = widgets.size_rules(id.id())[axis].exact().or_else(|| {
widgets widgets
.get_dyn(id.id()) .get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis)) .and_then(|widget| widget.size_hint(axis))
}); });
let rel_base = self.rel_base[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[axis].rel_base = Some(rel_base);
} }
} }
} resolved
/// Whether this is the first box a child is asked about in during a draw
/// that is itself the one its parent measured -- the question a cold
/// layout asks, whose answer is the one to keep. A drawing made again in
/// a box chosen from an answer asks about that box instead, and what it
/// hears back is not a measurement of anything.
fn offer(&mut self, child: WidgetId) -> bool {
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>) {
@@ -325,11 +353,11 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// Writes glyphs in the selected frame or extent coordinates. /// 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. // Glyph offsets and sizes are pixels, which compose additively.
// Only the shared origin needs composing through the extent. // Only the shared origin needs composing through the region.
let resolved = self.resolve(origin); 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() {
@@ -361,39 +389,28 @@ impl<'a> Painter<'a> {
} }
/// The symbolic length of this widget's own box along one axis, in the /// The symbolic length of this widget's own box along one axis, in the
/// lengths of its frame that it places its children in. Reading it pins /// lengths of its rel base that it places its children in. Reading it pins
/// the drawing to that length -- and to nothing about where the box /// the drawing to that length -- and to nothing about where the box
/// starts, which is what lets a container move without being drawn /// starts, which is what lets a container move without being drawn
/// again. One axis at a time, because a container that divides one axis /// again. One axis at a time, because a container that divides one axis
/// holds for any length of the other. /// holds for any length of the other.
pub fn extent_len(&mut self, axis: Axis) -> Len { pub fn region_len(&mut self, axis: Axis) -> Len {
let len = self.extent.axis(axis).len(); let len = self.region[axis].len();
self.extent_len[axis as usize] = Some(len); self.own[axis].region_len = Some(len);
self.answer_extent_len[axis as usize] = Some(len);
len len
} }
/// The symbolic length used to compute this widget's answer, where the /// This widget's rel base along one axis: what a fraction it or anything
/// final drawing itself is rebuilt without depending on that length. /// under it declares or reports is a fraction of. A container reads it
pub fn answer_extent_len(&mut self, axis: Axis) -> Len { /// to hand a length of it down -- padding, which takes its pixels off.
let len = self.extent.axis(axis).len(); /// Reading it pins the drawing to that rel base, the way
self.answer_extent_len[axis as usize] = Some(len); /// [`Self::region_len`] pins it to the box.
pub fn rel_base(&mut self, axis: Axis) -> Len {
let len = self.rel_base[axis];
self.own[axis].rel_base = Some(len);
len len
} }
/// Says that the final drawing uses a symbolic length already read for
/// the answer.
pub fn drawing_uses_extent_len(&mut self, axis: Axis, len: Len) {
debug_assert_eq!(self.extent.axis(axis).len(), len);
self.extent_len[axis as usize] = Some(len);
}
/// A part of this widget's box, expressed in its frame coordinates so it
/// can be used as a child frame decided here.
pub fn extent_part(&self, axis: Axis, part: Part) -> UiSpan {
part.of(*self.extent.axis(axis))
}
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
/// widget that positions its own content reads it to place that content /// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget. /// the way the box around it would have placed the widget.
@@ -411,10 +428,7 @@ impl<'a> Painter<'a> {
/// worth anything, since reading one is also what makes its own size /// worth anything, since reading one is also what makes its own size
/// depend on it. /// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool { pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc self.rsc.widgets().size_rules(self.id)[axis]
.widgets()
.size_rules(self.id)
.axis(axis)
.exact() .exact()
.is_some() .is_some()
} }
@@ -425,35 +439,16 @@ impl<'a> Painter<'a> {
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y)) PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
} }
/// This widget's own box in pixels, used only to compute its answer.
pub fn answer_px_size(&mut self) -> PxVec2 {
PxVec2::new(
self.answer_px_len(Axis::X),
self.answer_px_len(Axis::Y),
)
}
/// One axis of this widget's own box in pixels. Prefer this to /// 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 part = self.extent.axis(axis).len(); let len = self.region[axis].len();
let len = part.to_px(self.px.axis(axis)); let px = len.to_px(self.window[axis]);
let own = &mut self.extent_own[axis as usize]; let own = &mut self.own[axis].region;
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(px);
} }
len px
}
/// One pixel length used only to compute this widget's answer. The final
/// drawing may be retained when that answer is placed in another box.
pub fn answer_px_len(&mut self, axis: Axis) -> Px {
let len = self.extent.axis(axis).len().to_px(self.px.axis(axis));
let own = &mut self.answer_extent_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
} }
/// The lengths of this widget's own box on `axis` that what it is drawing /// The lengths of this widget's own box on `axis` that what it is drawing
@@ -461,41 +456,45 @@ impl<'a> Painter<'a> {
/// of the box, and the same reported size. A widget that read its length /// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise. /// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) { pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let part = self.extent.axis(axis).len(); let len = self.region[axis].len();
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))), holds.contains(len.to_px(self.window[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.extent_own[axis as usize] = holds; self.own[axis].region = holds;
} }
/// One axis of this widget's frame in pixels -- what a fraction of its /// A window length in pixels, which is what every length in layout is
/// area resolves against, and so what a container divides among its /// measured in. Reading one pins the drawing to this window wherever the
/// children. Its own box is a part of this one. /// length is a fraction of it; one that is only pixels is that many
pub fn frame_px_len(&mut self, axis: Axis) -> Px { /// pixels in any window and pins nothing.
let len = self.px.axis(axis); pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let own = &mut self.frame_own[axis as usize]; let window = self.window[axis];
if *own == Holds::ANY { if len.rel != Rel::ZERO {
*own = Holds::at(len); let own = &mut self.own[axis].window;
if *own == Holds::ANY {
*own = Holds::at(window);
}
} }
len len.to_px(window)
} }
/// [`Self::holds`] stated about the frame rather than about this /// The windows this drawing holds for, stated rather than taken: a
/// widget's own box, for a container whose drawing turns on what its /// container that branched on a length in pixels says which side of the
/// fractions are of rather than on the part of it it took. /// boundary it was on, which is wider than the one window reading that
pub fn frame_holds(&mut self, axis: Axis, holds: impl Into<Holds>) { /// length pins, and replaces it.
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.px.axis(axis)), holds.contains(self.window[axis]),
"'{}' ({:?}) says its drawing holds for lengths that leave out its frame", "'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(), self.label(),
self.id self.id
); );
self.frame_own[axis as usize] = holds; self.own[axis].window = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -554,7 +553,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
} }
pub fn len(self, axis: Axis) -> LayoutLen { pub fn len(self, axis: Axis) -> LayoutLen {
self.size().axis(axis) self.size()[axis]
} }
} }
@@ -583,187 +582,169 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// What a child depends on, said about the boxes the widget that drew it impl Painter<'_> {
/// has rather than the ones the child was given. /// Moves what a child depends on into this widget's own terms, taking
/// /// only what the child was asked with.
/// `frame` is the child's frame in this widget's frame coordinates and ///
/// `extent` the box it was given, in the child's own frame coordinates. Both /// Window ranges are already about the one unit and combine directly.
/// reach it as one length, so what it holds for maps back through that /// A rel base pin becomes this widget's own rel base wherever a length of it
/// length exactly -- and where the box it was given is this widget's own, /// is what reached the child; where only pixels did, no length of this
/// what it says about that box is what this widget can say about its own. /// rel base can change the child's and the pin stops here.
pub(crate) fn in_parent( ///
holds: LayoutHolds, /// A child's validity maps back through the part of this widget's box,
frame: UiRegion, /// where the box the child was asked in is that part; a declared length
extent: UiRegion, /// places the box inside the part instead, and then only that length
place: [Place; 2], /// reaches the child. A narrowed rel base is not one of these: it decides
declared: [Option<LayoutLen>; 2], /// what fractions under the child mean and leaves the box the part it
) -> LayoutHolds { /// was given.
let mut result = LayoutHolds::ANY; fn in_parent(
for axis in AXES { &self,
let n = axis as usize; holds: LayoutHolds,
let frame_len = frame.axis(axis).len(); region: UiRegion,
result.frame[n] = holds.frame[n].through(frame_len); place: PlaceDesc,
match (place[n].part(), declared[n]) { declared: Declared,
// Its box is this widget's own, or a part of it in that box's ) -> LayoutHolds {
// own lengths: so what it holds for is a range on this widget's let mut result = LayoutHolds::ANY;
// own box, which is what lets that box move without a redraw. A for axis in Axis::BOTH {
// length it pinned is this widget's length wherever the part is let declared = declared[axis];
// the whole of it, and pins the same way. let holds = holds[axis];
(Part::All, None) if *frame.axis(axis) == UiSpan::FULL => { let at = place[axis];
result.extent[n] = holds.extent[n]; let result = &mut result[axis];
result.extent_len[n] = holds.extent_len[n]; // Every read became pixels against the window, so a range on
} // it is already in this widget's terms.
// Its box is a part of this widget's own box, in that box's own result.window = holds.window;
// lengths, so what it holds for maps back through that part into // A length this widget named -- a resolved share, a box a sibling
// a range on this widget's box. // decided, a box it sized outright, which is its own base -- is
(Part::Of(span), None) => { // not a length of this widget's rel base, so a pin on it stops
result.extent[n] = holds.extent[n].through(span.len()); // here. So does a declaration in pixels: no length of either base
} // is in it to see.
// Its box is a part of this widget's frame: a length of the let reaches = !matches!(at.rel_base, RelBase::Len(_))
// frame is all that reaches it, so what it holds for is a range && declared.is_none_or(|len| len.rel != Rel::ZERO);
// on the frame and none of it on this widget's own box. result.rel_base = holds.rel_base.and(reaches.then(|| self.rel_base[axis]));
_ => { match (at.span, declared) {
result.frame[n] = result.frame[n].and( // Its box is a part of this widget's own box, in that box's
holds.extent[n] // own lengths, so what it holds for maps back through that
.through(extent.axis(axis).len()) // part into a range on this widget's box. A length it pinned
.through(frame_len), // 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.
(PlaceSpan::Within(span), None) => {
let part_len = span.len();
result.region = holds.region.through(part_len);
result.region_len = holds.region_len.map(|pinned| match part_len.rel {
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
_ => self.region[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 = result.window.and(holds.region.through(region[axis].len()));
}
} }
} }
result
} }
result
} }
/// A child's answer as lengths of the parent's own region. A widget reports impl Widgets {
/// a fraction of its own region, and `of` is that region as a length of this /// What a widget's box is where a rule or its own hint says so outright.
/// one. Pixels come through untouched, being that many pixels wherever they pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
/// end up. A declared axis is already the parent's: it resolved the rule in let rules = self.size_rules(id);
/// its own region, and the rule is what the report says. let widget = self.get_dyn(id);
fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size { Declared::from_axes(|axis| {
let mut size = size; rules[axis].declared().or_else(|| {
for (axis, declared) in AXES.into_iter().zip(declared) { // A hint still narrows the box where no rule does, which is
if declared.is_none() { // how a widget with a natural pixel size -- an image, a gap
*size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis)); // -- gets that size rather than the whole offer. That is the
} // offer's business rather than a declaration's, and this
} // falls away once a widget occupies its reported size inside
size // the box it was offered.
} widget
.and_then(|widget| widget.size_hint(axis))
/// What a widget declares a length of its box to be. `leftover` is not one: a .and_then(LayoutLen::declared)
/// share of what is left over is only a length to the widget dividing one, })
/// so it passes up in the size instead.
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] {
let rules = widgets.size_rules(id);
let widget = widgets.get_dyn(id);
AXES.map(|axis| {
rules.axis(axis).declared().or_else(|| {
// A hint still narrows the box where no rule does, which is how a
// widget with a natural pixel size -- an image, a gap -- gets that
// size rather than the whole offer. That is the offer's business
// rather than a declaration's, and this falls away once a widget
// occupies its reported size inside the box it was offered.
widget
.and_then(|widget| widget.size_hint(axis))
.filter(|len| len.leftover == Weight::ZERO)
}) })
})
}
/// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. A share fills, because a
/// share is a length only to whoever divides one, and whoever did is the one
/// that handed down this box. A declared axis does too: the rule already gave
/// the region its length, and the rule's length is what the widget reports
/// there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// Where a widget's drawing goes inside the part its parent gave it: what
/// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it.
///
/// The length it reported is a length of its frame, and the part is one too,
/// so this takes one from the other rather than composing it into the part.
/// That is what makes a fraction the same fraction wherever the part it is
/// placed in sits and however long it is -- the fraction is resolved once,
/// here, against the frame it was reported of.
pub(crate) fn placed_extent(
part: UiRegion,
size: Size,
declared: [Option<LayoutLen>; 2],
fill: [bool; 2],
align: RegionAlign,
) -> UiRegion {
let mut placed = part;
for axis in AXES {
let n = axis as usize;
let reported = size.axis(axis);
if fills(reported, declared[n], fill[n]) {
continue;
}
let len = Len::from_parts(reported.rel, reported.px);
let span = placed.axis_mut(axis);
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
} }
placed
} }
/// The part of a widget's own box a `place` names, in the coordinates that impl LayoutLen {
/// box is in. /// Whether what a widget reported along an axis is the whole of the box
pub(crate) fn part_of(extent: UiRegion, place: [Place; 2]) -> UiRegion { /// it is in rather than a part to be placed inside it. A share fills,
let mut part = extent; /// because a share is a length only to whoever divides one, and whoever
for axis in AXES { /// did is the one that handed down this box. A declared axis does too:
*part.axis_mut(axis) = place[axis as usize].part().of(*extent.axis(axis)); /// the rule already gave the region its length, and the rule's length is
/// what the widget reports there. And an axis the parent decided from
/// the answer is the answer already.
pub(super) fn fills(self, declared: Option<Len>, decided: bool) -> bool {
self.leftover != Weight::ZERO || declared.is_some() || decided
} }
part
} }
/// The length a rule gives a child's frame, per axis: a fraction in it is a impl PlaceDesc {
/// fraction of the frame the child was given, which is the one length the /// Where a widget's drawing goes inside the part its parent gave it: what
/// rule can mean. /// it reported, on the side of the part its alignment says, and the whole
pub(crate) fn narrowed_by(declared: [Option<LayoutLen>; 2], frame: UiRegion) -> [Option<Len>; 2] { /// part wherever the answer fills it.
AXES.map(|axis| { ///
declared[axis as usize] /// The length it reported is a length of its rel base, and the part is one
.map(|len| Len::from_parts(len.rel, len.px).within_len(frame.axis(axis).len())) /// too, so this takes one from the other rather than composing it into the
}) /// part. That is what makes a fraction the same fraction wherever the part
} /// it is placed in sits and however long it is -- the fraction is resolved
/// once, here, against the rel base it was reported of.
/// The frame a child is asked in and the box its drawing goes in, both in pub(super) fn placement(
/// the coordinates of the widget asking. self,
/// region: UiRegion,
/// `frame` is what the caller said the child's fractions are of, and `part` size: Size,
/// what of the caller's own box the drawing takes. `narrow` is the length a declared: Declared,
/// declared rule gives the frame, which makes the frame the box the drawing align: RegionAlign,
/// goes in -- a rule is what decided where it goes, and there is nothing ) -> UiRegion {
/// left to place inside it. A caller that narrowed the frame itself said the let mut placed = region;
/// same thing. for axis in Axis::BOTH {
/// let reported = size[axis];
/// The length is the caller's to supply so that a widget asked again gets if reported.fills(declared[axis], self[axis].fills) {
/// the frame it already has rather than a second resolution of its rule. continue;
pub(crate) fn frame_and_extent(
mut frame: UiRegion,
part: UiRegion,
narrow: [Option<Len>; 2],
align: RegionAlign,
) -> (UiRegion, UiRegion) {
let mut extent = part;
for (axis, narrow) in AXES.into_iter().zip(narrow) {
let span = frame.axis_mut(axis);
let narrowed = match narrow {
Some(len) => {
let slot = part.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*span = UiSpan::new(start, start + len);
true
} }
None => *span != UiSpan::FULL, placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
};
if narrowed {
*extent.axis_mut(axis) = UiSpan::FULL;
} }
placed
}
/// The rel base length and the box a child is asked in, in the coordinates the
/// widget asking draws in.
///
/// `own` is that widget's own box, and `place` what of it the child is
/// given, including any rel base it states -- a row's slot, or padding's rel
/// 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(super) fn rel_base_and_region(
self,
own: UiRegion,
parent_rel_base: UiVec2,
declared: Declared,
align: RegionAlign,
) -> (UiVec2, UiRegion) {
let given = self.of(own, align);
let mut rel_base = parent_rel_base;
let mut region = given;
for axis in Axis::BOTH {
let base = match self[axis].rel_base {
RelBase::Len(len) => len,
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
};
let len = declared[axis]
.map(|len| len.within_len(base))
.unwrap_or(base);
rel_base[axis] = len;
if declared[axis].is_some() {
region[axis] = given[axis].place(len, align[axis]);
}
}
(rel_base, region)
} }
(frame, extent)
} }
+180 -37
View File
@@ -1,54 +1,195 @@
use crate::{PrimitiveHandle, UiRegion, UiSpan}; use crate::util::impl_axis_index;
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
/// What of a widget's own box a child is given, along one axis. /// 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)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum Part { pub struct PlaceDescAxis {
/// The whole of it. pub span: PlaceSpan,
All, pub fills: bool,
/// Frame lengths from where the box starts, which is what a container pub rel_base: RelBase,
/// dividing room among its children speaks: a child's report is a length
/// of the frame, so the cursor that sums those reports is one too. A
/// moved box re-places every child by re-adding its start, exactly.
From(UiSpan),
/// A part of the box in its own coordinates, which is what a container
/// that insets one speaks: taking eleven pixels off the end needs no
/// length, where saying the same thing in frame lengths would make the
/// container read its own box -- and a box chosen from its own answer
/// then feeds back into the answer.
Of(UiSpan),
} }
impl Part { #[derive(Clone, Copy, Debug, PartialEq)]
/// Where it lands in the coordinates `extent` is in. pub enum PlaceSpan {
pub(crate) fn of(self, extent: UiSpan) -> UiSpan { Within(UiSpan),
match self { Shifted(UiSpan),
Self::All => extent, Sized(Len),
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end), }
Self::Of(span) => span.within(&extent),
/// What a child's fractions are of. [`PlaceSpan::Sized`] is a length the
/// caller named, which is always its own base, so nothing here constructs one
/// beside anything but [`Self::Len`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub 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 = UiSpan::FULL.within_desc();
/// 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
}
/// This along `axis`, and the whole of the caller's box across it: what
/// a container dividing one axis says, since nothing divides the other.
/// [`PlaceDesc::from_axis`] says the across one where it is not the
/// whole.
pub const fn on_axis(self, axis: Axis) -> PlaceDesc {
PlaceDesc::from_axis(axis, self, Self::WHOLE)
}
/// 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
}
/// Where it lands in the coordinates `own` is in.
pub 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) => own.place(len, align),
} }
} }
} }
/// Where a child goes along one axis, as a part of this widget's box. /// 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)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum Place { pub struct PlaceDesc {
/// The child's answer, aligned inside the part by the child's alignment. pub x: PlaceDescAxis,
Within(Part), pub y: PlaceDescAxis,
/// Exactly the part; the answer is not placed inside it again.
Fill(Part),
} }
impl Place { impl PlaceDesc {
pub(crate) fn part(self) -> Part { /// The whole of the caller's box, on both axes.
match self { pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
Self::Within(part) | Self::Fill(part) => part,
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 }
}
/// A description per axis, where the two differ and neither is the
/// axis a container divides.
pub fn from_axes(f: impl Fn(Axis) -> PlaceDescAxis) -> Self {
Self::new(f(Axis::X), f(Axis::Y))
}
/// `aligned` on `axis` and `ortho` on the other, which is how a
/// container that divides one axis says what it is doing.
pub const fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
Axis::Y => Self::new(ortho, aligned),
} }
} }
/// Whether the part is the drawing's box outright, rather than the box /// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
/// the answer is placed inside. pub const fn fills(self) -> Self {
pub(crate) fn fills(self) -> bool { Self::new(self.x.fills(), self.y.fills())
matches!(self, Self::Fill(_)) }
/// 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] = self[axis].rel_base(len);
self
}
/// The box each axis names, in the coordinates `own` is in.
pub 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 UiSpan {
/// This 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_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Within(self),
fills: false,
rel_base: RelBase::WithRegion,
}
}
/// This 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. The same span
/// says [`Self::within_desc`] as a part of that box instead, and which is
/// meant cannot be read off the numbers.
pub const fn shifted_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Shifted(self),
fills: false,
rel_base: RelBase::Inherit,
}
}
}
impl Len {
/// 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 as_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Sized(self),
fills: false,
rel_base: RelBase::Len(self),
}
}
}
impl From<UiRegion> for PlaceDesc {
fn from(region: UiRegion) -> Self {
Self::new(region.x.within_desc(), region.y.within_desc())
}
}
impl From<PlaceDescAxis> for PlaceDesc {
fn from(place: PlaceDescAxis) -> Self {
Self::splat(place)
} }
} }
@@ -59,3 +200,5 @@ pub struct RetainedPrimitive {
pub handle: PrimitiveHandle, pub handle: PrimitiveHandle,
pub region: UiRegion, pub region: UiRegion,
} }
impl_axis_index!(PlaceDesc => PlaceDescAxis);
File diff suppressed because it is too large. Load diff
+28
View File
@@ -93,3 +93,31 @@ macro_rules! impl_op {
} }
pub(crate) use impl_op; pub(crate) use impl_op;
/// `Index<Axis>` for a pair, which is how every pair here is read by axis.
/// The generics clause is given in braces where the type has one.
macro_rules! impl_axis_index {
($({$($gen:tt)*})? $T:ty => $Out:ty) => {
const impl $(<$($gen)*>)? std::ops::Index<crate::Axis> for $T {
type Output = $Out;
fn index(&self, axis: crate::Axis) -> &$Out {
match axis {
crate::Axis::X => &self.x,
crate::Axis::Y => &self.y,
}
}
}
const impl $(<$($gen)*>)? std::ops::IndexMut<crate::Axis> for $T {
fn index_mut(&mut self, axis: crate::Axis) -> &mut $Out {
match axis {
crate::Axis::X => &mut self.x,
crate::Axis::Y => &mut self.y,
}
}
}
};
}
pub(crate) use impl_axis_index;
+27 -28
View File
@@ -1,4 +1,5 @@
use crate::{Axis, LayoutLen, Weight}; use crate::util::impl_axis_index;
use crate::{Axis, LayoutLen, Len};
/// What a widget's length on one axis is, as a rule its parent applies where /// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself. /// it draws it rather than an answer the widget gives about itself.
@@ -19,14 +20,9 @@ pub enum SizeRule {
impl SizeRule { impl SizeRule {
/// The length this rule gives without the widget being drawn, if it can /// The length this rule gives without the widget being drawn, if it can
/// give one. `leftover` is never among them: a share is a length only to /// give one.
/// whoever divides one, so it passes up in the reported size instead and pub fn declared(&self) -> Option<Len> {
/// is resolved there. self.exact().and_then(LayoutLen::declared)
pub fn declared(&self) -> Option<LayoutLen> {
match self {
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
_ => None,
}
} }
/// The length this rule gives outright, whatever the widget reports -- /// The length this rule gives outright, whatever the widget reports --
@@ -40,14 +36,6 @@ impl SizeRule {
Self::Exact(len) => Some(*len), Self::Exact(len) => Some(*len),
} }
} }
/// The length a widget reporting `reported` ends up with.
pub fn apply(&self, reported: LayoutLen) -> LayoutLen {
match self {
Self::Free => reported,
Self::Exact(len) => *len,
}
}
} }
impl From<LayoutLen> for SizeRule { impl From<LayoutLen> for SizeRule {
@@ -70,18 +58,29 @@ pub struct SizeRules {
pub y: SizeRule, pub y: SizeRule,
} }
impl SizeRules { impl_axis_index!(SizeRules => SizeRule);
pub fn axis(&self, axis: Axis) -> SizeRule {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule { /// What a widget's box is on each axis where something says so outright,
match axis { /// before it is drawn: a rule beside it, or a hint it gives about itself.
Axis::X => &mut self.x, /// Whoever draws the widget resolves these against its rel base.
Axis::Y => &mut self.y, ///
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
/// -- see [`LayoutLen::declared`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Declared {
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Declared {
pub const NONE: Self = Self { x: None, y: None };
pub fn from_axes(f: impl Fn(Axis) -> Option<Len>) -> Self {
Self {
x: f(Axis::X),
y: f(Axis::Y),
} }
} }
} }
impl_axis_index!(Declared => Option<Len>);
+4 -4
View File
@@ -130,10 +130,10 @@ impl Widgets {
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) { pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
let id = id.id(); let id = id.id();
let data = self.data_mut(id).unwrap(); let data = self.data_mut(id).unwrap();
if *data.size.axis_mut(axis) == rule { if data.size[axis] == rule {
return; return;
} }
*data.size.axis_mut(axis) = rule; data.size[axis] = rule;
self.needs_redraw.insert(id); self.needs_redraw.insert(id);
} }
@@ -147,10 +147,10 @@ impl Widgets {
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) { pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
let id = id.id(); let id = id.id();
let data = self.data_mut(id).unwrap(); let data = self.data_mut(id).unwrap();
if *data.align.axis_mut(axis) == align { if data.align[axis] == align {
return; return;
} }
*data.align.axis_mut(axis) = align; data.align[axis] = align;
self.needs_redraw.insert(id); self.needs_redraw.insert(id);
} }
+9 -6
View File
@@ -22,12 +22,12 @@ impl UiRenderer {
} }
pub fn draw(&mut self) { pub fn draw(&mut self) {
let output = match self.surface.get_current_texture() { let (output, suboptimal) = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) => texture, CurrentSurfaceTexture::Success(texture) => (texture, false),
CurrentSurfaceTexture::Suboptimal(texture) => { // Used for this frame, and the swapchain rebuilt after it has
self.surface.configure(&self.device, &self.config); // been presented: configuring the surface while a texture it
texture // handed out is still alive panics.
} CurrentSurfaceTexture::Suboptimal(texture) => (texture, true),
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => { CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
return; return;
@@ -60,6 +60,9 @@ impl UiRenderer {
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify(); self.window.pre_present_notify();
self.queue.present(output); self.queue.present(output);
if suboptimal {
self.surface.configure(&self.device, &self.config);
}
} }
pub fn resize(&mut self, size: &PhysicalSize<u32>) { pub fn resize(&mut self, size: &PhysicalSize<u32>) {
+22 -8
View File
@@ -118,20 +118,34 @@ 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 cut = Len::from_parts(Rel::ZERO, Px::from_int(40)); let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut))); let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
let measured = painter let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(Part::All), top]) .widget_at(&self.probe, top.on_axis(Axis::Y))
.len(Axis::X); .len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_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 below = Place::Within(Part::Of(UiSpan::new(cut, Len::FULL))); let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
let place = [Place::Within(Part::All), below]; let place = below.on_axis(Axis::Y);
match px > Px::from_f32(self.threshold) { match px > threshold {
true => painter.widget_at(&self.wide, UiRegion::FULL, place), true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, UiRegion::FULL, place), 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 {
+1 -1
View File
@@ -12,7 +12,7 @@ impl Widget for Image {
} }
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> { fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(self.handle.size().axis(axis))) Some(LayoutLen::px(self.handle.size()[axis]))
} }
} }
+4
View File
@@ -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 -9
View File
@@ -7,14 +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 {
// The whole of this widget's box, moved: the frame passes through, so painter
// what the child declares or reports means the same as it would .widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
// without the offset. .size()
let moved = |len: Len, amt: Len| Place::Within(Part::From(UiSpan::new(amt, len + amt)));
let place = [
moved(painter.extent_len(Axis::X), self.amt.x),
moved(painter.extent_len(Axis::Y), self.amt.y),
];
painter.widget_at(&self.inner, UiRegion::FULL, place).size()
} }
} }
+6 -17
View File
@@ -14,23 +14,12 @@ impl Widget for Pad {
// 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.
// //
// The padding goes around what it pads: the frame passes through, so // Padding is an inset of both: it comes off the rel base, so `rel(1)`
// the inner's fractions mean what they would without it, and only // under it fills this widget rather than overflowing it by the
// the box it draws in is moved in by the pixels. Said as a part of // padding, and it comes off the box, so what is drawn sits inside.
// this widget's own box in that box's own lengths, so nothing here // The two stay distinct -- the box can be narrower still, where a row
// reads how long the box is -- and a box chosen from this widget's // asked this widget in the room left, and a text wraps at that.
// own answer therefore does not feed back into that answer. let inner = painter.widget_at(&self.inner, self.padding.region()).size();
let inset = |lead: Px, trail: Px| {
Place::Within(Part::Of(UiSpan::new(
Len::from_parts(Rel::ZERO, lead),
Len::from_parts(Rel::ONE, -trail),
)))
};
let place = [
inset(self.padding.left, self.padding.right),
inset(self.padding.top, self.padding.bottom),
];
let inner = painter.widget_at(&self.inner, UiRegion::FULL, place).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
+24 -24
View File
@@ -12,30 +12,29 @@ 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);
// Measured in the whole viewport, then drawn at the scrolled offset. // Asked in the whole viewport, then put at the scrolled offset.
let whole = UiRegion::FULL;
let answer_len = painter let answer_len = painter
.widget_at(&self.inner, whole, [Place::Fill(Part::All); 2]) .widget_at(&self.inner, PlaceDesc::WHOLE.fills())
.len(self.axis); .len(self.axis);
let fixed = Len::from_parts(answer_len.rel, answer_len.px).to_px(container_len); let answer_px = painter.to_px(answer_len.without_leftover(), self.axis);
self.container_len = container_len; self.container_len = container_len;
self.content_len = fixed.max(container_len); self.content_len = answer_px.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;
} }
self.update_amt(); self.update_amt();
let align = painter.alignment().axis(self.axis); let align = painter.alignment()[self.axis];
// Content of a fixed length that fits sits at the start of any box it // Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of // fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and // the room left over, so it moves with every length the box takes and
// 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 = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO; let answer_is_px = answer_len.is_px();
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { if answer_is_px && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, fixed..=Px::MAX); painter.holds(self.axis, answer_px..=Px::MAX);
} else if fixed_len && !self.snap_end { } else if answer_is_px && !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);
} }
@@ -45,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 content = UiSpan::FULL;
// Content that fills the viewport and has not been scrolled is the // Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. Writing the same box as // viewport, and is handed back as it came. Writing the same box as
// its own length in pixels is the same box in another form, and the // its own length in pixels is the same box in another form, and the
@@ -53,26 +51,28 @@ 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 start = Len::from_parts(Rel::ZERO, anchor - self.amt); true => {
content = UiSpan::new(start, start.offset(self.content_len)); let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
} UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
// The viewport is the inner's frame, so a fraction it declares or }
false => PlaceDescAxis::WHOLE,
};
// 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 // reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is drawn is the // content box its own answer decided. Where it goes is the content
// content box, scrolled. // box, scrolled: its drawing moved there, not made again there.
painter.widget_at( painter.place_at(&self.inner, content.on_axis(self.axis).fills());
&self.inner,
whole,
self.axis
.pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)),
);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
// is. // is.
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
impl Scroll { impl Scroll {
+74 -89
View File
@@ -10,32 +10,32 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The row: this span's own box, as a length of the frame its children // The row this span lays its children out along, as a length of the
// are laid out against. Its start is nothing's business -- a slot is // rel base they are laid out against. Where it starts is nothing's
// a length from it -- so what this reads is the length alone. // business -- a slot is a length from there -- so what this reads is
let far = painter.answer_extent_len(axis); // the length alone.
let measure_along = |from: Len, to: Len| match self.dir.sign { let row = painter.region_len(axis);
Sign::Pos => UiSpan::new(from, to), // A length for every child before their final slots are chosen: from
Sign::Neg => UiSpan::new(far - to, far - 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
// Across itself the child sits where its own alignment says, in the // given whatever else is in it and wherever this child sits among
// whole of the row: a span is what contains its children there, and // them; what a drawn child is asked in is the room left from the
// nothing divides that axis. // cursor, because a text has to wrap at the width actually there.
let across = Place::Within(Part::All); let mut cursor = Len::ZERO;
// A length for every child before their final slots are chosen. The
// frame passes through unchanged, so `rel(0.5)` is half the area this
// span was given whatever else is in it and wherever this child sits
// among them; what it is drawn in is the room left from the cursor,
// because a text has to wrap at the width actually there.
let mut cursor = Len::rel_min();
let mut 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 room = Place::Fill(Part::From(measure_along(cursor, far))); let len = match painter.size_hint(child, axis) {
let len = painter Some(len) => len,
.widget_at(child, UiRegion::FULL, axis.pair(room, across)) None => {
.len(axis); // Across itself the child sits where its own alignment
cursor.px += len.px + self.gap; // says, in the whole of the row: a span is what contains
cursor.rel += len.rel; // its children there, and nothing divides that axis.
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
painter.widget_at(child, room).len(axis)
}
};
cursor += len.without_leftover();
cursor.px += self.gap;
lens.push(len); lens.push(len);
} }
@@ -50,10 +50,9 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
let fixed_total = Len::from_parts(total.rel, total.px);
// What is left for the shares to divide: the row less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the frame rather than a number of pixels. // fixed, as a length of the rel base rather than a number of pixels.
let room = far - fixed_total; let room = row - total.without_leftover();
// 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. Asked of `room` // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the // itself, and answered back through the same expression, so the
@@ -63,28 +62,16 @@ impl Widget for Span {
// sign of `room.rel`, which `through` already reads. What the // sign of `room.rel`, which `through` already reads. What the
// generated oracle checks is the consequence, since which children // generated oracle checks is the consequence, since which children
// exist at all turns on this. // exist at all turns on this.
let mut shares = false; let any_leftover = total.leftover > Weight::ZERO;
if total.leftover > Weight::ZERO { let has_room = any_leftover && painter.to_px(room, axis) > Px::ZERO;
shares = room.to_px(painter.frame_px_len(axis)) > Px::ZERO; if any_leftover {
let holds = match shares { let holds = match has_room {
true => Holds::from(Px::STEP..=Px::MAX), true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO), false => Holds::from(Px::MIN..=Px::ZERO),
}; };
painter.frame_holds(axis, holds.through(room)); painter.window_holds(axis, holds.through(room));
} }
if shares {
painter.drawing_uses_extent_len(axis, far);
}
let drawing_far = match shares {
true => far,
false => fixed_total,
};
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(drawing_far - to, drawing_far - from),
};
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them // rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. A rule // answers nothing and makes its size depend on theirs for it. A rule
@@ -93,61 +80,59 @@ impl Widget for Span {
let shrinks = !painter.has_exact_size(!axis); let shrinks = !painter.has_exact_size(!axis);
// What the fixed parts and the gaps before here take, which is a sum // What the fixed parts and the gaps before here take, which is a sum
// of lengths and exact, and how much of the leftover weight is // of lengths and exact, and how much of the leftover weight is
// spoken for. A position is one from the other rather than a step // spoken for. Both ends of a slot are read from those two rather
// from the last child: the share of the room is rounded, and taking // than stepped from the last child: the share of the room is
// each from the one before it would carry every rounding along the // rounded, and taking each end from the one before it would carry
// row. // every rounding along the row.
let mut fixed = Len::rel_min(); let mut fixed = Len::ZERO;
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
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) { // Nothing divides the room where no child asked for any of it, and a
// ratio of a whole of nothing has no answer.
let reached = |fixed: Len, taken: Weight| match any_leftover {
false => fixed,
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
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.
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares if len.is_only_leftover() && !has_room {
{
painter.undraw(child); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let from = start; let from = reached(fixed, taken);
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && has_room {
taken += len.leftover; taken += len.leftover;
} }
fixed.px += len.px; fixed += len.without_leftover();
fixed.rel += len.rel; let to = reached(fixed, taken);
start = shared(fixed, taken, total.leftover, room);
// Along the row the span says where the child goes, and that slot // Along the row the span says where the child goes, and that slot
// is the drawing's box outright rather than something to place an // is the child's box outright rather than something to place an
// answer inside again. // answer inside again. A share is decided here and nowhere
let span = along(from, start); // else: its slot narrows its rel base, and the child is asked in
let (frame, slot) = match len.leftover > Weight::ZERO && shares { // it, since a text wraps at the width it is actually given. A
true => ( // fixed child's slot is its own answer, so a drawing made in the
UiRegion::from_axis( // room is put there as it is, and one not made yet is made here.
axis, let slot = self.slot(row, from, to);
painter.extent_part(axis, Part::From(span)), let mut place = slot.shifted_desc().fills().on_axis(axis);
UiSpan::FULL, if len.leftover > Weight::ZERO && has_room {
), place = place.rel_base(axis, slot.len());
Place::Fill(Part::All), }
), let used = painter.place_at(child, place).len(!axis);
false => (UiRegion::FULL, Place::Fill(Part::From(span))),
};
let placed = painter.widget_at(child, frame, axis.pair(slot, across));
if shrinks { if shrinks {
let used = placed.len(!axis);
// 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 the span scalable too;
// only fixed children are compared with one another. // only fixed children are compared with one another.
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO { if !used.is_px() {
ortho = LayoutLen::LEFTOVER; ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO { } else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px); ortho.px = ortho.px.max(used.px);
} }
} }
fixed.px += self.gap; fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
} }
// Carried whole rather than collapsed to one share: a span that sizes // Carried whole rather than collapsed to one share: a span that sizes
@@ -157,26 +142,26 @@ impl Widget for Span {
// get a quarter each, which collapsing to `leftover(1)` per level does // get a quarter each, which collapsing to `leftover(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length, // not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one. // and the root always has one.
let along = total;
let ortho = match shrinks { let ortho = match shrinks {
true => ortho, true => ortho,
false => LayoutLen::rel(1.0), false => LayoutLen::rel(1.0),
}; };
Size::from_axis(axis, along, ortho) Size::from_axis(axis, total, ortho)
} }
} }
/// Where a row has reached: everything fixed before this point, which is a
/// sum and exact, plus the share of the room the weights so far are worth,
/// which is one rounding wherever it is asked for.
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
if taken == Weight::ZERO {
return fixed;
}
fixed + room.scale(Rel::ratio(taken, weight))
}
impl Span { impl Span {
/// The stretch of the row between two distances from where this span
/// starts laying children out, as a span of its own box. A negative
/// direction lays out from the far end, so the same two distances mirror
/// in a row `row` long.
fn slot(&self, row: Len, from: Len, to: Len) -> UiSpan {
match self.dir.sign {
Sign::Pos => from.to(to),
Sign::Neg => (row - to).to(row - from),
}
}
pub fn empty(dir: Dir) -> Self { pub fn empty(dir: Dir) -> Self {
Self { Self {
children: Vec::new(), children: Vec::new(),
+23 -12
View File
@@ -22,29 +22,40 @@ impl Widget for Stack {
// drawing belongs to the layer it was made on. // drawing belongs to the layer it was made on.
Some((i, child)) => { Some((i, child)) => {
painter.child_layer_at(i); painter.child_layer_at(i);
painter painter.widget_at(child, PlaceDesc::WHOLE.fills()).size()
.widget_at(child, UiRegion::FULL, [Place::Fill(Part::All); 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 place = PlaceDesc::from_axes(|axis| {
let len = size[axis];
match len.leftover == Weight::ZERO {
true => len.without_leftover().as_desc().fills(),
false => PlaceDescAxis::WHOLE,
}
});
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);
let place = [Axis::X, Axis::Y].map(|axis| { painter.widget_at(child, place);
let len = size.axis(axis);
let part = match len.leftover > Weight::ZERO {
true => Part::All,
false => Part::From(UiSpan::new(Len::ZERO, Len::from_parts(len.rel, len.px))),
};
Place::Within(part)
});
painter.widget_at(child, UiRegion::FULL, place);
} }
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)]
+7 -7
View File
@@ -22,17 +22,17 @@ 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 cut = Len::from_parts(Rel::ZERO, Px::from_int(40)); let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut))); let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
let measured = painter let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(Part::All), top]) .widget_at(&self.probe, top.on_axis(Axis::Y))
.len(Axis::X); .len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let px = painter.to_px(measured.apply_leftover(), Axis::X);
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y)))); let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
let place = [Place::Within(Part::All), below]; let place = below.on_axis(Axis::Y);
match px > Px::from_f32(self.threshold) { match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, UiRegion::FULL, place), true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, UiRegion::FULL, place), false => painter.widget_at(&self.narrow, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
+129 -11
View File
@@ -83,10 +83,8 @@ 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}");
} }
/// The same reading through a pad: padding goes around what it pads and /// Padding is an inset: it narrows the frame a fraction resolves against and
/// does not narrow what a fraction under it is a fraction of, so half of the /// adds itself back to the padded widget's reported length.
/// window plus the padding is what the pad takes and where the next child
/// starts.
#[test] #[test]
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() { fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
let mut h = Harness::new((400, 100)); let mut h = Harness::new((400, 100));
@@ -97,9 +95,83 @@ fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_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), (210, 90)); assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (220, 100)); assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (220, 0), (320, 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]
@@ -428,12 +500,11 @@ 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 drawn = active.extent.within(&active.frame_abs); let region = h.render.moves.resolve(active.move_idx, active.placement);
let region = h.render.moves.resolve(active.parent_move, drawn); let dim = h.size()[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());
let span = region.axis(axis); let span = region[axis];
(edge(span.start), edge(span.end)) (edge(span.start), edge(span.end))
} }
@@ -740,3 +811,50 @@ fn a_fixed_child_is_centered_in_its_wrappers_share() {
assert_corners!(h, wrapper, (200, 0), (900, 400)); assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250)); 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[dir.axis], Px::from_int(from));
assert_eq!(region.bot_right[dir.axis], Px::from_int(to));
}
}
}
+304 -98
View File
@@ -12,7 +12,6 @@ struct Counted {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
size: Size, size: Size,
reads_box: bool, reads_box: bool,
reads_answer_box: bool,
} }
impl Widget for Counted { impl Widget for Counted {
@@ -20,8 +19,6 @@ impl Widget for Counted {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
if self.reads_box { if self.reads_box {
painter.px_size(); painter.px_size();
} else if self.reads_answer_box {
painter.answer_px_size();
} }
self.size self.size
} }
@@ -41,18 +38,11 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
draws: draws.clone(), draws: draws.clone(),
size, size,
reads_box, reads_box,
reads_answer_box: false,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
(id, Counts(draws)) (id, Counts(draws))
} }
fn answer_counted(h: &mut Harness, size: Size) -> (WeakWidget<Counted>, Counts) {
let (id, draws) = counted(h, size, false);
h.rsc[id].reads_answer_box = true;
(id, draws)
}
struct Layered { struct Layered {
children: [StrongWidget<Rect>; 2], children: [StrongWidget<Rect>; 2],
_revision: usize, _revision: usize,
@@ -135,7 +125,7 @@ fn moving_an_ordinary_subtree_remaps_its_mask() {
let active = &h.render.active[&masked.id()]; let active = &h.render.active[&masked.id()];
assert_eq!( assert_eq!(
h.rsc.ui().masks[active.mask.idx()].region, h.rsc.ui().masks[active.mask.idx()].region,
UiRegion::new(UiSpan::new(Len::px(150.0), Len::rel_max()), UiSpan::FULL,) UiRegion::new(UiSpan::new(Len::px(150.0), Len::FULL), UiSpan::FULL,)
); );
assert_corners!(h, inner, (150, 0), (400, 200)); assert_corners!(h, inner, (150, 0), (400, 200));
} }
@@ -166,9 +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);
// Its final slot is a parent decision, so it is evaluated there after // Asked once, from the cursor; its slot is its answer and the drawing is
// the provisional ask established its length. // moved there.
assert_eq!(asked_draws.get(), 2); assert_eq!(asked_draws.get(), 1);
} }
#[test] #[test]
@@ -224,11 +214,7 @@ impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap(); let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px)); let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
painter.widget_at( painter.widget_at(&self.inner, top.shifted_desc().on_axis(Axis::Y));
&self.inner,
UiRegion::FULL,
[Place::Within(Part::All), Place::Within(Part::From(top))],
);
Size::LEFTOVER Size::LEFTOVER
} }
} }
@@ -250,7 +236,7 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
assert_corners!(h, inner, (0, 0), (400, 120)); assert_corners!(h, inner, (0, 0), (400, 120));
} }
/// Reads its box's size to compute its answer. /// Reads its box's size, which nothing but its own draw can put right.
struct ReadsBox { struct ReadsBox {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
@@ -258,36 +244,25 @@ struct ReadsBox {
impl Widget for ReadsBox { impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::from_px(painter.answer_px_size().div_int(4)) Size::from_px(painter.px_size().div_int(4))
} }
} }
/// Reads its box across one axis only, so its drawing holds for a taller /// Reads its box across one axis only, so its drawing holds for a taller
/// box on its own and only a wider one is worth a draw. /// box on its own and only a wider one is worth a draw.
/// ///
/// Both report a quarter of what they read. Their empty drawings are /// Both of these report a quarter of what they read. The quarter-sized box
/// independent of that read, so a changed question costs one draw. /// the answer places them in is not a question: the drawing is moved there,
/// so each length they are asked at costs one draw.
struct ReadsWidth { struct ReadsWidth {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
struct ReadsDrawingWidth {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsDrawingWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.px_len(Axis::X);
Size::LEFTOVER
}
}
impl Widget for ReadsWidth { impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::from_px(PxVec2::new( Size::from_px(PxVec2::new(
painter.answer_px_len(Axis::X).div_int(4), painter.px_len(Axis::X).div_int(4),
Px::from_int(20), Px::from_int(20),
)) ))
} }
@@ -343,7 +318,6 @@ fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
draws: ruled.clone(), draws: ruled.clone(),
size: Size::LEFTOVER, size: Size::LEFTOVER,
reads_box: false, reads_box: false,
reads_answer_box: false,
}; };
let second = match declared { let second = match declared {
true => second.width(rel(0.25)).add(&mut h.rsc), true => second.width(rel(0.25)).add(&mut h.rsc),
@@ -521,7 +495,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// Every wrapper up to the outer pad read the size below it, so the outer // Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same // pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf. // size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), false); let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
let padded = leaf.pad(10).add(&mut h.rsc); let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12)); h.set_root((padded, below).span(Dir::DOWN).pad(12));
@@ -819,7 +793,7 @@ fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
} }
#[test] #[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() { fn changing_an_inherited_region_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) { 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 first = rect(Color::RED).width(width).add(&mut h.rsc);
let words = wtext(text).size(20).wrap(true).add(&mut h.rsc); let words = wtext(text).size(20).wrap(true).add(&mut h.rsc);
@@ -899,11 +873,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
UiRegion::FULL, PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
); );
Size::LEFTOVER Size::LEFTOVER
} }
@@ -981,18 +951,18 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
} }
struct Frame { struct Frame {
child: StrongWidget, child: StrongWidget,
frame: UiRegion,
region: UiRegion, region: UiRegion,
extent: UiRegion,
} }
impl Widget for Frame { impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
self.region, PlaceDesc::new(
[ self.region.x.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.x)), self.region.y.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.y)), )
], .rel_base(Axis::X, self.frame.x.len()),
); );
Size::LEFTOVER Size::LEFTOVER
} }
@@ -1008,15 +978,15 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
h.rsc.widgets_mut().set_region_node(text, node); h.rsc.widgets_mut().set_region_node(text, node);
let root = Frame { let root = Frame {
child: text.add_strong(&mut h.rsc), child: text.add_strong(&mut h.rsc),
frame: UiRegion::FULL,
region: UiRegion::FULL, region: UiRegion::FULL,
extent: UiRegion::FULL,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
h.set_root(root); h.set_root(root);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] { for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let before = draws.get(); let before = draws.get();
h.rsc[root].region.x = UiSpan::new(Len::px(13.125), Len::px(287.375)); h.rsc[root].frame.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
h.rsc[root].extent = UiRegion::new( h.rsc[root].region = UiRegion::new(
UiSpan::new(Len::rel(start), Len::rel(end)), UiSpan::new(Len::rel(start), Len::rel(end)),
UiSpan::new(Len::px(7.25), Len::rel(end)), UiSpan::new(Len::px(7.25), Len::rel(end)),
); );
@@ -1087,7 +1057,7 @@ fn a_declared_size_change_stops_at_an_independent_parent() {
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() { fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = ReadsDrawingWidth { let leaf = ReadsWidth {
draws: draws.clone(), draws: draws.clone(),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
@@ -1098,7 +1068,7 @@ fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
h.frame(); h.frame();
assert!(draws.get() > settled); assert!(draws.get() > settled);
assert_corners!(h, leaf, (0, 0), (800, 200)); assert_corners!(h, leaf, (300, 90), (500, 110));
} }
#[test] #[test]
@@ -1139,7 +1109,7 @@ fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
assert_eq!(leaf_draws.get(), settled + 1); assert_eq!(leaf_draws.get(), settled + 1);
} }
#[test] #[test]
fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() { fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
struct Observed<W> { struct Observed<W> {
widget: W, widget: W,
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
@@ -1152,23 +1122,22 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
} }
struct Frame { struct Frame {
child: StrongWidget, child: StrongWidget,
extent: UiRegion, region: UiRegion,
} }
impl Widget for Frame { impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
UiRegion::FULL, PlaceDesc::new(
[ self.region.x.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.x)), self.region.y.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.y)), ),
],
); );
Size::LEFTOVER Size::LEFTOVER
} }
} }
for node in [false, true] { for node in [false, true] {
let plant = |h: &mut Harness, extent| { let plant = |h: &mut Harness, region| {
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = rect(Color::BLUE).masked().add(&mut h.rsc); let leaf = rect(Color::BLUE).masked().add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node); h.rsc.widgets_mut().set_region_node(leaf, node);
@@ -1189,7 +1158,7 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
draws: draws.clone(), draws: draws.clone(),
} }
.add_strong(&mut h.rsc); .add_strong(&mut h.rsc);
let root = Frame { child: pad, extent }.add(&mut h.rsc); let root = Frame { child: pad, region }.add(&mut h.rsc);
h.set_root(root); h.set_root(root);
(root, leaf, fixed, draws) (root, leaf, fixed, draws)
}; };
@@ -1204,13 +1173,13 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
let mut warm = Harness::new((403, 211)); let mut warm = Harness::new((403, 211));
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13)); let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
for start in [0.13, -0.17, 0.31] { for start in [0.13, -0.17, 0.31] {
let extent = at(start); let region = at(start);
let before = draws.get(); let before = draws.get();
warm.rsc[root].extent = extent; warm.rsc[root].region = region;
warm.frame(); warm.frame();
assert_eq!(draws.get(), before); assert_eq!(draws.get(), before);
let mut cold = Harness::new((403, 211)); let mut cold = Harness::new((403, 211));
let (_, other, other_fixed, _) = plant(&mut cold, extent); let (_, other, other_fixed, _) = plant(&mut cold, region);
for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] { for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] {
assert_eq!(warm.region(&a), cold.region(&b)); assert_eq!(warm.region(&a), cold.region(&b));
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b)); assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
@@ -1229,11 +1198,11 @@ fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
} }
#[test] #[test]
fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() { fn moving_a_childs_region_preserves_the_slot_chosen_from_its_measurement() {
struct Measured; struct Measured;
impl Widget for Measured { impl Widget for Measured {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let width = painter.answer_px_len(Axis::X); let width = painter.px_len(Axis::X);
painter.primitive(RectPrimitive::color(Color::BLUE)); painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 })) Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
} }
@@ -1246,14 +1215,12 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at( painter.widget_at(
&self.child, &self.child,
UiRegion::FULL, PlaceDesc::new(
[ UiSpan::new(Len::px(self.start), Len::px(self.start + 200.0))
Place::Fill(Part::From(UiSpan::new( .shifted_desc()
Len::px(self.start), .fills(),
Len::px(self.start + 200.0), UiSpan::FULL.shifted_desc().fills(),
))), ),
Place::Fill(Part::From(UiSpan::FULL)),
],
); );
Size::LEFTOVER Size::LEFTOVER
} }
@@ -1278,7 +1245,7 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
} }
#[test] #[test]
fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() { fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() {
struct Container { struct Container {
child: StrongWidget, child: StrongWidget,
region: UiRegion, region: UiRegion,
@@ -1288,18 +1255,14 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter painter
.widget_at( .widget_at(
&self.child, &self.child,
UiRegion::FULL, PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
[
Place::Within(Part::From(self.region.x)),
Place::Within(Part::From(self.region.y)),
],
) )
.size() .size()
} }
} }
struct Frame { struct Frame {
child: StrongWidget, child: StrongWidget,
extent: UiRegion, region: UiRegion,
answer: Rc<Cell<Size>>, answer: Rc<Cell<Size>>,
} }
impl Widget for Frame { impl Widget for Frame {
@@ -1308,11 +1271,10 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter painter
.widget_at( .widget_at(
&self.child, &self.child,
UiRegion::FULL, PlaceDesc::new(
[ self.region.x.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.x)), self.region.y.shifted_desc().fills(),
Place::Fill(Part::From(self.extent.y)), ),
],
) )
.size(), .size(),
); );
@@ -1324,7 +1286,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
UiRegion::FULL, UiRegion::FULL,
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL), UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
] { ] {
let plant = |h: &mut Harness, extent| { let plant = |h: &mut Harness, outer| {
let size = if fractional { let size = if fractional {
Size { Size {
x: rel(0.5), x: rel(0.5),
@@ -1333,10 +1295,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
} else { } else {
Size::from((80, 27)) Size::from((80, 27))
}; };
let (leaf, _) = match fractional { let (leaf, _) = counted(h, size, !fractional);
true => counted(h, size, false),
false => answer_counted(h, size),
};
let child = Container { let child = Container {
child: leaf.add_strong(&mut h.rsc), child: leaf.add_strong(&mut h.rsc),
region, region,
@@ -1345,7 +1304,7 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
let answer = Rc::new(Cell::new(Size::ZERO)); let answer = Rc::new(Cell::new(Size::ZERO));
let root = Frame { let root = Frame {
child, child,
extent, region: outer,
answer: answer.clone(), answer: answer.clone(),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
@@ -1355,15 +1314,262 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
let mut warm = Harness::new((403, 211)); let mut warm = Harness::new((403, 211));
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL); let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
for width in [191.125, 297.25, 83.75] { for width in [191.125, 297.25, 83.75] {
let extent = let region =
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL); UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
warm.rsc[root].extent = extent; warm.rsc[root].region = region;
warm.frame(); warm.frame();
let mut cold = Harness::new((403, 211)); let mut cold = Harness::new((403, 211));
let (_, other, other_answer) = plant(&mut cold, extent); let (_, other, other_answer) = plant(&mut cold, region);
assert_eq!(answer.get(), other_answer.get()); assert_eq!(answer.get(), other_answer.get());
assert_eq!(warm.region(&leaf), cold.region(&other)); 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);
}
}
/// A leaf that reports less than the box it is given and states which lengths
/// of that box its drawing holds for, so a test can widen the contract
/// without changing the answer. It counts its draws, since what a kept
/// contract costs is whether the parent has to make it draw again.
struct Contracted {
holds: std::ops::RangeInclusive<Px>,
size: Size,
draws: Rc<Cell<usize>>,
}
impl Widget for Contracted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.holds(Axis::X, self.holds.clone());
self.size
}
}
struct CountedParent {
inner: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for CountedParent {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.inner).size()
}
}
#[test]
fn widening_what_a_drawing_holds_for_does_not_relay_out_the_parent() {
let mut h = Harness::new((400, 200));
let child = Contracted {
holds: Px::from_int(300)..=Px::from_int(500),
size: Size::from((100, 200)),
draws: Rc::new(Cell::new(0)),
}
.add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let root = CountedParent {
inner: child.upgrade(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(root);
let settled = draws.get();
// The same answer, good for more boxes than before, so the guarantee the
// parent kept still holds.
h.rsc[child].holds = Px::from_int(200)..=Px::from_int(600);
h.frame();
assert_eq!(
draws.get(),
settled,
"a wider contract for the same answer is not a change to lay out"
);
}
/// The other half of the rule above: a kept contract is the narrower one, so
/// it is only worth keeping where it still holds. A window the old range is
/// outside is not one its parent can be handed back, and keeping it there
/// throws away the drawing the widget just made.
#[test]
fn a_contract_this_window_is_outside_is_not_kept() {
let mut h = Harness::new((400, 200));
let leaf_draws = Rc::new(Cell::new(0));
let child = Contracted {
holds: Px::from_int(300)..=Px::from_int(500),
size: Size::from((100, 200)),
draws: leaf_draws.clone(),
}
.add(&mut h.rsc);
let root = CountedParent {
inner: child.upgrade(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add(&mut h.rsc);
h.set_root(root);
// Wide enough that the old contract leaves the new box out, and the leaf
// is marked in the same frame -- so it settles itself first and its
// parent draws afterwards, asking about what it settled.
h.resize((600, 200));
h.rsc[child].holds = Px::from_int(200)..=Px::from_int(700);
let settled = leaf_draws.get();
h.frame();
assert_eq!(
leaf_draws.get(),
settled + 1,
"the leaf settled once and its parent kept what it settled"
);
}
+16
View File
@@ -145,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));
}
+250
View File
@@ -733,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
@@ -846,3 +891,208 @@ fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
let (_, other, _) = build(&mut cold, true); let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other)); 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);
}
/// A scroll clamps its position against the box it is drawn in, so drawing it
/// once at one viewport and again at another writes state the second draw then
/// reads. That the answer is still the one a cold layout gives is a property
/// of the clamp, not something the layout enforces.
#[test]
fn a_scrolled_view_resized_lands_where_a_cold_layout_puts_it() {
for amt in [10.0, 40.0, 90.0, 140.0] {
let mut warm = Harness::new((100, 100));
let (_, warm_scroll) = plant_wider(&mut warm, 100.0);
warm.move_to((50.0, 50.0));
warm.scroll((-amt, 0.0));
warm.frame();
warm.resize((160, 100));
warm.frame();
let mut cold = Harness::new((160, 100));
let (_, cold_scroll) = plant_wider(&mut cold, 100.0);
cold.move_to((50.0, 50.0));
cold.scroll((-amt, 0.0));
cold.frame();
assert_eq!(
warm.region(&warm_scroll),
cold.region(&cold_scroll),
"scrolled by {amt} then widened"
);
}
}
+48 -51
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} \
@@ -44,57 +47,50 @@ fn check(seed: u64, depth: usize, case: Case) {
} }
} }
macro_rules! case { /// A test per case, and the list of which cases have one, from the same
($name:ident, $case:expr) => { /// place. A case the ordinary suite leaves out runs only in the long scan,
#[test] /// which nobody runs by hand.
fn $name() { macro_rules! cases {
for seed in SEEDS { ($($name:ident = $case:expr,)*) => {
check(seed, depth(), $case); $(
#[test]
fn $name() {
for seed in SEEDS {
check(seed, depth(), $case);
}
} }
} )*
const NAMED: [Case; [$($case,)*].len()] = [$($case,)*];
}; };
} }
case!( cases! {
many_widgets_redrawing_at_once_leaves_every_box_where_it_was, many_widgets_redrawing_at_once_leaves_every_box_where_it_was = Case::RepaintSome,
Case::RepaintSome everything_redrawing_at_once_leaves_every_box_where_it_was = Case::Repaint,
); a_resize_lands_where_starting_at_that_size_would = Case::Resize,
case!( a_resize_and_a_repaint_land_where_starting_that_way_would = Case::ResizeRepaint,
everything_redrawing_at_once_leaves_every_box_where_it_was, a_size_change_after_a_resize_lands_the_same_way = Case::ResizeSize,
Case::Repaint a_resize_after_a_size_change_lands_the_same_way = Case::SizeResize,
); a_size_change_lands_where_growing_it_that_way_would = Case::Size,
case!( every_size_changing_at_once_lands_where_growing_it_that_way_would = Case::EverySize,
a_resize_lands_where_starting_at_that_size_would, an_alignment_change_lands_where_growing_it_that_way_would = Case::Align,
Case::Resize giving_and_taking_a_movable_region_rebuilds_what_resolves_it = Case::RegionNode,
); reordering_a_span_lands_where_growing_it_that_way_would = Case::Reorder,
case!( }
a_resize_and_a_repaint_land_where_starting_that_way_would,
Case::ResizeRepaint /// The shuffles are one test between them, so they are the only cases `ALL`
); /// may hold without a test of their own.
case!( #[test]
a_size_change_after_a_resize_lands_the_same_way, fn every_case_runs_without_the_long_scan() {
Case::ResizeSize for case in ALL {
); assert!(
case!( NAMED.contains(&case) || matches!(case, Case::Shuffle(_)),
a_size_change_lands_where_growing_it_that_way_would, "{} runs only in the long seed scan; give it a case here",
Case::Size case.name()
); );
case!( }
every_size_changing_at_once_lands_where_growing_it_that_way_would, }
Case::EverySize
);
case!(
an_alignment_change_lands_where_growing_it_that_way_would,
Case::Align
);
case!(
giving_and_taking_a_movable_region_rebuilds_what_resolves_it,
Case::RegionNode
);
case!(
reordering_a_span_lands_where_growing_it_that_way_would,
Case::Reorder
);
#[test] #[test]
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() { fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
@@ -119,8 +115,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);
} }
}); });
} }
+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}");
}
+60 -4
View File
@@ -103,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
@@ -119,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,
@@ -146,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",
@@ -170,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) {
@@ -284,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()
}, },
@@ -384,6 +400,17 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label label
} }
/// One widget's layout as it stands: the frame its fractions resolved
/// against, the box it was asked in, the box its drawing went in, and what
/// it reported. In window units, which is what both trees are in.
fn record(id: WidgetId, h: &Harness) -> String {
let active = &h.render.active[&id];
format!(
"rel_base {} region {} placement {} size {}",
active.rel_base, active.region, active.placement, active.size,
)
}
/// Runs `case` on the tree `plan` describes, warm and cold, and says where /// Runs `case` on the tree `plan` describes, warm and cold, and says where
/// the two disagree. `seed` chooses only the values a case picks at random, /// the two disagree. `seed` chooses only the values a case picks at random,
/// so one plan under one case is one comparison however it was reached. /// so one plan under one case is one comparison however it was reached.
@@ -400,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);
@@ -413,9 +451,16 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
if 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];
@@ -424,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(),
+2 -2
View File
@@ -42,12 +42,12 @@ fn dump(label: &str, report: &diag::Report, text: WidgetId) {
TraceEvent::DrawRequest { TraceEvent::DrawRequest {
id, id,
region, region,
pixel_size, region_px,
.. ..
} if *id == text => { } if *id == text => {
println!( println!(
" draw in {:.2}x{:.2} region {region:?}", " draw in {:.2}x{:.2} region {region:?}",
pixel_size.x, pixel_size.y region_px.x, region_px.y
) )
} }
TraceEvent::SizeReported { id, size } if *id == text => { TraceEvent::SizeReported { id, size } if *id == text => {