Compare commits

...
Author SHA1 Message Date
iris-aiandClaude Opus 5 2dba90bd0f Grow images in the generated trees
`Image` is the only widget in the repository whose size hint is a length in
pixels -- everything else hints a share, or nothing -- so it is the only one
that exercises a rule beside a hint, a box a widget knows before it is drawn,
and the answer the commit before this one changed. The generated trees had
none, which is why nothing there could reach that case.

`Kind::Image` is a fifth leaf, drawn one time in five, and it steps to a plain
rect when the shrinker reduces it: a picture measures nothing either, but its
length is its own, so the leaf that takes whatever it is given is the simpler
one. The picture is a 64x64 checkerboard of purple and black in 8 px cells,
committed at `src/assets/checkerboard.png` beside the generator that draws it
-- the way `examples/tabs` keeps its own -- and included rather than opened, so
that growing a tree does not depend on a working directory and one seed is one
tree whatever anything else does.

One upload per tree, however many images it grows: a `TextureHandle` is a
counted reference, so the first image in a tree uploads the checkerboard and
every one after it clones the handle. Measured: seed 1 at depth 4 grows 13
images and holds 1 texture, seed 6 grows none and holds none, and
`a_tree_of_images_uploads_one_texture` asserts it. `Image::new` is what a
caller holding a handle needs, since `image` uploads what it is given.

A seed names a tree only while the generator draws the same things in the same
order, so every seed now grows a different tree. The seed list in
`generated.rs` says so: 20 and 86 no longer grow the trees whose defects they
once caught, and both of those live on as shrunk fixtures in `unsettled.rs`,
which are trees rather than numbers. The seeds those fixtures name are
similarly historical, and their file says that too.

Format, clippy with and without layout-diagnostics, and the suite (135 + 19 +
13 + 4) are clean. The cold dump is a new baseline of 34,571 boxes over the 400
depth-5 trees, since the trees themselves changed; all three seed scans pass
over the new ones -- 400 at depth 5 in 62.79s, 1,000 at depth 6 in 160.20s,
2,000 at depth 4 in 299.58s -- which is what actually checks that images lay
out warm the way they do cold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 04:50:31 -04:00
iris-aiandClaude Opus 5 b295c8b97a Read a leftover as a minimum where nothing divides it
A share under a parent that divides nothing is still a share: the pixels and
fraction beside it are taken first, the share fills whatever the box has left,
and where those are already longer than the box they overflow it exactly as
they would without the share. So the length is `max(box, px + rel*box)`, a
minimum the share imposes rather than an addition to what was asked for
(Bryan, 2026-09-20, generalising the same `max` he gave for `Scroll`'s content
length two days earlier).

A span does that. Measured at `77ed7a2`, a probe recording the box it is asked
in, in a 400 px window, under `.wrapper()` against a one-child span:

    rule                      nothing divides   a span divides
    leftover(1)                           400              400
    px(50) + leftover(1)                  400              400
    px(500) + leftover(1)                 400              500
    rel(0.5) + leftover(1)                400              400
    px(500), no leftover                  500              500

One row disagreed, and the same length without the share overflows fine
(drawn -50..450, its alignment centring it), so what swallowed the overflow was
the share. `LayoutLen::declared` refuses to answer for anything carrying
leftover weight, so the non-dividing path never learned the fixed part and fell
back to the offer.

Said as the place the parent gives rather than as a declaration, because that
is what the retained record already keeps: where the fixed part is the longer,
`widget_at` hands the child `fixed.as_desc().fills()` -- a box of that length,
placed by the child's alignment, its own rel base -- which is what a declared
length already comes to, and `active.placed` stores it, so a recomposed subtree
reads the same box without resolving anything again. A place that is already
the child's placement is skipped: a parent that divides has given the share
whatever it was owed, and re-placing a span's slot moved its child.

Which of two lengths is longer is a question in pixels, so it is one operation
with the crossing kept as a window range, and both callers now share it.
`Painter::longer_than` is that operation -- the span's room for the shares it
divides, and a share past the box it was given -- and it narrows this widget's
range where the span replaced it, since a comparison the framework makes on an
arbitrary parent's behalf is one more reason its drawing holds, not the only
one. A `SizeRule::Min` of `rel(1.0)` is the same operation again, which is what
this is (Bryan, 2026-09-20); when that lands it belongs on this path.

`a_share_is_a_minimum_wherever_nothing_divides_it` walks the table above and
holds the two parents to the same length; the crossing case is checked from
both sides, by a window that crosses it and by the rule itself crossing while
the window holds still. Both fail at `77ed7a2` with 400 where 500 is wanted. A
change of rule needs nothing to escalate it: the reported size is the rule
resolved, so the answer changes and the parent refuses its own drawing --
verified by writing the escalation, finding the tests pass without it, and
dropping it.

Format, clippy with and without layout-diagnostics, and the suite (134 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`77ed7a2` across all 34,488 boxes, since no generated tree carries a share with
pixels beside it -- which the next commit changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 04:49:50 -04:00
iris-aiandClaude Opus 5 c2b8bf83de Let a rule beat a hint, and name marking a widget for redraw
A ninth sweep, over the part no earlier round named -- the widget vocabulary and
the builder methods, `Widgets`, the examples, the `util` additions and the
manifests -- and once more over `77ed7a2`, the eighth sweep's own commit and so
itself unreviewed.

A hint overrode a rule. `declared_lens` asked `rules[axis].declared()` first and
fell through to the widget's own `size_hint` whenever that answered `None` --
which it does for a share, since a share is not a declaration. So a widget
carrying `width(leftover(1))` and hinting a pixel length of its own was handed a
box of the hint, against the rule and against the comment inside the function:
"a hint still narrows the box where no rule does". `Painter::size_hint` spells
the same rule-else-hint step three hundred lines up and gets it right, with the
reason written on it; both read `Widgets::exact_len` now, and `declared_lens` is
the part of its answer that needs nobody to divide it. `Image` is the only
widget here whose hint is a declared length, and neither the tests nor the
generator builds one, so nothing in this repository could reach the difference
-- which is why the dump is unchanged and why the test builds a widget of its
own. It records the box it was asked in: 400 with the rule and 50 without, and
50 either way before this.

Marking a widget for redraw had no name. Twenty-one sites under `tests/` said it
as `widgets_mut().get_dyn_mut(id);` with the widget thrown away, five with a
`let _ =` in front, one with a comment explaining what the line was for, and one
wrapped in a local function called `mark`. `Widgets::mark_for_redraw` says it.
`revision_cost.rs` keeps the long spelling and now says why in place: it is
deliberately in the API subset an old worktree also has.

`assert_same_regions` could not see the defect the eighth sweep had just fixed.
It zips the warm and cold id lists, so a list naming one widget twice -- which
is what `width`, `sized` and `align` giving back their own argument produces --
compares fewer boxes than it lists and says nothing about it. It now rejects a
repeated id and two lists of different lengths, which also checks the nine
fixtures that round left alone: all eighteen cases pass.

Bare pairs where the framework has named ones. `random.rs`'s `Lens` and `Aligns`
were `[Option<LayoutLen>; 2]` and `[Option<AxisAlign>; 2]`, read as `[0]`/`[1]`
and zipped against a hand-written `[Axis::X, Axis::Y]`. They are `SizeRules` and
`Align`; `Align` took the `Index<Axis>` every other per-axis pair on this branch
has, and `RegionAlign::from` does the "an axis left out is centred" step two
rigs were spelling per axis. The three sites that wrote the axis pair out say
`Axis::BOTH`, which is what the rest of the layout code says.

`BothAxis<T>`, `AxisT`, `XAxis` and `YAxis` -- 45 lines with a const trait, two
marker types and three accessors -- have no user anywhere in the workspace. They
are the mechanism `impl_axis_index!` replaced, in the file this branch took
`Vec2::axis`/`axis_mut` out of. Deleted, which is a drive-by in a block the
branch was already rewriting; drop it if the scope matters more.

Smaller things, each in its own place: `Wrapper` arrived beside core's
`WidgetWrapper`, one word for a widget that wraps a child and for a dynamic
borrow guard, so the alias is gone and its two uses name `DynBorrower` -- which
is what they are. `Wrapper::new`, `Wrapper::empty` and its `Default` were three
names for one value, two of them unused. `Arena::get_mut` was the only
`pub(crate)` among `pub` siblings on a public type. `Selector` rounded the
pointer onto the pixel grid to do arithmetic on two values already there, losing
the precision the platform gave it for nothing; the step between the regions is
taken on the grid instead. And the two `debug` profile settings carry their
reason where the next reader looks rather than only in the commit that made
them, one of which was about renaming `rest`.

Format, clippy with and without layout-diagnostics, and the suite (132 + 19 + 13
+ 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`77ed7a2` across all 34,488 boxes, and all three seed scans pass: 400 at depth 5
in 63.27s, 1,000 at depth 6 in 160.45s, 2,000 at depth 4 in 302.52s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 03:56:01 -04:00
iris-aiandClaude Opus 5 77ed7a24c0 Say how many widgets a shrunk fixture has, and share what tests repeat
An eighth sweep, over the part no earlier round named: the 6,300 lines of
tests, and once more over the seventh sweep's own commit, which was itself
unreviewed.

Four of the shrunk fuzz fixtures name one widget two or three times. `width`,
`sized` and `align` set a rule on the widget they are given and return its own
id -- only `pad` and `wrapper` make a new one -- so `let sized =
wrapped.width(76).add(..)` and the `let aligned = sized` beside it are three
names for one text. Each name then went into the list of ids the case compares
warm against cold, so a case that says it checks six boxes checks four, and
three doc comments quote that inflated count as the size of the tree the
shrinker reduced to. Measured: `plant` and `plant_fixed` list 6 and hold 4,
`plant_pair` lists 4 and holds 3, `plant_scrolled` lists 8 and holds 7. The
aliases are gone and the counts say what the fixtures build; each rebuilt
fixture was diffed against the old one, and both the widget slots and every
region are identical, for both settings of `swapped`.

`assert_same_regions` sits at the top of `unsettled.rs` and six tests call it.
Seven more spell its body out instead, byte for byte. They call it now, and it
is `#[track_caller]` so the panic names the case.

`tests/gpu/mod.rs` holds the adapter probe and the surface configuration that
`draw_cost` and `chain_cost` had a copy of each -- `config` identical, and the
probe identical but for the feature it asks for. The leak's justification lived
in one file with the other referring to it; it now sits on the thing it is
about. Shared through `#[path]`, the way `scenario/mod.rs` already is.

The mask a widget is clipped by was resolved in three places, two of them a
byte-identical closure. `mask_bounds` takes the slot rather than the widget,
because the third site deliberately reads the slot it saved before the frame:
that a redraw keeps the slot is what it is checking.

`Layered::_revision` was a field nothing reads, incremented to mark the widget
dirty. Two tests in the same file already do that with
`get_dyn_mut`, which is what the underscore was hiding.

`plan.rs` claimed every simplification is strictly smaller, and asserted `<=`.
Measured: 53 of one tree's 101 simplifications keep the widget count, since a
dropped alignment and a simpler leaf both do. The assertion is right and the
claim was not; the comment now gives the argument that does hold.

`generated.rs` said "Seven that have never failed" and "the nine the others
check" of a ten-seed array. The `should_panic` scroll test ended in an
`h.frame()` that cannot run, since `set_root` lays out and is where the panic
comes from. Two `drop(tree)` at the end of their own scope did nothing.

Format, clippy with and without layout-diagnostics, and the suite (131 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
f8aa0c5 across all 34,490 boxes. No library code changed, so the seed scans
have nothing to find. Both GPU rigs were rebuilt and run: chain cost +470% at
depth 64, draw cost ~4.4 us per layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 03:18:18 -04:00
iris-aiandClaude Opus 5 f8aa0c5cdf Stop a scroll asking a question it has already answered
A seventh sweep, over the parts no earlier round named: the tree generator
and the scenario harness, `Fixed`, the headless rig, and once more over the
commit the sixth sweep left, which was itself unreviewed.

`Scroll`'s content box is `answer_px.max(container_len)`, so a scroll whose
content fits has nothing to scroll through and `update_amt` has already put
`amt` at zero. The test choosing between the viewport and a scrolled span
asked `amt != ZERO || content_len != container_len`, where the first
disjunct can never decide it -- the same defect `b7b8d09` removed from the
line above, one operand over. A `debug_assert` of the implication held
across the whole suite, including every scrolling test.

`Fixed::to_scale` and its private `shift_round` arrived on this branch with
no caller and never got one; the only thing that called either was the test
written for them.

`Len::align` wrote `Len` arithmetic out a component at a time, around an
`at.px` that is always zero, where `Len::scale` and the `Add`/`Sub` beside
it say the whole rule in two lines. `LayoutLen::without_leftover` took
`self` where the `apply_leftover` its own doc calls the opposite reading of
the same value takes `&self`.

`run-headless.sh --resize` changed the output's mode but not `out_w`/`out_h`,
which is the extent `replay-touch` scales a recording against -- so
`--resize` with `--replay` put every sample of the gesture somewhere else
and still finished like a run that worked. Both come from one function now.

The generator's plan/build split stranded a comment: "a row takes the height
it is given" describes the size rule `build` derives from `dir`, and it was
left above the `gap` draw, which is the one line it is not about and which
does consume randomness.

Format, clippy with and without layout-diagnostics, and the suite (131 + 19
+ 13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical
to b7b8d09 across all 34,492 boxes, and all three seed scans pass: 400 at
depth 5 in 62.75s, 1,000 at depth 6 in 162.37s, 2,000 at depth 4 in 305.25s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 02:53:02 -04:00
iris-aiandClaude Opus 5 b7b8d09e40 Write a shared constant once, and stop a scroll placing its own content
Two findings from a sweep over the WGSL prelude and the position widgets,
scoped against upstream/main at ca2b4b2.

`module_source` already builds each shader's preamble from iris_core's own
constants, so the move-chain work's second copy of `MOVE_NONE` and
`CHAIN_LIMIT` -- under "keep in step with iris_core::CHAIN_LIMIT" -- asked a
reader by hand for what the mechanism beside it exists to do. Both are
injected now, with `MASK_NONE` beside them replacing a bare literal, and the
shader declares none of them.

`Scroll`'s `content_len` is never less than its box, so `slack` and the
`anchor` computed from it were always zero whatever the alignment: the
framework centres short content by placing the answer in the whole box, and
the comment credited arithmetic that could not have done it. The same belief
guarded the fits-in-the-box contract with `align == NEG`, so at the default
alignment -- the middle -- every box change redrew the scroll, measured as 1
widget against 0 at TOP_LEFT. `align` now has no reader at all.

`UiSpan::translated` and `UiRegion::translated` are reachable only from each
other and from nothing else.

Format, clippy with and without layout-diagnostics, and the 131-test suite
are clean. The cold dump over 400 depth-5 trees is byte-identical to
1096c31, and all three seed scans pass: 400 at depth 5 in 69.07s, 1,000 at
depth 6 in 169.29s, 2,000 at depth 4 in 300.75s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 02:09:41 -04:00
iris-ai 1096c3167a Drop the last thing nothing reads
`Painter::text_data` had no caller. It was left in the previous round because
it is the only way a widget inside `draw` can reach `TextData`, and the app's
pending integration might have wanted it; nothing in iris is kept for the
app's sake, since the app is to be largely rewritten against this API rather
than ported call by call (Bryan, 2026-09-20).
2026-09-20 01:30:35 -04:00
iris-ai 445287c95c Ask a value a question through a reference
A method taking `self` can only be called on a value, so anywhere the caller
holds a reference it has to dereference to ask -- which costs the caller
whether or not the type is `Copy` (Bryan, 2026-09-20, correcting the opposite
change made in 7502176).

So every method that answers a question about a value takes `&self`:
`Holds`'s four, `AxisHolds` and `LayoutHolds`'s three each, `LayoutLen`'s
`is_px`, `is_only_leftover`, `declared` and `fills`, and `Size::within_box`.
The two callers passing `LayoutLen::declared` as a function value say the
closure instead.

Builders that return a changed copy, and methods on a handle that is meant to
be given up, still take `self`.
2026-09-20 01:30:29 -04:00
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
53 changed files with 2196 additions and 1685 deletions

No files matched your search

+6
View File
@@ -25,6 +25,12 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"]
[workspace] [workspace]
members = ["core", "macro", "rig-input"] members = ["core", "macro", "rig-input"]
# Full debug info was the bulk of what the linker wrote here and almost none of
# what anything read. `dev` keeps line tables and scopes, which is what stepping
# through an example wants; the tests keep the line tables alone, which is what
# a backtrace reads. Measured when the tests became one target: relinking them
# went from 9.8 s to 7.7 s with these, and target/ from 45 GB to 13 GB with the
# two changes together.
[profile.dev] [profile.dev]
debug = 1 debug = 1
+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)
-27
View File
@@ -131,14 +131,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
self.0 as f32 / Self::one().0 as f32 self.0 as f32 / Self::one().0 as f32
} }
/// The same value on another grid, rounded where the new one is coarser.
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
Fixed(match TO >= SHIFT {
true => self.0 << (TO - SHIFT),
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
})
}
pub const fn add(self, rhs: Self) -> Self { pub const fn add(self, rhs: Self) -> Self {
Self(self.0.wrapping_add(rhs.0)) Self(self.0.wrapping_add(rhs.0))
} }
@@ -246,16 +238,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
} }
} }
/// Back to a single step, rounding halves away from zero so that a value and
/// its negation round to the same distance.
const fn shift_round(v: i64, bits: u32) -> i64 {
let half = (1i64 << bits) >> 1;
match v < 0 {
true => -((-v + half) >> bits),
false => (v + half) >> bits,
}
}
const fn div_round(num: i64, den: i64) -> i64 { const fn div_round(num: i64, den: i64) -> i64 {
let (q, rem) = (num / den, num % den); let (q, rem) = (num / den, num % den);
match rem.unsigned_abs() * 2 >= den.unsigned_abs() { match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
@@ -531,15 +513,6 @@ mod tests {
assert_eq!(Px::from_f32(-1e12), Px::MIN); assert_eq!(Px::from_f32(-1e12), Px::MIN);
} }
#[test]
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
// A third, which neither grid holds exactly.
let third = Rel::ONE / Rel::from_int(3);
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
let coarse = Fixed::<6>::from_raw(21);
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
}
#[test] #[test]
fn lerp_takes_the_fraction_as_the_receiver() { fn lerp_takes_the_fraction_as_the_receiver() {
let (from, to) = (Px::from_int(10), Px::from_int(20)); let (from, to) = (Px::from_int(10), Px::from_int(20));
+100 -92
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,100 +23,71 @@ use std::{
time::Instant, time::Instant,
}; };
/// Declares a counter or timer kind beside the name its report prints. Two
/// lists in the same order was one list too many: a variant inserted without
/// its label moving with it renames every total after it, and nothing says
/// so.
macro_rules! labelled {
($(#[$meta:meta])* $vis:vis enum $Name:ident { $($variant:ident = $label:literal,)* }) => {
$(#[$meta])*
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
$vis enum $Name { $($variant,)* }
impl $Name {
const COUNT: usize = [$($label,)*].len();
const NAMES: [&'static str; Self::COUNT] = [$($label,)*];
}
};
}
labelled! {
pub(crate) enum Counter { pub(crate) enum Counter {
Updates, Updates = "updates",
DrawRequests, DrawRequests = "draw requests",
WidgetDraws, WidgetDraws = "widget draws",
RegionNodeDraws, RegionNodeDraws = "region-node draws",
SizeReads, SizeReads = "draw-result size reads",
HintHits, HintHits = "hint hits",
HintMisses, HintMisses = "hint misses",
ReuseAttempts, ReuseAttempts = "reuse attempts",
ReuseExact, ReuseExact = "reuse exact",
ReuseMoved, ReuseMoved = "reuse moved",
ReuseDirty, ReuseDirty = "reuse: dirty",
ReuseWrongParent, ReuseUndrawn = "reuse: nothing drawn to keep",
ReuseRemapped, ReuseWrongParent = "reuse: wrong parent",
ReuseOutside, ReuseRemapped = "reuse remapped",
ReuseWrongLayer, ReuseOutside = "reuse: outside what it holds for",
ReuseWrongNode, ReuseWrongLayer = "reuse: another layer",
QueuePops, ReuseWrongNode = "reuse: region-node choice changed",
DepthReads, ReuseWrongMask = "reuse: a different inherited mask",
LocalRedraws, QueuePops = "redraw queue pops",
SizeChanges, DepthReads = "depth reads",
ReaderEdges, LocalRedraws = "local redraws",
PrimitiveWrites, SizeChanges = "size changes",
TextRenders, ReaderEdges = "reader edges",
TextShapeHits, PrimitiveWrites = "primitive writes",
TextShapes, TextRenders = "text renders",
TextBreaks, TextShapeHits = "text shape hits",
GlyphPlacements, TextShapes = "text shapes",
OutsidePinnedLen, TextBreaks = "text line breaks",
OutsideFrame, GlyphPlacements = "glyph placements",
OutsideExtent, OutsidePinnedLen = "reuse outside: the length it was pinned to",
OutsideWindow = "reuse outside: this window",
OutsideRelBase = "reuse outside: a rel base",
OutsideRegion = "reuse outside: a region length",
}
} }
impl Counter { labelled! {
const COUNT: usize = Self::OutsideExtent as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"updates",
"draw requests",
"widget draws",
"region-node draws",
"draw-result size reads",
"hint hits",
"hint misses",
"reuse attempts",
"reuse exact",
"reuse moved",
"reuse: dirty",
"reuse: wrong parent",
"reuse remapped",
"reuse: outside what it holds for",
"reuse: another layer",
"reuse: region-node choice changed",
"redraw queue pops",
"depth reads",
"local redraws",
"size changes",
"reader edges",
"primitive writes",
"text renders",
"text shape hits",
"text shapes",
"text line breaks",
"glyph placements",
"reuse outside: the length it was pinned to",
"reuse outside: a frame length",
"reuse outside: an extent length",
];
}
#[derive(Clone, Copy)]
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)]
@@ -251,6 +222,8 @@ pub enum ReuseOutcome {
Dirty, Dirty,
WrongParent, WrongParent,
WrongLayer, WrongLayer,
WrongMask,
WrongNode,
Remapped, Remapped,
Outside, Outside,
Undrawn, Undrawn,
@@ -264,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 {
@@ -360,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(
@@ -369,7 +342,7 @@ pub(crate) fn draw_request(
id, id,
parent, parent,
region, region,
pixel_size, region_px,
region_node, region_node,
}, },
); );
@@ -379,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;
+10 -26
View File
@@ -1,8 +1,9 @@
use crate::util::impl_axis_index;
use crate::{Px, Rel}; use crate::{Px, Rel};
use super::*; use super::*;
#[derive(Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Align { pub struct Align {
pub x: Option<AxisAlign>, pub x: Option<AxisAlign>,
pub y: Option<AxisAlign>, pub y: Option<AxisAlign>,
@@ -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);
@@ -172,13 +151,15 @@ impl Vec2 {
} }
impl Len { impl Len {
/// This length placed in the box it is measured in: the alignment names a
/// point along that box, and the two ends are that point less the part of
/// the length falling before it and plus the part falling after.
pub const fn align(&self, align: AxisAlign) -> UiSpan { pub const fn align(&self, align: AxisAlign) -> UiSpan {
let rel = align.rel(); let rel = align.rel();
let rest = Rel::ONE.sub(rel);
let at = Len::from_parts(rel, Px::ZERO); let at = Len::from_parts(rel, Px::ZERO);
UiSpan { UiSpan {
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))), start: at - self.scale(rel),
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))), end: at + self.scale(Rel::ONE.sub(rel)),
} }
} }
} }
@@ -231,3 +212,6 @@ impl RegionAlign {
UiVec2::from(self) UiVec2::from(self)
} }
} }
impl_axis_index!(RegionAlign => AxisAlign);
impl_axis_index!(Align => Option<AxisAlign>);
+5 -80
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 {
@@ -104,47 +71,5 @@ impl Vec2 {
} }
} }
pub const trait AxisT { impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
fn get() -> Axis; impl_axis_index!(Vec2 => f32);
}
pub struct XAxis;
const impl AxisT for XAxis {
fn get() -> Axis {
Axis::X
}
}
pub struct YAxis;
const impl AxisT for YAxis {
fn get() -> Axis {
Axis::Y
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct BothAxis<T> {
pub x: T,
pub y: T,
}
impl<T> BothAxis<T> {
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
match A::get() {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub fn take_axis<A: const AxisT>(self) -> T {
match A::get() {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
+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 -60
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,17 +269,17 @@ impl UiSpan {
} }
} }
pub const fn len(&self) -> Len { /// A box `len` long inside this one, on the side `align` says. Both must
self.end - self.start /// 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)
} }
/// Both ends by the same amount, which is what moving a box without pub const fn len(&self) -> Len {
/// changing its length does to every part of it. self.end - self.start
pub const fn translated(self, by: Len) -> Self {
Self {
start: self.start + by,
end: self.end + by,
}
} }
} }
@@ -316,17 +291,6 @@ pub struct UiRegion {
} }
impl UiRegion { impl UiRegion {
/// Every part of the box by the same amount on each axis. Done to the
/// whole region rather than an end at a time, because that is what it is
/// -- and because four adds in a row are four adds, where four asked for
/// separately are four sequences.
pub const fn translated(self, x: Len, y: Len) -> Self {
Self {
x: self.x.translated(x),
y: self.y.translated(y),
}
}
pub const FULL: Self = Self { pub const FULL: Self = Self {
x: UiSpan::FULL, x: UiSpan::FULL,
y: UiSpan::FULL, y: UiSpan::FULL,
@@ -348,20 +312,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 +412,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);
-4
View File
@@ -133,10 +133,6 @@ impl TextBuffer {
} }
} }
pub fn new_empty() -> Self {
Self::new("")
}
pub fn text(&self) -> &str { pub fn text(&self) -> &str {
&self.text &self.text
} }
-4
View File
@@ -167,10 +167,6 @@ impl GlyphAtlas {
pub fn page_count(&self) -> u32 { pub fn page_count(&self) -> u32 {
self.pages.len() as u32 self.pages.len() as u32
} }
pub fn glyph_count(&self) -> usize {
self.entries.len()
}
} }
impl Page { impl Page {
+13 -4
View File
@@ -23,13 +23,22 @@ pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
fn module_source(wgsl: &str) -> String { fn module_source(wgsl: &str) -> String {
// The steps come from the same constants the CPU counts in, rather than // Every number both sides count in, written once here rather than a
// a second copy of them written into the shader: a grid the two disagree // second time in the shader: a grid the two disagree about puts every
// about puts every coordinate somewhere else. // coordinate somewhere else, and a sentinel they disagree about makes one
// of them walk a chain from a slot the other says is not there.
format!( format!(
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}", "const PX_STEP: f32 = 1.0 / {}.0;\n\
const REL_STEP: f32 = 1.0 / {}.0;\n\
const MASK_NONE: u32 = {}u;\n\
const MOVE_NONE: u32 = {}u;\n\
const CHAIN_LIMIT: u32 = {}u;\n\
{PRELUDE}\n{wgsl}",
1u32 << crate::PX_SHIFT, 1u32 << crate::PX_SHIFT,
1u32 << crate::REL_SHIFT, 1u32 << crate::REL_SHIFT,
MaskIdx::NONE.idx(),
MoveIdx::NONE.idx(),
crate::CHAIN_LIMIT,
) )
} }
+6 -10
View File
@@ -26,9 +26,11 @@ struct MoveOffset {
parent: u32, parent: u32,
} }
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants: // `PX_STEP`, `REL_STEP`, `MASK_NONE`, `MOVE_NONE` and `CHAIN_LIMIT` are
// what it stores is a whole count of each, both powers of two, so decoding // prepended from `iris_core`'s own constants, so none of them is written
// is exact and the number here is the number the CPU decided. // twice. What the CPU stores is a whole count of each step, and both steps
// are powers of two, so decoding is exact and the number here is the number
// the CPU decided.
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that // Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
// composes to within half a step of a pixel boundary is on that boundary and // composes to within half a step of a pixel boundary is on that boundary and
@@ -70,12 +72,6 @@ struct Region {
y: UiSpan, y: UiSpan,
} }
const MOVE_NONE: u32 = 4294967295u;
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
// rather than any real tree, and the CPU walk uses the same number so both
// resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u;
// The same expression `Len::within` uses, in floats rather than on the // The same expression `Len::within` uses, in floats rather than on the
// CPU's grid: a move is resolved here so that scrolling a subtree writes one // CPU's grid: a move is resolved here so that scrolling a subtree writes one
// entry instead of walking it. What has to hold is that this agrees with // entry instead of walking it. What has to hold is that this agrees with
@@ -171,7 +167,7 @@ fn vs_main(
} }
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> { fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
if in.mask_idx == 4294967295u { if in.mask_idx == MASK_NONE {
return color; return color;
} }
let mask = masks[in.mask_idx]; let mask = masks[in.mask_idx];
+35 -27
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
@@ -10,29 +10,24 @@ use crate::{
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// Where its drawing goes, in its region node's coordinates. /// Where its drawing goes, in its region node's coordinates.
pub extent: UiRegion, pub placement: UiRegion,
/// What a fraction declared or reported under this widget is a fraction /// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window. /// of, as a length of the window.
pub frame: UiVec2, pub rel_base: UiVec2,
/// A frame its parent decided for it on each axis -- a row's slot, or /// Where its drawing was put, and where it was asked. The two differ
/// padding's frame less its pixels -- as a length of the window. `None` /// where a container asks in one place and puts the answer in another --
/// forwards the parent's frame. What it declared is kept separately in /// a row measures from its cursor and puts the child in its slot. Each
/// `declared` and is a fraction of whichever of the two reached it. /// carries the rel base that ask stated, so asking again from either is
pub narrow: [Option<Len>; 2], /// the same question it was.
/// Where its drawing was put, and where it was asked, each as a part of pub placed: PlaceDesc,
/// its parent's box. The two differ where a container asks in one place pub asked: PlaceDesc,
/// and puts the answer in another -- a row measures from its cursor and
/// puts the child in its slot. A part is a length from the box's start,
/// so a box that moved re-places every child by re-adding that start.
pub placed: [Place; 2],
pub asked: [Place; 2],
/// The box it was asked in, in the parent's region-node coordinates: the /// The box it was asked in, in the parent's region-node coordinates: the
/// box its drawing was made in and the one its contract is about. Its /// box its drawing was made in and the one its contract is about. Its
/// drawing is placed elsewhere by re-expression, never by asking again. /// drawing is placed elsewhere by re-expression, never by asking again.
pub part: UiRegion, pub region: 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>,
/// Asked more than once in its parent's last draw -- measured in one box /// 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 /// 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 /// rests on the first answer and its drawing on the last, so only the
@@ -40,8 +35,8 @@ pub struct ActiveData {
pub re_asked: bool, pub re_asked: bool,
/// What the widget reported, in window-unit lengths. /// What the widget reported, in window-unit lengths.
pub size: Size, pub size: Size,
/// The window and extent reads that this drawing holds for, and the /// The window and region reads that this drawing holds for, and the
/// frame and box it pinned. /// 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>,
@@ -51,24 +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 `extent` uses when this widget /// The movable region whose coordinates its placement is in when this
/// does not own a region node. /// 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.
@@ -86,6 +80,20 @@ impl ActiveData {
/// all. Not `size`, which is what its last drawing reported: a drawing /// all. Not `size`, which is what its last drawing reported: a drawing
/// re-expressed in the box that answer chose is not a second answer. /// re-expressed in the box that answer chose is not a second answer.
pub fn measured(&self) -> Option<Size> { pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size) self.answer.map(|answer| answer.size)
}
/// Whether it owns a region node rather than sharing the one it was drawn
/// under, which is what its two move indices being different says.
pub fn is_region_node(&self) -> bool {
self.move_idx != self.parent_move
} }
} }
/// 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,
}
+8 -2
View File
@@ -33,7 +33,13 @@ impl Holds {
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw() len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
} }
pub const fn and(self, other: Self) -> Self { /// 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 {
Self { Self {
lo: self.lo.max(other.lo), lo: self.lo.max(other.lo),
hi: self.hi.min(other.hi), hi: self.hi.min(other.hi),
@@ -51,7 +57,7 @@ impl Holds {
/// boxes therefore give one length. That is a floor rather than an /// boxes therefore give one length. That is a floor rather than an
/// allowance: inverting it is two divisions and nothing else, and the /// allowance: inverting it is two divisions and nothing else, and the
/// whole of a box maps back to itself. /// whole of a box maps back to itself.
pub const fn through(self, len: Len) -> Self { pub const fn through(&self, len: Len) -> Self {
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() { if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
return Self::ANY; return Self::ANY;
} }
+84 -55
View File
@@ -1,79 +1,108 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2}; 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 window lengths its reads /// symbolic lengths of that box and of its rel base where either one is what
/// hold for, the pixel lengths of its own box, and the symbolic lengths of /// it was expressed in.
/// that box and of its frame where either one is what it was expressed in.
/// ///
/// The symbolic lengths are pins rather than ranges: 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. A box pin reaches the parent only where the box it pinned is the /// is. A box pin reaches the parent only where the box it pinned is the
/// parent's own; anywhere else the parent chose that length itself, and a /// parent's own; anywhere else the parent chose that length itself, and a
/// widget pinned this way is checked when it is re-placed. /// widget pinned this way is checked when it is re-placed.
/// ///
/// A frame pin says the answer or the drawing is a fraction of the frame, /// A rel base pin says the answer or the drawing is a fraction of the rel base,
/// which is a different length wherever the frame is a different one -- at /// 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 /// the same window size, so no range of window pixels can say it. A length
/// of the frame that is only pixels is not one: it is that many pixels /// of the rel base that is only pixels is not one: it is that many pixels
/// whatever the frame turns out to be. /// 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 window: [Holds; 2], pub x: AxisHolds,
pub frame_len: [Option<Len>; 2], pub y: AxisHolds,
pub extent: [Holds; 2],
pub extent_len: [Option<Len>; 2],
} }
impl LayoutHolds { impl LayoutHolds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
window: [Holds::ANY; 2], x: AxisHolds::ANY,
frame_len: [None; 2], y: AxisHolds::ANY,
extent: [Holds::ANY; 2],
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.window[n] = self.window[n].and(other.window[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]
);
debug_assert!(
self.frame_len[n].is_none()
|| other.frame_len[n].is_none()
|| self.frame_len[n] == other.frame_len[n]
);
result.extent_len[n] = self.extent_len[n].or(other.extent_len[n]);
result.frame_len[n] = self.frame_len[n].or(other.frame_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.window[n].lo <= other.window[n].lo
&& self.window[n].hi >= other.window[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))
&& self.frame_len[n].is_none_or(|len| other.frame_len[n] == Some(len))
})
} }
pub fn contains(self, window: PxVec2, frame: UiVec2, 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.window[n].contains(window.axis(axis))
&& self.frame_len[n].is_none_or(|pinned| pinned == frame.axis(axis))
&& self.extent[n].contains(len.to_px(window.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)]
+343 -262
View File
@@ -1,33 +1,32 @@
#[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, Rel, Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, 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, Placing}, 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,
/// This widget's frame, per axis: a length of the window, and what a /// This widget's rel base, per axis: a length of the window, and what a
/// fraction it or anything under it declares or reports is a fraction /// fraction it or anything under it declares or reports is a fraction
/// of. A length rather than a box, so padding can take from both the /// of. A length rather than a box, so padding can take from both the
/// frame and the box without either becoming the other. /// rel base and the box without either becoming the other.
pub(super) frame: UiVec2, pub(super) rel_base: UiVec2,
/// Where this widget's drawing goes, in its region node's coordinates. /// The box this widget was asked in, in its region node's coordinates:
pub(super) extent: UiRegion, /// what it draws in, and what its children's places are parts of.
/// The extent's symbolic length where this draw read it, which makes the pub(super) region: UiRegion,
/// 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],
/// The window in pixels. Frames and boxes become pixels against this one /// The window in pixels. Frames and boxes become pixels against this one
/// unit, regardless of region-node boundaries. /// unit, regardless of region-node boundaries.
pub(super) window: PxVec2, pub(super) window: PxVec2,
@@ -35,19 +34,19 @@ pub struct Painter<'a> {
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 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 the window in pixels, per axis: every /// What this draw itself reads, as against what its children's drawings
/// window 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) window_own: [Holds; 2], /// reads one, then that one unless it says otherwise, and the rel base or
/// Its frame's symbolic length where this draw read it, which makes the /// region length it read symbolically, each of which makes the drawing
/// drawing one that holds for that frame alone. /// hold for that length alone.
pub(super) frame_own_len: [Option<Len>; 2], pub(super) own: LayoutHolds,
/// The window reads' equivalent for its own box.
pub(super) extent_own: [Holds; 2],
/// What each child's drawing depends on. Asking a child again replaces /// What each child's drawing depends on. Asking a child again replaces
/// its drawing, so it replaces this too rather than narrowing it. /// its drawing, so it replaces this too rather than narrowing it.
pub(super) under: Vec<(WidgetId, LayoutHolds)>, pub(super) under: Vec<(WidgetId, LayoutHolds)>,
@@ -73,10 +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 its region /// A box in this widget's region, composed into its region node's
/// node's coordinates. /// coordinates.
fn resolve(&self, region: UiRegion) -> UiRegion { fn resolve(&self, region: UiRegion) -> UiRegion {
region.within(&self.extent) region.within(&self.region)
} }
fn write_resolved<P: Primitive>( fn write_resolved<P: Primitive>(
@@ -104,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);
@@ -129,59 +127,93 @@ 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, [None; 2], [Place::Within(Part::All); 2]) self.widget_at(id, UiRegion::FULL)
}
/// Resolves what the place says about the child's rel base into a length,
/// 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. /// Asks a child, saying what its fractions are of and where it is asked.
/// ///
/// `narrow` is a length this widget decided for the child's frame, per /// `place` says where the child goes and what its fractions are of:
/// axis, as a length of this widget's own frame: a resolved share, or a /// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which
/// box a sibling's answer decided. `None` forwards this widget's frame, /// is a box of this widget's own with the answer placed inside it.
/// which is what a container that only divides room passes, so a
/// fraction under it means the same wherever it sits and however deeply
/// it is nested. A declared length narrows the frame here whatever the
/// caller says. A narrowed frame is placed in the part by the child's
/// alignment and is the box the child is asked in.
/// ///
/// `place` is where the child is asked, per axis, as a part of this /// The child draws once, in the region that comes of it, and its answer
/// widget's box: see [`Place`]. The child draws once, in that box, and /// is placed inside that region by re-expressing the drawing. Nothing is
/// its answer is placed inside it by re-expressing the drawing. Nothing /// drawn again in a box an answer chose; a container that puts the
/// is drawn again in a box an answer chose; a container that puts the
/// answer somewhere else says so with [`Self::place_at`]. /// 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>,
narrow: [Option<Len>; 2], place: impl Into<PlaceDesc>,
place: [Place; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let mut 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 align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
let (frame, extent) = // A share fills what the pixels and fraction beside it leave of the
frame_and_extent(self.extent, self.frame, place, narrow, declared, align); // box and overflows where they are longer, which is the rule a span
// follows with one child. Only the overflow is a box of the child's
// own: a share that fits is the box it was given, which is what this
// place already says.
for axis in Axis::BOTH {
if let Some(len) = self.share_past_the_offer(id.id(), place, align, axis) {
place[axis] = len.as_desc().fills();
}
}
let declared = self.declared_lens(id);
let (rel_base, region) =
place.rel_base_and_region(self.region, self.rel_base, declared, align);
#[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, extent); diag::region_node(id.id(), self.id, region);
} }
// A child listed twice would be moved twice. // A child listed twice would be moved twice.
let re_asked = self.children.contains(&id.id()); let re_asked = self.children.contains(&id.id());
if !re_asked { if !re_asked {
self.children.push(id.id()); self.children.push(id.id());
} }
let px = frame.to_px(self.window); let drawn = self.state.draw_inner(
let (size, answer_holds, holds) = self.state.draw_inner(
id.id(), id.id(),
DrawInfo { DrawInfo {
layer: self.layer, layer: self.layer,
@@ -190,19 +222,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, rel_base,
part: extent, region,
placed: place, placed: place,
asked: place, asked: place,
narrow,
re_asked, re_asked,
px,
}, },
None, None,
self.rsc, self.rsc,
); );
let holds = self.in_parent(holds, extent, place, narrow, declared); let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
let answer_holds = self.in_parent(answer_holds, extent, place, narrow, declared); 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)),
@@ -210,7 +240,7 @@ impl<'a> Painter<'a> {
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size, size: drawn.answer.size,
answer_holds, answer_holds,
} }
} }
@@ -224,79 +254,127 @@ impl<'a> Painter<'a> {
self.state.undraw_rec(id.id(), self.rsc); self.state.undraw_rec(id.id(), self.rsc);
} }
/// Puts a child asked about in this draw somewhere else in this /// Puts a child in `place` of this widget's box, where that box is the
/// widget's box: its answer, placed in this part instead. The drawing /// answer the child already gave: the drawing is re-expressed there
/// is re-expressed there rather than made again -- what a row does once /// rather than made again -- what a row does once it knows every slot,
/// it knows every slot, having measured each child from its cursor. /// having measured each child from its cursor.
pub fn place_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, place: [Place; 2]) { ///
debug_assert!( /// A child this draw has not asked about, and one whose rel base this
self.children.contains(&id.id()), /// narrows, is asked here instead: there is no answer to re-express, or
"'{}' placed a child it did not ask about in this draw", /// the question has changed. So a container that places every child the
self.label() /// 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(); let at = self.placing();
self.state.place_in(id.id(), &at, place, self.rsc); self.state.place_in(id.id(), &at, place, self.rsc);
let active = &self.state.active[&id.id()];
let size = active.measured().unwrap_or(active.size);
DrawResult {
child: id,
painter: self,
size,
// Read where it was asked; moving it is not a second answer.
answer_holds: LayoutHolds::ANY,
}
} }
/// This widget as the thing its children are placed within. /// This widget as the thing its children are placed within.
fn placing(&self) -> Placing { fn placing(&self) -> Placing {
Placing { Placing {
id: self.id, id: self.id,
extent: self.extent, region: self.region,
frame: self.frame, rel_base: self.rel_base,
window: self.window,
depth: self.depth, depth: self.depth,
move_idx: self.move_idx, move_idx: self.move_idx,
mask: self.mask, mask: self.mask,
} }
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// What a rule or a hint declares a widget's lengths to be, which whoever
/// it resolves into its frame. Reading them depends on nothing -- the box /// 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 /// that comes of them is kept on the child, and `redraw` compares it
/// there. /// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] { fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> Declared {
declared_lens(self.rsc.widgets(), id.id()) self.rsc.widgets().declared_lens(id.id())
}
/// The box a child's own share asks for where that is longer than the box
/// `place` gives it, and nothing where the share fits.
///
/// A share is a length only to whoever divides one, and nothing divides a
/// box handed to one child: what is left of it after the pixels and the
/// fraction beside the share is what the share takes, so the length comes
/// to the whole box until those are longer than it and to them once they
/// are. Only that second case is a box this widget did not give, and the
/// crossing between them is a question in pixels, so this widget's drawing
/// holds for the windows on one side of it. Narrowed rather than stated,
/// because this widget may have read its own box as well, and a range it
/// pinned for that still holds.
fn share_past_the_offer(
&mut self,
id: WidgetId,
place: PlaceDesc,
align: RegionAlign,
axis: Axis,
) -> Option<Len> {
// A place that is the child's placement outright is a box its parent
// decided, and a parent that divides one has already given the share
// whatever it was owed. Only an offer -- a box with the answer still
// to be placed inside it -- is a box a share reads.
if place[axis].fills {
return None;
}
// A share with nothing beside it is the box whatever the box is, so
// there is no comparison to make and no range to keep for one.
let stated = self.rsc.widgets().exact_len(id, axis)?;
if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
return None;
}
let fixed = stated
.without_leftover()
.within_len(place.base(axis, self.rel_base));
let offer = place.of(self.region, align)[axis].len();
self.longer_than(fixed, offer, axis).then_some(fixed)
} }
/// What a child says its length is without being drawn, if it can say, /// What a child says its length is without being drawn, if it can say,
/// as the length its draw would report: a fraction in it is resolved /// as the length its draw would report: a fraction in it is resolved
/// against this widget's frame, which is the frame a child asked with /// against this widget's rel base, which is the rel base a child asked with
/// nothing narrowed gets. Asking counts as reading its size. /// 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 hint = self.rsc.widgets().exact_len(id.id(), axis);
// A rule is the answer where there is one: it wins over whatever the let rel_base = self.rel_base[axis];
// widget would draw, so it has to win over what the widget says too. let resolved = hint.map(|hint| hint.within_len(rel_base));
let hint = widgets
.size_rules(id.id())
.axis(axis)
.exact()
.or_else(|| {
widgets
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis))
})
.map(|hint| hint.within_len(self.frame.axis(axis)));
#[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,
});
}
if let Some(hint) = hint {
self.depend_on(id); self.depend_on(id);
// A fraction was just resolved against this frame, so what // Resolving a fraction against this rel base makes this draw a
// this draw does with it is a function of the frame's length. // function of the rel base's length. The fraction to ask about is
// the child's own: resolved against a rel base of pixels, none is
// left to see it by.
if hint.rel != Rel::ZERO { if hint.rel != Rel::ZERO {
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis)); self.own[axis].rel_base = Some(rel_base);
}
Some(hint)
}
None => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintMisses);
None
} }
} }
resolved
} }
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
@@ -317,11 +395,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() {
@@ -353,25 +431,25 @@ 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);
len len
} }
/// The symbolic length of this widget's frame along one axis: what a /// This widget's rel base along one axis: what a fraction it or anything
/// fraction it or anything under it declares is a fraction of. A /// under it declares or reports is a fraction of. A container reads it
/// container reads it to hand a length of it down -- padding, which /// to hand a length of it down -- padding, which takes its pixels off.
/// takes its pixels off. Reading it pins the drawing to that frame, the /// Reading it pins the drawing to that rel base, the way
/// way [`Self::extent_len`] pins it to the box. /// [`Self::region_len`] pins it to the box.
pub fn frame_len(&mut self, axis: Axis) -> Len { pub fn rel_base(&mut self, axis: Axis) -> Len {
let len = self.frame.axis(axis); let len = self.rel_base[axis];
self.frame_own_len[axis as usize] = Some(len); self.own[axis].rel_base = Some(len);
len len
} }
@@ -392,10 +470,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()
} }
@@ -409,13 +484,13 @@ impl<'a> Painter<'a> {
/// 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.window.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
} }
/// 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
@@ -423,15 +498,15 @@ 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.window.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;
} }
/// A window length in pixels, which is what every length in layout is /// A window length in pixels, which is what every length in layout is
@@ -439,9 +514,9 @@ impl<'a> Painter<'a> {
/// length is a fraction of it; one that is only pixels is that many /// length is a fraction of it; one that is only pixels is that many
/// pixels in any window and pins nothing. /// pixels in any window and pins nothing.
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px { pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let window = self.window.axis(axis); let window = self.window[axis];
if len.rel != Rel::ZERO { if len.rel != Rel::ZERO {
let own = &mut self.window_own[axis as usize]; let own = &mut self.own[axis].window;
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(window); *own = Holds::at(window);
} }
@@ -449,6 +524,40 @@ impl<'a> Painter<'a> {
len.to_px(window) len.to_px(window)
} }
/// Whether `len` is longer than `than`, kept as the windows that comparison
/// comes out the same way on: a drawing that took one of two lengths holds
/// where the same one is the longer, and nowhere else.
///
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than 300
/// px at a box of 600 and shorter at 400 -- and it is asked of the
/// difference and answered back through that same difference, so the
/// boundary is the drawing's own rather than a second way of finding it.
/// Narrowed rather than stated, because whatever else this widget read
/// about the window is a reason its drawing holds where it does too.
///
/// This is the one operation a length that is the longer of two needs: the
/// room a container has left for the shares it divides, and a share that
/// overflows the box it was given because the pixels beside it are longer
/// than the box.
pub fn longer_than(&mut self, len: Len, than: Len, axis: Axis) -> bool {
let over = len - than;
let window = self.window[axis];
let longer = over.to_px(window) > Px::ZERO;
let side = match longer {
true => Px::STEP..=Px::MAX,
false => Px::MIN..=Px::ZERO,
};
let holds = Holds::from(side).through(over);
debug_assert!(
holds.contains(window),
"'{}' ({:?}) compared two lengths and kept a range without this window",
self.label(),
self.id
);
self.own[axis].window = self.own[axis].window.and(holds);
longer
}
/// The windows this drawing holds for, stated rather than taken: a /// The windows this drawing holds for, stated rather than taken: a
/// container that branched on a length in pixels says which side of the /// container that branched on a length in pixels says which side of the
/// boundary it was on, which is wider than the one window reading that /// boundary it was on, which is wider than the one window reading that
@@ -456,16 +565,12 @@ impl<'a> Painter<'a> {
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) { 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.window.axis(axis)), holds.contains(self.window[axis]),
"'{}' ({:?}) says its drawing holds for windows that leave out this one", "'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(), self.label(),
self.id self.id
); );
self.window_own[axis as usize] = holds; self.own[axis].window = holds;
}
pub fn text_data(&mut self) -> &mut TextData {
&mut self.rsc.ui_mut().text
} }
pub fn child_layer(&mut self) { pub fn child_layer(&mut self) {
@@ -520,7 +625,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]
} }
} }
@@ -549,50 +654,46 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// Moves what a child depends on into this widget's own terms: this
/// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// only what the child was asked with.
impl Painter<'_> { impl Painter<'_> {
/// Window ranges are already about the one unit and combine directly. /// Moves what a child depends on into this widget's own terms, taking
/// A frame pin becomes this widget's own frame wherever a length of it /// only what the child was asked with.
/// is what reached the child; where only pixels did, no length of this
/// frame can change the child's and the pin stops here.
/// ///
/// Extent validity maps back through the part of this widget's box, /// Window ranges are already about the one unit and combine directly.
/// A rel base pin becomes this widget's own rel base wherever a length of it
/// is what reached the child; where only pixels did, no length of this
/// rel base can change the child's and the pin stops here.
///
/// A child's validity maps back through the part of this widget's box,
/// where the box the child was asked in is that part; a declared length /// where the box the child was asked in is that part; a declared length
/// places the box inside the part instead, and then only that length /// places the box inside the part instead, and then only that length
/// reaches the child. A narrowed frame is not one of these: it decides /// reaches the child. A narrowed rel base is not one of these: it decides
/// what fractions under the child mean and leaves the box the part it /// what fractions under the child mean and leaves the box the part it
/// was given. /// was given.
fn in_parent( fn in_parent(
&self, &self,
holds: LayoutHolds, holds: LayoutHolds,
extent: UiRegion, region: UiRegion,
place: [Place; 2], place: PlaceDesc,
narrow: [Option<Len>; 2], declared: Declared,
declared: [Option<LayoutLen>; 2],
) -> LayoutHolds { ) -> LayoutHolds {
let mut result = LayoutHolds::ANY; let mut result = LayoutHolds::ANY;
for axis in AXES { for axis in Axis::BOTH {
let n = axis as usize; let declared = declared[axis];
let holds = holds[axis];
let at = place[axis];
let result = &mut result[axis];
// Every read became pixels against the window, so a range on // Every read became pixels against the window, so a range on
// it is already in this widget's terms. // it is already in this widget's terms.
result.window[n] = holds.window[n]; result.window = holds.window;
let reaches = narrow[n].is_none() // A length this widget named -- a resolved share, a box a sibling
&& !matches!(place[n].part(), Part::Sized(_)) // decided, a box it sized outright, which is its own base -- is
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO); // not a length of this widget's rel base, so a pin on it stops
result.frame_len[n] = holds.frame_len[n].and(reaches.then(|| self.frame.axis(axis))); // here. So does a declaration in pixels: no length of either base
match (place[n].part(), declared[n].is_some()) { // is in it to see.
// Its box is this widget's own, or a part of it in that let reaches = !matches!(at.rel_base, RelBase::Len(_))
// box's own lengths: so what it holds for is a range on this && declared.is_none_or(|len| len.rel != Rel::ZERO);
// widget's own box, which is what lets that box move without result.rel_base = holds.rel_base.and(reaches.then(|| self.rel_base[axis]));
// a redraw. A length it pinned is this widget's length match (at.span, declared) {
// wherever the part is the whole of it, and pins the same
// way.
(Part::All, false) => {
result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n];
}
// Its box is a part of this widget's own box, in that box's // Its box is a part of this widget's own box, in that box's
// own lengths, so what it holds for maps back through that // own lengths, so what it holds for maps back through that
// part into a range on this widget's box. A length it pinned // part into a range on this widget's box. A length it pinned
@@ -600,21 +701,20 @@ impl Painter<'_> {
// part is the whole of the box less pixels, which is the one // part is the whole of the box less pixels, which is the one
// shape that inverts exactly; any other part pins this // shape that inverts exactly; any other part pins this
// widget's own length. // widget's own length.
(Part::Of(span), false) => { (PlaceSpan::Within(span), None) => {
let part_len = span.len(); let part_len = span.len();
result.extent[n] = holds.extent[n].through(part_len); result.region = holds.region.through(part_len);
result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel { result.region_len = holds.region_len.map(|pinned| match part_len.rel {
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px), Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
_ => self.extent.axis(axis).len(), _ => self.region[axis].len(),
}); });
} }
// Its box is a length this widget decided, from its own // Its box is a length this widget decided, from its own
// frame or from a sibling's answer: no length of this // 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 // widget's box reaches it, so what it holds for is a range
// on the window and none of it on that box. // on the window and none of it on that box.
_ => { _ => {
result.window[n] = result.window = result.window.and(holds.region.through(region[axis].len()));
result.window[n].and(holds.extent[n].through(extent.axis(axis).len()));
} }
} }
} }
@@ -622,120 +722,101 @@ impl Painter<'_> {
} }
} }
/// What a widget declares a length of its box to be. `leftover` is not one: a impl Widgets {
/// share of what is left over is only a length to the widget dividing one, /// What says a widget's length on one axis without drawing it, if anything
/// so it passes up in the size instead. /// does. A rule is the answer where there is one: it wins over whatever the
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] { /// widget would draw, so it has to win over what the widget says too -- a
let rules = widgets.size_rules(id); /// share included, since a share is a length only to whoever divides one,
let widget = widgets.get_dyn(id); /// and that is the parent rather than this widget.
AXES.map(|axis| { fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
rules.axis(axis).declared().or_else(|| { self.size_rules(id)[axis].exact().or_else(|| {
// A hint still narrows the box where no rule does, which is how a // 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 // widget with a natural pixel size -- an image, a gap -- gets that
// size rather than the whole offer. That is the offer's business // size rather than the whole offer. That is the offer's business
// rather than a declaration's, and this falls away once a widget // rather than a declaration's, and this falls away once a widget
// occupies its reported size inside the box it was offered. // occupies its reported size inside the box it was offered.
widget self.get_dyn(id)?.size_hint(axis)
.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 /// What a widget's box is where a rule or its own hint gives one outright,
/// is in rather than a part to be placed inside it. A share fills, because a /// rather than a share for whoever draws it to divide.
/// share is a length only to whoever divides one, and whoever did is the one pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
/// that handed down this box. A declared axis does too: the rule already gave Declared::from_axes(|axis| self.exact_len(id, axis)?.declared())
/// 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
} }
impl LayoutLen {
/// 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(super) fn fills(&self, declared: Option<Len>, decided: bool) -> bool {
self.leftover != Weight::ZERO || declared.is_some() || decided
}
}
impl PlaceDesc {
/// Where a widget's drawing goes inside the part its parent gave it: what /// 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 /// it reported, on the side of the part its alignment says, and the whole
/// part wherever the answer fills it. /// part wherever the answer fills it.
/// ///
/// The length it reported is a length of its frame, and the part is one too, /// The length it reported is a length of its rel base, and the part is one
/// so this takes one from the other rather than composing it into the part. /// too, so this takes one from the other rather than composing it into the
/// That is what makes a fraction the same fraction wherever the part it is /// part. That is what makes a fraction the same fraction wherever the part
/// placed in sits and however long it is -- the fraction is resolved once, /// it is placed in sits and however long it is -- the fraction is resolved
/// here, against the frame it was reported of. /// once, here, against the rel base it was reported of.
pub(crate) fn placed_extent( pub(super) fn placement(
part: UiRegion, self,
region: UiRegion,
size: Size, size: Size,
declared: [Option<LayoutLen>; 2], declared: Declared,
fill: [bool; 2],
align: RegionAlign, align: RegionAlign,
) -> UiRegion { ) -> UiRegion {
let mut placed = part; let mut placed = region;
for axis in AXES { for axis in Axis::BOTH {
let n = axis as usize; let reported = size[axis];
let reported = size.axis(axis); if reported.fills(declared[axis], self[axis].fills) {
if fills(reported, declared[n], fill[n]) {
continue; continue;
} }
let len = Len::from_parts(reported.rel, reported.px); placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
let span = placed.axis_mut(axis);
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
} }
placed placed
} }
/// The frame length and the box a child is asked in, in the coordinates the /// The rel base length and the box a child is asked in, in the coordinates the
/// widget asking draws in. /// widget asking draws in.
/// ///
/// `own` is that widget's own box, and `place` what of it the child is /// `own` is that widget's own box, and `place` what of it the child is
/// given. `narrow` is a frame the container decided for the child -- a row's /// given, including any rel base it states -- a row's slot, or padding's rel
/// slot, or padding's frame less its pixels -- and [`Part::Sized`] one a /// base less its pixels. That is a window length, like every other length
/// sibling's answer decided; both are window lengths, like every other /// here, since a slot of a row is not a fraction of anything the row can
/// length here, since a slot of a row is not a fraction of anything the row /// name. The child's declaration is a fraction of whichever reached it, and
/// can name. The child's declaration is a fraction of whichever reached it, /// is the only one that also places the box: a box the caller decided is
/// and is the only one of the three that also places the box: a box the /// what `place` names.
/// caller decided is what `place` names. pub(super) fn rel_base_and_region(
pub(crate) fn frame_and_extent( self,
own: UiRegion, own: UiRegion,
parent_frame: UiVec2, parent_rel_base: UiVec2,
place: [Place; 2], declared: Declared,
narrow: [Option<Len>; 2],
declared: [Option<LayoutLen>; 2],
align: RegionAlign, align: RegionAlign,
) -> (UiVec2, UiRegion) { ) -> (UiVec2, UiRegion) {
let part = part_of(own, place, align); let given = self.of(own, align);
let mut frame = parent_frame; let mut rel_base = parent_rel_base;
let mut extent = part; let mut region = given;
for axis in AXES { for axis in Axis::BOTH {
let n = axis as usize; let base = self.base(axis, parent_rel_base);
let sized = match place[n].part() { let len = declared[axis]
Part::Sized(len) => Some(len), .map(|len| len.within_len(base))
_ => None,
};
let base = sized
.or(narrow[n])
.unwrap_or_else(|| parent_frame.axis(axis));
let len = declared[n]
.map(|len| Len::from_parts(len.rel, len.px).within_len(base))
.unwrap_or(base); .unwrap_or(base);
*frame.axis_mut(axis) = len; rel_base[axis] = len;
if declared[n].is_some() { if declared[axis].is_some() {
let slot = part.axis(axis); region[axis] = given[axis].place(len, align[axis]);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*extent.axis_mut(axis) = UiSpan::new(start, start + len);
} }
} }
(frame, extent) (rel_base, region)
} }
/// The part of a widget's own box a `place` names, in the coordinates that
/// box is in.
fn part_of(extent: UiRegion, place: [Place; 2], align: RegionAlign) -> UiRegion {
let mut part = extent;
for axis in AXES {
*part.axis_mut(axis) = place[axis as usize]
.part()
.of(*extent.axis(axis), align.axis(axis));
}
part
} }
+193 -52
View File
@@ -1,66 +1,205 @@
use crate::{AxisAlign, Len, PrimitiveHandle, UiRegion, UiSpan}; use crate::util::impl_axis_index;
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan, UiVec2};
/// 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,
/// Window 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 window }
/// length, so the cursor that sums those reports is one too. A moved box
/// re-places every child by re-adding its start, exactly. A fraction #[derive(Clone, Copy, Debug, PartialEq)]
/// here is a fraction of the window and not of the box -- the whole of a pub enum PlaceSpan {
/// box is [`Self::All`], not a `rel(1.0)` span. Within(UiSpan),
From(UiSpan), Shifted(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 window 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),
/// A box of this length, wherever in the parent's box the child's own
/// alignment puts it, and that same length as its frame. Unlike `From`,
/// it is a length decided from above rather than a place along a
/// container's cursor -- what a stack's sizing child decides for the
/// rest.
Sized(Len), Sized(Len),
} }
impl Part { /// What a child's fractions are of. [`PlaceSpan::Sized`] is a length the
/// Where it lands in the coordinates `extent` is in. /// caller named, which is always its own base, so nothing here constructs one
pub(crate) fn of(self, extent: UiSpan, align: AxisAlign) -> UiSpan { /// beside anything but [`Self::Len`].
match self {
Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Of(span) => span.within(&extent),
Self::Sized(len) => {
let start = extent.start + (extent.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
}
}
}
/// Where a child goes along one axis, as a part of this widget's box.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum Place { pub enum RelBase {
/// The child's answer, aligned inside the part by the child's alignment. /// The caller's own, unchanged.
Within(Part), Inherit,
/// Exactly the part; the answer is not placed inside it again. /// The caller's own, narrowed the way the region is.
Fill(Part), WithRegion,
/// This length of the window.
Len(Len),
} }
impl Place { impl PlaceDescAxis {
pub(crate) fn part(self) -> Part { /// The whole of the caller's box.
match self { pub const WHOLE: Self = UiSpan::FULL.within_desc();
Self::Within(part) | Self::Fill(part) => part,
/// 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),
}
} }
} }
/// Whether the part is the drawing's box outright, rather than the box /// Where a child is asked, on both axes. A [`UiRegion`] converts into the
/// the answer is placed inside. /// common case: that box of the caller's own, the answer placed inside it.
pub(crate) fn fills(self) -> bool { #[derive(Clone, Copy, Debug, PartialEq)]
matches!(self, Self::Fill(_)) pub struct PlaceDesc {
pub x: PlaceDescAxis,
pub y: PlaceDescAxis,
}
impl PlaceDesc {
/// The whole of the caller's box, on both axes.
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
Self { x, y }
}
/// The same on both axes.
pub const fn splat(place: PlaceDescAxis) -> Self {
Self { x: place, y: place }
}
/// 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),
}
}
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
pub const fn fills(self) -> Self {
Self::new(self.x.fills(), self.y.fills())
}
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
self[axis] = self[axis].rel_base(len);
self
}
/// What a child's fractions on one axis are of, as a length of the
/// window: a length this place names, or the rel base of the widget
/// giving it, which is `parent_rel_base`.
pub(super) fn base(&self, axis: Axis, parent_rel_base: UiVec2) -> Len {
match self[axis].rel_base {
RelBase::Len(len) => len,
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
}
}
/// 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)
} }
} }
@@ -71,3 +210,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
+1 -1
View File
@@ -35,7 +35,7 @@ impl<T, I: IdNum> Arena<T, I> {
self.data[i] self.data[i]
} }
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T { pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
&mut self.data[id.idx()] &mut self.data[id.idx()]
} }
} }
+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;
+28 -29
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(|len| len.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 { /// What a widget's box is on each axis where something says so outright,
Axis::X => self.x, /// before it is drawn: a rule beside it, or a hint it gives about itself.
Axis::Y => self.y, /// Whoever draws the widget resolves these against its rel base.
///
/// 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),
}
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule { impl_axis_index!(Declared => Option<Len>);
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
+15 -9
View File
@@ -30,6 +30,14 @@ impl Widgets {
!self.needs_redraw.is_empty() !self.needs_redraw.is_empty()
} }
/// Marks this widget for the next frame to draw again, with nothing about
/// it changed. Taking a widget mutably marks it too, which is the ordinary
/// content-change signal; this is for a change the borrow cannot express,
/// and for asking for the same tree over again.
pub fn mark_for_redraw(&mut self, id: impl IdLike) {
self.needs_redraw.insert(id.id());
}
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> { pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
Some(self.vec.get(id)?.widget.as_ref()) Some(self.vec.get(id)?.widget.as_ref())
} }
@@ -41,14 +49,14 @@ impl Widgets {
/// get_dyn but dynamic borrow checking of widgets /// get_dyn but dynamic borrow checking of widgets
/// lets you do recursive (tree) operations, like the painter does /// lets you do recursive (tree) operations, like the painter does
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> { pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> DynBorrower<'a, dyn Widget> {
// SAFETY: must guarantee no other mutable references to this widget exist // SAFETY: must guarantee no other mutable references to this widget exist
// done through the borrow variable // done through the borrow variable
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) }; let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
if data.borrowed { if data.borrowed {
panic!("tried to mutably borrow the same widget twice"); panic!("tried to mutably borrow the same widget twice");
} }
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed) DynBorrower::new(data.widget.as_mut(), &mut data.borrowed)
} }
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget> pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
@@ -130,10 +138,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,14 +155,14 @@ 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);
} }
/// Both axes at once, for a caller holding a pair. /// Both axes at once.
pub fn set_size_rules( pub fn set_size_rules(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
@@ -188,8 +196,6 @@ impl Default for Widgets {
} }
} }
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
impl<I: IdLike> std::ops::Index<I> for Widgets impl<I: IdLike> std::ops::Index<I> for Widgets
where where
I::Widget: Sized + Widget, I::Widget: Sized + Widget,
+11 -6
View File
@@ -106,11 +106,16 @@ export WAYLAND_DISPLAY
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2 echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
swaymsg output HEADLESS-1 mode "$mode" >/dev/null # The extent `replay-touch` positions against, so a script's coordinates are
# The extent `replay-touch` positions against, so a script's coordinates # the output's own pixels. Set beside every mode change, since a gesture
# are the output's own pixels. # scaled against a mode the output no longer has lands somewhere else and
out_w=${mode%x*} # still looks like a run that worked.
out_h=${mode#*x}; out_h=${out_h%@*} set_mode() {
swaymsg output HEADLESS-1 mode "$1" >/dev/null
out_w=${1%x*}
out_h=${1#*x}; out_h=${out_h%@*}
}
set_mode "$mode"
# Built before the app starts, so a compile error is not reported as a # Built before the app starts, so a compile error is not reported as a
# window that failed to move. # window that failed to move.
@@ -149,7 +154,7 @@ while [ $i -lt "$((seconds * 2))" ]; do
done done
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
swaymsg output HEADLESS-1 mode "$resize" >/dev/null set_mode "$resize"
echo "run-headless: resized to $resize" >&2 echo "run-headless: resized to $resize" >&2
sleep 2 sleep 2
fi fi
Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

+4 -3
View File
@@ -15,9 +15,10 @@ where
let region = ctx.data.render.window_region(&id).unwrap(); let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left; let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left; let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
// The pointer arrives from the platform in floats; everything // The two regions are on the grid and the pointer is not, so the
// it is compared against is on the grid. // step between them is taken there and the pointer keeps the
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32(); // precision the platform gave it.
let pos = ctx.data.pos + (container_pos - id_pos).to_f32();
let size = region.size().to_f32(); let size = region.size().to_f32();
select( select(
rsc, rsc,
+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>) {
+60 -35
View File
@@ -8,23 +8,16 @@
use crate::prelude::*; use crate::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
/// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<LayoutLen>; 2];
/// Where one widget carrying an alignment sits, by axis. `None` uses the
/// centered default.
pub type Aligns = [Option<AxisAlign>; 2];
/// What a test changes between two trees grown from the same seed, so the /// What a test changes between two trees grown from the same seed, so the
/// warm one can be mutated and the cold one grown that way to begin with. /// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)] #[derive(Default)]
pub struct Edits { pub struct Edits {
/// Declared sizes, by the order the rules were put on. /// Declared sizes, by the order the rules were put on.
pub sizes: HashMap<usize, Lens>, pub sizes: HashMap<usize, SizeRules>,
/// Which children a span has, by the order the spans were made. /// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>, pub spans: HashMap<usize, SpanEdit>,
/// Alignments, by the order they were put on. /// Alignments, by the order they were put on.
pub aligns: HashMap<usize, Aligns>, pub aligns: HashMap<usize, Align>,
/// Which widgets own a movable region, by the order they were offered /// Which widgets own a movable region, by the order they were offered
/// one. Region nodes change what a move writes and how deep a primitive's /// one. Region nodes change what a move writes and how deep a primitive's
/// chain is, so a tree that never grows one leaves both untested. /// chain is, so a tree that never grows one leaves both untested.
@@ -118,9 +111,9 @@ 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, [None; 2], [Place::Within(Part::All), top]) .widget_at(&self.probe, top.on_axis(Axis::Y))
.len(Axis::X); .len(Axis::X);
let len = measured.apply_leftover(); let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X); let px = painter.to_px(len, Axis::X);
@@ -134,11 +127,11 @@ impl Widget for Branch {
}; };
painter.window_holds(Axis::X, holds.through(len)); painter.window_holds(Axis::X, holds.through(len));
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 > threshold { match px > threshold {
true => painter.widget_at(&self.wide, [None; 2], place), true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, [None; 2], place), false => painter.widget_at(&self.narrow, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
@@ -176,9 +169,11 @@ pub struct Plan {
/// it one and the offer is taken or declined; a second offer to the same /// it one and the offer is taken or declined; a second offer to the same
/// widget is dropped, because two rules on one widget would settle in the /// widget is dropped, because two rules on one widget would settle in the
/// order they were applied rather than in grow order. /// order they were applied rather than in grow order.
pub size: Option<Lens>, pub size: Option<SizeRules>,
/// The alignment it carries, under the same one-offer rule. /// The alignment it carries, under the same one-offer rule. An axis left
pub align: Option<Aligns>, /// out takes the centered default, which is what [`RegionAlign`] reads it
/// as.
pub align: Option<Align>,
/// Whether it was offered a movable region of its own and what it /// Whether it was offered a movable region of its own and what it
/// answered. `Some(false)` is an offer declined, which still uses up the /// answered. `Some(false)` is an offer declined, which still uses up the
/// one offer, where `None` is an offer never made. /// one offer, where `None` is an offer never made.
@@ -195,6 +190,9 @@ pub enum Kind {
color: usize, color: usize,
alpha: u8, alpha: u8,
}, },
/// The one leaf whose own length is a number of pixels it knows before it
/// is drawn, which is the hint a rule beside it has to win over.
Image,
/// Scrolling reads the pixel length of its box, which nothing else here /// Scrolling reads the pixel length of its box, which nothing else here
/// does, and gives its child a box longer than its own. /// does, and gives its child a box longer than its own.
Scroll { Scroll {
@@ -448,9 +446,11 @@ impl Kind {
} }
match self { match self {
// The one leaf that reads the width it is given, then the one // The one leaf that reads the width it is given, then the one
// that does not, then the one that measures nothing at all. // that does not, then the one that measures nothing at all. A
// picture measures nothing either, but its length is its own, so
// it steps to the leaf that takes whatever it is given.
Kind::Wrapped => out.push(Kind::OneLine), Kind::Wrapped => out.push(Kind::OneLine),
Kind::OneLine => out.push(Kind::Rect { Kind::OneLine | Kind::Image => out.push(Kind::Rect {
color: 0, color: 0,
alpha: 255, alpha: 255,
}), }),
@@ -632,9 +632,10 @@ struct Sow<'a> {
impl Sow<'_> { impl Sow<'_> {
fn leaf(&mut self) -> Plan { fn leaf(&mut self) -> Plan {
Plan::bare(match self.rng.below(4) { Plan::bare(match self.rng.below(5) {
0 => Kind::Wrapped, 0 => Kind::Wrapped,
1 => Kind::OneLine, 1 => Kind::OneLine,
2 => Kind::Image,
_ => { _ => {
let color = self.rng.below(COLORS.len()); let color = self.rng.below(COLORS.len());
let alpha = (self.rng.below(5) * 63) as u8; let alpha = (self.rng.below(5) * 63) as u8;
@@ -651,7 +652,7 @@ impl Sow<'_> {
} }
} }
fn align(&mut self) -> Aligns { fn align(&mut self) -> Align {
let axis = |s: &mut Self| match s.rng.below(4) { let axis = |s: &mut Self| match s.rng.below(4) {
0 => None, 0 => None,
1 => Some(AxisAlign::NEG), 1 => Some(AxisAlign::NEG),
@@ -661,15 +662,21 @@ impl Sow<'_> {
let (x, y) = (axis(self), axis(self)); let (x, y) = (axis(self), axis(self));
// Aligning on neither axis leaves the branch unexercised. // Aligning on neither axis leaves the branch unexercised.
match x.is_none() && y.is_none() { match x.is_none() && y.is_none() {
true => [Some(AxisAlign::CENTER), y], true => Align {
false => [x, y], x: Some(AxisAlign::CENTER),
y,
},
false => Align { x, y },
} }
} }
/// A declared size over half the tree, kept where a test can change it. /// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: &mut Plan) { fn sized(&mut self, inner: &mut Plan) {
let take = self.rng.chance(); let take = self.rng.chance();
let lens = [self.len(), self.len()]; let lens = SizeRules {
x: self.len().into(),
y: self.len().into(),
};
if !take || inner.size.is_some() { if !take || inner.size.is_some() {
return; return;
} }
@@ -773,10 +780,6 @@ impl Sow<'_> {
self.spans += 1; self.spans += 1;
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default(); let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
let dir = self.rng.below(4); let dir = self.rng.below(4);
// A row takes the height it is given rather than its tallest child,
// which is a rule beside it. Derived from an existing choice and
// consuming no randomness: a seed must keep growing the same tree
// when the generator gains another configuration.
let gap = self.rng.below(3) as i32 * 4; let gap = self.rng.below(3) as i32 * 4;
let grown: Vec<usize> = (0..children.len()).collect(); let grown: Vec<usize> = (0..children.len()).collect();
let order = span_edited(&grown, children.len(), spares.len(), &edit); let order = span_edited(&grown, children.len(), spares.len(), &edit);
@@ -796,6 +799,7 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
let mut build = Build { let mut build = Build {
rsc, rsc,
tree: Tree::default(), tree: Tree::default(),
checkerboard: None,
}; };
let root = build.node(plan); let root = build.node(plan);
(root, build.tree) (root, build.tree)
@@ -804,6 +808,10 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
struct Build<'a, Rsc> { struct Build<'a, Rsc> {
rsc: &'a mut Rsc, rsc: &'a mut Rsc,
tree: Tree, tree: Tree,
/// The checkerboard, uploaded when the first image in this tree is built.
/// A handle is a reference to the texture, so every image after that one
/// clones this rather than uploading the same picture again.
checkerboard: Option<TextureHandle>,
} }
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> { impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
@@ -811,16 +819,14 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
let built = self.kind(&plan.kind); let built = self.kind(&plan.kind);
let id = built.id(); let id = built.id();
if let Some(lens) = plan.size { if let Some(lens) = plan.size {
self.rsc self.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.tree.sized.push(id); self.tree.sized.push(id);
} }
if let Some(align) = plan.align { if let Some(align) = plan.align {
let resolved = RegionAlign::from(align);
let widgets = &mut self.rsc.ui_mut().widgets; let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { for axis in Axis::BOTH {
widgets.set_alignment(id, axis, align.unwrap_or_default()); widgets.set_alignment(id, axis, resolved[axis]);
} }
self.tree.aligned.push(id); self.tree.aligned.push(id);
} }
@@ -831,6 +837,20 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
built built
} }
/// The one picture the generated trees draw: a 64x64 checkerboard of purple
/// and black in 8 px cells. Committed rather than drawn here, so that one
/// seed is one tree whatever anything else does, and included rather than
/// opened, so that growing a tree does not depend on a working directory.
fn checkerboard(&mut self) -> TextureHandle {
if self.checkerboard.is_none() {
let image = include_bytes!("assets/checkerboard.png")
.get_image()
.expect("the checkerboard is committed beside this file");
self.checkerboard = Some(self.rsc.ui_mut().textures.add(image));
}
self.checkerboard.clone().unwrap()
}
fn kind(&mut self, kind: &Kind) -> StrongWidget { fn kind(&mut self, kind: &Kind) -> StrongWidget {
let id: StrongWidget = match kind { let id: StrongWidget = match kind {
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc), Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
@@ -839,6 +859,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
.wrap(false) .wrap(false)
.add_strong(self.rsc), .add_strong(self.rsc),
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc), Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
Kind::Image => Image::new(self.checkerboard()).add_strong(self.rsc),
Kind::Scroll { axis, inner } => { Kind::Scroll { axis, inner } => {
let inner = self.node(inner); let inner = self.node(inner);
let id = Scroll::new(inner, *axis).add(self.rsc); let id = Scroll::new(inner, *axis).add(self.rsc);
@@ -915,6 +936,10 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
gap: Px::from_int(*gap), gap: Px::from_int(*gap),
} }
.add(self.rsc); .add(self.rsc);
// A row takes the height it is given rather than its tallest
// child, which is a rule beside the span rather than anything
// it draws. Derived from `dir` rather than stored, so a plan
// that says the direction says this too.
if dir.axis == Axis::X { if dir.axis == Axis::X {
self.rsc self.rsc
.widgets_mut() .widgets_mut()
+10 -1
View File
@@ -12,7 +12,16 @@ 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]))
}
}
impl Image {
/// One texture already uploaded, for a caller holding its handle: [`image`]
/// uploads what it is given, and several widgets showing one picture want
/// one upload and one slot between them.
pub fn new(handle: TextureHandle) -> Self {
Self { handle }
} }
} }
+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, [None; 2], place).size()
} }
} }
+2 -21
View File
@@ -14,31 +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.
// //
// Padding is an inset of both: it comes off the frame, so `rel(1)` // Padding is an inset of both: it comes off the rel base, so `rel(1)`
// under it fills this widget rather than overflowing it by the // under it fills this widget rather than overflowing it by the
// padding, and it comes off the box, so what is drawn sits inside. // padding, and it comes off the box, so what is drawn sits inside.
// The two stay distinct -- the box can be narrower still, where a row // The two stay distinct -- the box can be narrower still, where a row
// asked this widget in the room left, and a text wraps at that. // asked this widget in the room left, and a text wraps at that.
let inset = |lead: Px, trail: Px| { let inner = painter.widget_at(&self.inner, self.padding.region()).size();
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),
];
// Read from this widget's own frame rather than written as a
// fraction of it: a frame is a length of the window like everything
// else here, and taking the padding off is the whole of what this
// widget does to it.
let narrow = [
(Axis::X, self.padding.left + self.padding.right),
(Axis::Y, self.padding.top + self.padding.bottom),
]
.map(|(axis, pixels)| Some(painter.frame_len(axis) - Len::from_parts(Rel::ZERO, pixels)));
let inner = painter.widget_at(&self.inner, narrow, 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,
+33 -35
View File
@@ -14,58 +14,56 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis); let container_len = painter.px_len(self.axis);
// Asked in the whole viewport, then put at the scrolled offset. // Asked in the whole viewport, then put at the scrolled offset.
let answer_len = painter let answer_len = painter
.widget_at(&self.inner, [None; 2], [Place::Fill(Part::All); 2]) .widget_at(&self.inner, PlaceDesc::WHOLE.fills())
.len(self.axis); .len(self.axis);
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis); 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); // Reading the box in pixels above holds this drawing to that one
// Content of a fixed length that fits sits at the start of any box it // length, so these two say where it holds more widely.
// 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 // Content of a fixed length that fits is handed the whole box below,
// the drawing holds for that length alone. One scrolled part way sits // and nothing here reads the box again, so every longer box gives the
// where it is until the box shrinks past what is left of it. Kept to // same drawing: it holds from the length the content needs upwards,
// the end, it moves with every length. // and shrinking past that is what changes it. Where it sits in a box
let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO; // longer than itself is not this widget's to say -- placing its
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { // answer in the whole box is its own alignment, and that placement is
painter.holds(self.axis, fixed..=Px::MAX); // a fraction of the box, so it holds at every length too.
} else if fixed_len && !self.snap_end { //
// One scrolled part way sits where it is until the box shrinks past
// what is left of it. Kept to the end, it moves with every length.
let answer_is_px = answer_len.is_px();
if answer_is_px && self.content_len <= self.container_len {
painter.holds(self.axis, answer_px..=Px::MAX);
} 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);
} }
// Content shorter than the viewport has room to sit in, and where it // Content that fills the viewport is the viewport, and is handed back
// sits is this widget's own alignment -- the same property that would // as it came -- it has nothing to scroll through, so the clamp above
// have placed the whole scroll in a box longer than it. // has already put `amt` at zero. Writing the same box as its own
let slack = (self.container_len - self.content_len).max(Px::ZERO); // length in pixels is the same box in another form, and the two do
let anchor = slack.mul(align.rel()); // not round alike: a part centred in `rel 1` lands a step from one
// Content that fills the viewport and has not been scrolled is the // centred in `px 900`, since halving a difference is not halving each
// viewport, and is handed back as it came. Writing the same box as // part of it.
// its own length in pixels is the same box in another form, and the let content = match self.content_len > self.container_len {
// two do not round alike: a part centred in `rel 1` lands a step from
// one centred in `px 900`, since halving a difference is not halving
// each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
let content = match moved || self.content_len != self.container_len {
true => { true => {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt); let start = Len::from_parts(Rel::ZERO, -self.amt);
Part::From(UiSpan::new(start, start.offset(self.content_len))) UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
} }
false => Part::All, false => PlaceDescAxis::WHOLE,
}; };
// The viewport is the inner's frame, so a fraction it declares or // 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 goes is the content // content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there. // box, scrolled: its drawing moved there, not made again there.
painter.place_at( painter.place_at(&self.inner, content.on_axis(self.axis).fills());
&self.inner,
self.axis.pair(Place::Fill(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
+62 -94
View File
@@ -10,48 +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.extent_len(axis); // the length alone.
let along = |from: Len, to: Len| match self.dir.sign { let row = painter.region_len(axis);
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = Place::Within(Part::All);
// A length for every child before their final slots are chosen: from // A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The frame passes // 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 // 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 // given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the // them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there. // cursor, because a text has to wrap at the width actually there.
// This is the one ask a drawn fixed child gets: its slot is its let mut cursor = Len::ZERO;
// answer, and the drawing is moved there once the shares are known.
// A hinted child is asked once, in its slot.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
let mut measured = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let size = match painter.size_hint(child, axis) { let len = match painter.size_hint(child, axis) {
Some(len) => { Some(len) => len,
measured.push(None);
len
}
None => { None => {
let room = Place::Within(Part::From(along(cursor, far))); // Across itself the child sits where its own alignment
let size = painter // says, in the whole of the row: a span is what contains
.widget_at(child, [None; 2], axis.pair(room, across)) // its children there, and nothing divides that axis.
.size(); let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
measured.push(Some(size)); painter.widget_at(child, room).len(axis)
size.axis(axis)
} }
}; };
let len = size; cursor += len.without_leftover();
cursor.px += len.px + self.gap; cursor.px += self.gap;
cursor.rel += len.rel;
lens.push(len); lens.push(len);
} }
@@ -67,26 +51,16 @@ impl Widget for Span {
); );
// 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 - Len::from_parts(total.rel, total.px); let all_fixed = total.without_leftover();
// Whether anything is left over is a question in pixels: `rel(0.5)` let room = row - all_fixed;
// beside 300 px is full at 600 and overfull at 400. Asked of `room` // The three cases a rounded division needed -- the fixed parts
// itself, and answered back through the same expression, so the
// boundary is the drawing's own and not a second way of finding it:
// the three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the // growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which `through` already reads. What the // sign of `room.rel`, which the range `longer_than` keeps already
// generated oracle checks is the consequence, since which children // reads. What the generated oracle checks is the consequence, since
// exist at all turns on this. // which children 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.longer_than(row, all_fixed, axis);
shares = painter.to_px(room, axis) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.window_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them // rule beside it gives that length outright, and then reading them
@@ -96,65 +70,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), measured) in self.children.iter().zip(&lens).zip(&measured) { // Nothing divides the room where no child asked for any of it, and a
let len = *len; // 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 child's box outright rather than something to place an // is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere // answer inside again. A share is decided here and nowhere
// else: its slot narrows its frame, and the child is asked in // else: its slot narrows its rel base, and the child is asked in
// it, since a text wraps at the width it is actually given. A // it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so a drawing made in the // fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here. // room is put there as it is, and one not made yet is made here.
let slot = along(from, start); let slot = self.slot(row, from, to);
let place = axis.pair(Place::Fill(Part::From(slot)), across); let mut place = slot.shifted_desc().fills().on_axis(axis);
let mut narrow = [None; 2]; if len.leftover > Weight::ZERO && has_room {
if len.leftover > Weight::ZERO && shares { place = place.rel_base(axis, slot.len());
narrow[axis as usize] = Some(slot.len());
} }
let used = match (measured, narrow[axis as usize]) { let used = painter.place_at(child, place).len(!axis);
(Some(size), None) => {
painter.place_at(child, place);
size.axis(!axis)
}
_ => painter.widget_at(child, narrow, place).len(!axis),
};
if shrinks { if shrinks {
// 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
@@ -164,26 +132,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(),
+6 -8
View File
@@ -22,9 +22,7 @@ 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, [None; 2], [Place::Fill(Part::All); 2])
.size()
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
@@ -33,11 +31,11 @@ impl Widget for Stack {
// fraction under them is a fraction of it. A share leaves the axis // 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 // to whoever gave the stack its box. Where a child sits in a box
// bigger than itself is its own business. // bigger than itself is its own business.
let place = [Axis::X, Axis::Y].map(|axis| { let place = PlaceDesc::from_axes(|axis| {
let len = size.axis(axis); let len = size[axis];
match len.leftover == Weight::ZERO { match len.leftover == Weight::ZERO {
true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))), true => len.without_leftover().as_desc().fills(),
false => Place::Within(Part::All), false => PlaceDescAxis::WHOLE,
} }
}); });
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
@@ -45,7 +43,7 @@ impl Widget for Stack {
continue; continue;
} }
painter.child_layer_at(i); painter.child_layer_at(i);
painter.widget_at(child, [None; 2], place); painter.widget_at(child, place);
} }
size size
} }
+1 -3
View File
@@ -321,12 +321,10 @@ impl<'a> TextEditCtx<'a> {
let old = (self.text.view.buf.text().to_string(), self.text.selection); let old = (self.text.view.buf.text().to_string(), self.text.selection);
let mut undo = false; let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo); let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo { if undo && let Some((old, selection)) = self.text.history.pop() {
if let Some((old, selection)) = self.text.history.pop() {
self.set(&old); self.set(&old);
self.text.selection = selection; self.text.selection = selection;
self.clamp_selection_to_layout(); self.clamp_selection_to_layout();
}
} else if self.text.view.buf.text() != old.0 { } else if self.text.view.buf.text() != old.0 {
self.text.history.push(old); self.text.history.push(old);
} }
+2 -2
View File
@@ -19,8 +19,8 @@ widget_trait! {
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
let widgets = &mut state.ui_mut().widgets; let widgets = &mut state.ui_mut().widgets;
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] { for axis in Axis::BOTH {
if let Some(align) = align { if let Some(align) = align[axis] {
widgets.set_alignment(id, axis, align); widgets.set_alignment(id, axis, align);
} }
} }
+2 -11
View File
@@ -9,6 +9,7 @@ use std::marker::Unsize;
/// ///
/// Its child is optional so it can also be the swappable slot a tab bar /// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for. /// needs, which is what it was written for.
#[derive(Default)]
pub struct Wrapper { pub struct Wrapper {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
} }
@@ -26,11 +27,7 @@ impl Wrapper {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn empty() -> Self {
Self {
inner: Default::default(),
}
}
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) { pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
self.inner = Some(to) self.inner = Some(to)
} }
@@ -42,9 +39,3 @@ impl Wrapper {
self.inner.replace(to) self.inner.replace(to)
} }
} }
impl Default for Wrapper {
fn default() -> Self {
Self::empty()
}
}
+9 -9
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, [None; 2], [Place::Within(Part::All), top]) .widget_at(&self.probe, top.on_axis(Axis::Y))
.len(Axis::X); .len(Axis::X);
let px = painter.to_px(measured.apply_leftover(), 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, [None; 2], place), true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, [None; 2], place), false => painter.widget_at(&self.narrow, place),
}; };
Size::LEFTOVER Size::LEFTOVER
} }
@@ -69,8 +69,8 @@ fn a_branch_taken_on_a_measurement_holds_across_repaints() {
assert_ne!(first, (false, false), "threshold {threshold}: neither drew"); assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
for frame in 0..4 { for frame in 0..4 {
h.rsc.widgets_mut().get_dyn_mut(wide); h.rsc.widgets_mut().mark_for_redraw(wide);
h.rsc.widgets_mut().get_dyn_mut(narrow); h.rsc.widgets_mut().mark_for_redraw(narrow);
h.frame(); h.frame();
assert_eq!( assert_eq!(
taken(&h, wide, narrow), taken(&h, wide, narrow),
@@ -88,7 +88,7 @@ fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
let (wide, narrow) = plant(&mut warm, threshold); let (wide, narrow) = plant(&mut warm, threshold);
warm.resize((640, 480)); warm.resize((640, 480));
warm.frame(); warm.frame();
warm.rsc.widgets_mut().get_dyn_mut(wide); warm.rsc.widgets_mut().mark_for_redraw(wide);
warm.frame(); warm.frame();
let mut cold = Harness::new((640, 480)); let mut cold = Harness::new((640, 480));
+1 -1
View File
@@ -18,7 +18,7 @@ fn a_wrapping_text_in_a_span_settles_on_one_width() {
let r = h.region(&t.id()).unwrap(); let r = h.region(&t.id()).unwrap();
widths.push(r.bot_right.x - r.top_left.x); widths.push(r.bot_right.x - r.top_left.x);
// Redrawing it changes nothing about the state, so nothing may move. // Redrawing it changes nothing about the state, so nothing may move.
h.rsc.widgets_mut().get_dyn_mut(t.id()); h.rsc.widgets_mut().mark_for_redraw(t.id());
h.frame(); h.frame();
} }
println!("widths over six frames: {widths:?}"); println!("widths over six frames: {widths:?}");
+147 -9
View File
@@ -1,5 +1,7 @@
//! Where a frame puts things, with no window to put them in. //! Where a frame puts things, with no window to put them in.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners}; use iris::harness::{Harness, assert_corners};
use iris::prelude::*; use iris::prelude::*;
@@ -108,7 +110,7 @@ const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the bo
/// the icon's width, while a wrapping text beside it is asked in the room /// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there. /// left, 900 - 24 - 32, and wraps there.
#[test] #[test]
fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() { fn padding_keeps_the_rel_base_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200)); let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc); 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 fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
@@ -124,8 +126,8 @@ fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0))); h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()]; let active = &h.render.active[&text.id()];
let window = h.render.output_size().x; let window = h.render.output_size().x;
let asked = active.part.x.len().to_px(window); let asked = active.region.x.len().to_px(window);
assert_eq!(active.frame.x.to_px(window), Px::from_int(868)); assert_eq!(active.rel_base.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844)); assert_eq!(asked, Px::from_int(844));
} }
@@ -155,7 +157,7 @@ fn a_share_inside_padding_fills_the_slot_it_was_given() {
/// The same padding in a share instead: the slot is 450, so both the /// 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. /// fraction and the wrap are the slot less the padding, and the two agree.
#[test] #[test]
fn padding_narrows_both_frame_and_box_inside_a_share() { fn padding_narrows_both_rel_base_and_box_inside_a_share() {
let mut h = Harness::new((900, 200)); let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc); 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 padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
@@ -170,8 +172,8 @@ fn padding_narrows_both_frame_and_box_inside_a_share() {
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0))); h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()]; let active = &h.render.active[&text.id()];
let window = h.render.output_size().x; let window = h.render.output_size().x;
assert_eq!(active.frame.x.to_px(window), Px::from_int(418)); assert_eq!(active.rel_base.x.to_px(window), Px::from_int(418));
assert_eq!(active.part.x.len().to_px(window), Px::from_int(418)); assert_eq!(active.region.x.len().to_px(window), Px::from_int(418));
} }
#[test] #[test]
@@ -219,6 +221,105 @@ fn an_empty_widget_takes_a_share_of_a_span() {
assert_corners!(h, right, (300, 0), (400, 200)); assert_corners!(h, right, (300, 0), (400, 200));
} }
/// A widget with a natural pixel size, like an image, which records the box
/// it was asked in so a test can see which length decided it.
struct NaturalSize {
len: f32,
asked: Rc<Cell<f32>>,
}
impl Widget for NaturalSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.asked.set(painter.px_len(Axis::X).to_f32());
Size::px(Vec2::new(self.len, self.len))
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(self.len))
}
}
/// A rule wins over what the widget says about itself, and a share is a rule:
/// it is a length only to whoever divides one, and nobody here does, so the
/// widget is asked in the whole box rather than in the size it asked for.
#[test]
fn a_share_rule_beats_the_widgets_own_pixel_size() {
let mut h = Harness::new((400, 200));
let asked = Rc::new(Cell::new(0.0));
let natural = NaturalSize {
len: 50.0,
asked: asked.clone(),
}
.add(&mut h.rsc);
h.set_root(natural.wrapper());
assert_eq!(asked.get(), 50.0, "its hint gives it its own size");
h.set_len(natural, Axis::X, LayoutLen::LEFTOVER);
h.frame();
assert_eq!(asked.get(), 400.0, "the share is all of the box");
}
/// A share with pixels or a fraction beside it is the longer of the two: it
/// fills what they leave of the box and overflows the box where they are
/// longer than it. A parent that divides nothing gives the same length as a
/// span with one child, because in both there is nobody else to divide with.
#[test]
fn a_share_is_a_minimum_wherever_nothing_divides_it() {
let asked = |rule: LayoutLen, in_a_span: bool| {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, rule);
match in_a_span {
true => h.set_root((probe,).span(Dir::RIGHT)),
false => h.set_root(probe.wrapper()),
}
h.region(&probe).unwrap().size().x
};
for (rule, want) in [
(LayoutLen::LEFTOVER, 400),
(LayoutLen::px(50) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500) + LayoutLen::LEFTOVER, 500),
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500), 500),
] {
let want = Px::from_int(want);
assert_eq!(asked(rule, false), want, "{rule:?} where nothing divides");
assert_eq!(asked(rule, true), want, "{rule:?} in a span");
}
}
/// Which of the two is longer is a question in pixels, so the box is decided
/// again wherever the answer can change: a window that crosses the length the
/// pixels ask for, and the rule itself crossing it while the window holds
/// still. The first is a range the drawing holds for; the second cannot be
/// seen in what the widget declares, since a share declares nothing either
/// way, so it reaches the parent as a length only the parent can resolve.
#[test]
fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER);
h.set_root(probe.wrapper());
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500));
h.resize((900, 200));
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
h.resize((400, 200));
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500));
h.set_len(probe, Axis::X, LayoutLen::px(50) + LayoutLen::LEFTOVER);
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400));
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER);
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500));
}
#[test] #[test]
fn a_child_drawn_twice_moves_once() { fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -500,11 +601,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 region = h.render.moves.resolve(active.move_idx, active.extent); let region = h.render.moves.resolve(active.move_idx, active.placement);
let dim = h.size().axis(axis); let dim = h.size()[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))
} }
@@ -821,3 +922,40 @@ fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
h.set_root(root); h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450)); 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));
}
}
}
+44 -9
View File
@@ -5,7 +5,9 @@
//! and the oracle another. And reducing a plan has to end, or a shrinker //! and the oracle another. And reducing a plan has to end, or a shrinker
//! searching for the smallest counterexample never returns. //! searching for the smallest counterexample never returns.
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan}; use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, grow, plan};
use std::collections::HashMap; use std::collections::HashMap;
fn some_edits(seed: u64, of: &Plan) -> Edits { fn some_edits(seed: u64, of: &Plan) -> Edits {
@@ -25,11 +27,27 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
Edits { Edits {
sizes: pick(sized, &mut rng) sizes: pick(sized, &mut rng)
.into_iter() .into_iter()
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None])) .map(|i| {
(
i,
SizeRules {
x: SizeRule::Exact(LayoutLen::LEFTOVER),
y: SizeRule::Free,
},
)
})
.collect(), .collect(),
aligns: pick(aligned, &mut rng) aligns: pick(aligned, &mut rng)
.into_iter() .into_iter()
.map(|i| (i, [Some(AxisAlign::POS), None])) .map(|i| {
(
i,
Align {
x: Some(AxisAlign::POS),
y: None,
},
)
})
.collect(), .collect(),
nodes: pick(nodes, &mut rng) nodes: pick(nodes, &mut rng)
.into_iter() .into_iter()
@@ -51,8 +69,6 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
} }
} }
use iris::prelude::*;
/// The two routes to an edited tree are one tree. `plan` resolves edits out /// The two routes to an edited tree are one tree. `plan` resolves edits out
/// of the random stream as it draws; `edited` puts them on a tree that /// of the random stream as it draws; `edited` puts them on a tree that
/// already exists, which is the only route a shrunk plan has, since no seed /// already exists, which is the only route a shrunk plan has, since no seed
@@ -70,11 +86,13 @@ fn editing_a_plan_is_growing_one_with_those_edits() {
} }
} }
/// Every simplification is strictly smaller, so taking them in turn reaches a /// No simplification is larger, which is the half of "the shrinker stops" a
/// fixed point instead of circling. A shrinker that can return to a tree it /// widget count can see. Most are not smaller either -- a dropped alignment
/// has already tried does not stop. /// and a simpler leaf both keep the count -- so what rules out circling is
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
/// down a ladder with no way back up.
#[test] #[test]
fn every_simplification_of_a_plan_is_smaller_than_it() { fn no_simplification_of_a_plan_is_larger_than_it() {
for seed in 1..=60 { for seed in 1..=60 {
let tree = plan(seed, 4, &Edits::default()); let tree = plan(seed, 4, &Edits::default());
let mut queue = vec![tree]; let mut queue = vec![tree];
@@ -119,3 +137,20 @@ fn reducing_a_plan_all_the_way_ends() {
); );
} }
} }
/// Every image in a tree is the same picture, and a handle is a reference to
/// the texture rather than a copy of it, so one upload and one slot serve all
/// of them however many a tree grows -- and the trees are grown in hundreds.
#[test]
fn a_tree_of_images_uploads_one_texture() {
let mut images = 0;
let mut tree = plan(1, 4, &Edits::default());
tree.walk_mut(&mut |p| images += (p.kind == Kind::Image) as usize);
assert!(images > 1, "a tree of {images} images tests nothing");
let mut h = Harness::new((900, 1200));
let (root, _) = grow(&mut h.rsc, 1, 4, &Edits::default());
h.state.root = Some(root);
h.frame();
assert_eq!(h.rsc.ui().textures.count(), 1);
}
+311 -90
View File
@@ -45,7 +45,6 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
struct Layered { struct Layered {
children: [StrongWidget<Rect>; 2], children: [StrongWidget<Rect>; 2],
_revision: usize,
} }
impl Widget for Layered { impl Widget for Layered {
@@ -65,14 +64,10 @@ fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
rect(Color::RED).add_strong(&mut h.rsc), rect(Color::RED).add_strong(&mut h.rsc),
rect(Color::BLUE).add_strong(&mut h.rsc), rect(Color::BLUE).add_strong(&mut h.rsc),
]; ];
let root = Layered { let root = Layered { children }.add(&mut h.rsc);
children,
_revision: 0,
}
.add(&mut h.rsc);
h.set_root(root); h.set_root(root);
h.rsc[root]._revision += 1; h.rsc.widgets_mut().mark_for_redraw(root.id());
h.frame(); h.frame();
let label = h.rsc.widgets().label(root.id()); let label = h.rsc.widgets().label(root.id());
@@ -125,7 +120,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));
} }
@@ -181,9 +176,9 @@ fn a_repaint_that_keeps_its_size_does_not_relay_out() {
h.set_root((first, second).span(Dir::RIGHT)); h.set_root((first, second).span(Dir::RIGHT));
let settled = draws.get(); let settled = draws.get();
// Taking mutable access is the ordinary content-change signal. This // Marked with nothing about it changed, and it reports the same size
// widget returns the same size, so the parent has nothing to lay out. // either way, so the parent has nothing to lay out.
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id()); h.rsc.widgets_mut().mark_for_redraw(first.id());
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 1); assert_eq!(draws.get(), settled + 1);
@@ -198,7 +193,7 @@ fn a_span_child_survives_the_next_frame() {
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc); let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN)); h.set_root((top, bottom).span(Dir::DOWN));
h.rsc.widgets_mut().get_dyn_mut(top.id()); h.rsc.widgets_mut().mark_for_redraw(top.id());
h.frame(); h.frame();
assert_corners!(h, top, (0, 0), (400, 80)); assert_corners!(h, top, (0, 0), (400, 80));
@@ -214,11 +209,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,
[None; 2],
[Place::Within(Part::All), Place::Within(Part::From(top))],
);
Size::LEFTOVER Size::LEFTOVER
} }
} }
@@ -664,7 +655,7 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
let masked = inner.masked().add(&mut h.rsc); let masked = inner.masked().add(&mut h.rsc);
let other = rect(Color::RED).width(100).add(&mut h.rsc); let other = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((other, masked).span(Dir::RIGHT)); h.set_root((other, masked).span(Dir::RIGHT));
h.rsc.widgets_mut().get_dyn_mut(masked.id()); h.rsc.widgets_mut().mark_for_redraw(masked.id());
h.frame(); h.frame();
assert_corners!(h, inner, (100, 0), (400, 200)); assert_corners!(h, inner, (100, 0), (400, 200));
} }
@@ -778,6 +769,17 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
); );
} }
/// Where a mask slot clips, in window pixels: the region it holds, carried
/// through whatever move entry it hangs from. Taken by slot rather than by
/// widget, so a test can name the slot it expects a redraw to keep.
fn mask_bounds(h: &Harness, mask: MaskIdx) -> PixelRegion {
let mask = &h.rsc.ui().masks[mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
}
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> { fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id] h.render.active[&id]
.primitives .primitives
@@ -797,7 +799,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);
@@ -877,11 +879,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,
[None; 2], 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
} }
@@ -927,15 +925,10 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
primitive_bounds(&warm, leaf.id()), primitive_bounds(&warm, leaf.id()),
primitive_bounds(&cold, other.id()) primitive_bounds(&cold, other.id())
); );
let mask = |h: &Harness, id: WidgetId| { assert_eq!(
let active = &h.render.active[&id]; mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
let mask = &h.rsc.ui().masks[active.mask.idx()]; mask_bounds(&cold, cold.render.active[&other.id()].mask)
h.render );
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
} }
} }
@@ -959,18 +952,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,
[Some(self.region.x.len()), None], 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
} }
@@ -986,15 +979,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)),
); );
@@ -1002,7 +995,7 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
assert_eq!(draws.get(), before); assert_eq!(draws.get(), before);
let retained = primitive_bounds(&h, text.id()); let retained = primitive_bounds(&h, text.id());
assert!(!retained.is_empty()); assert!(!retained.is_empty());
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id()); h.rsc.widgets_mut().mark_for_redraw(text.id());
h.frame(); h.frame();
assert!(draws.get() > before); assert!(draws.get() > before);
assert_eq!(retained, primitive_bounds(&h, text.id())); assert_eq!(retained, primitive_bounds(&h, text.id()));
@@ -1117,7 +1110,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>>,
@@ -1130,23 +1123,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,
[None; 2], 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);
@@ -1167,7 +1159,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)
}; };
@@ -1182,32 +1174,27 @@ 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));
} }
let mask = |h: &Harness, id: WidgetId| { assert_eq!(
let active = &h.render.active[&id]; mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
let mask = &h.rsc.ui().masks[active.mask.idx()]; mask_bounds(&cold, cold.render.active[&other.id()].mask)
h.render );
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
} }
} }
} }
#[test] #[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 {
@@ -1224,14 +1211,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,
[None; 2], 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
} }
@@ -1256,7 +1241,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,
@@ -1266,18 +1251,14 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter painter
.widget_at( .widget_at(
&self.child, &self.child,
[None; 2], 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 {
@@ -1286,11 +1267,10 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
painter painter
.widget_at( .widget_at(
&self.child, &self.child,
[None; 2], 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(),
); );
@@ -1302,7 +1282,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),
@@ -1320,7 +1300,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);
@@ -1330,15 +1310,256 @@ 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().mark_for_redraw(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();
assert_eq!(mask_bounds(&h, mask), 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"
);
}
+2 -2
View File
@@ -113,7 +113,7 @@ fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
let mut warm = Harness::new((900, 300)); let mut warm = Harness::new((900, 300));
let (text, content) = plant(&mut warm); let (text, content) = plant(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(text); warm.rsc.widgets_mut().mark_for_redraw(text);
warm.frame(); warm.frame();
let mut cold = Harness::new((900, 300)); let mut cold = Harness::new((900, 300));
@@ -142,8 +142,8 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
let mut h = Harness::new((100, 100)); let mut h = Harness::new((100, 100));
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc); let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
let clipper = Clipper(tall).add(&mut h.rsc); let clipper = Clipper(tall).add(&mut h.rsc);
// `set_root` lays the tree out, so this is where it is caught.
h.set_root(clipper); h.set_root(clipper);
h.frame();
} }
/// Content that fits sits in the viewport, not in a box of the window's /// Content that fits sits in the viewport, not in a box of the window's
+88 -102
View File
@@ -9,17 +9,45 @@
//! reached through a region node's own entry rather than through the offer //! reached through a region node's own entry rather than through the offer
//! that node was given. The last is a wrapping text handed back the width //! that node was given. The last is a wrapping text handed back the width
//! it measured, rounded to a step below the line it measured there. //! it measured, rounded to a step below the line it measured there.
//!
//! Each says which seed it was shrunk from, of the generator as it stood when
//! it was found. Those numbers no longer grow those trees -- a seed names one
//! only while the generator draws the same things in the same order, and the
//! leaves have grown an image since -- so what is written out below is the
//! record of the case, and the seed is where it came from.
use std::collections::HashSet;
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch; use iris::random::Branch;
/// Every widget in the same place warm as cold, reported all at once: which
/// of a dozen boxes moved is the whole of what a shrunk case has to say.
///
/// A list that names one widget twice is an error rather than a redundant
/// check. `width`, `sized` and `align` give back the widget they were handed,
/// so a fixture built through them can name one text three times, and then a
/// case comparing six boxes compares four and says nothing about it. One
/// fixture builds both lists, so checking the warm one checks both.
#[track_caller]
fn assert_same_regions( fn assert_same_regions(
warm: &Harness, warm: &Harness,
warm_ids: &[WidgetId], warm_ids: &[WidgetId],
cold: &Harness, cold: &Harness,
cold_ids: &[WidgetId], cold_ids: &[WidgetId],
) { ) {
assert_eq!(
warm_ids.len(),
cold_ids.len(),
"the warm and cold fixtures list different widgets"
);
let named: HashSet<&WidgetId> = warm_ids.iter().collect();
assert_eq!(
named.len(),
warm_ids.len(),
"a widget is listed twice: {warm_ids:?}"
);
let mut wrong = Vec::new(); let mut wrong = Vec::new();
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() { for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
@@ -92,7 +120,7 @@ fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
let mut warm = Harness::new((900, 1200)); let mut warm = Harness::new((900, 1200));
let ids = plant_stack_in_its_sizing_childs_box(&mut warm); let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
for &id in &ids { for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id); warm.rsc.widgets_mut().mark_for_redraw(id);
} }
warm.frame(); warm.frame();
@@ -188,36 +216,31 @@ fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
assert_same_regions(&warm, &ids, &cold, &cold_ids); assert_same_regions(&warm, &ids, &cold, &cold_ids);
} }
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// Four widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is /// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a /// taken again -- so no box may move, and a warm frame has to land where a
/// cold one does. /// cold one does.
fn plant(h: &mut Harness) -> Vec<WidgetId> { fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes")
let sized = wrapped.width(76).add(&mut h.rsc); .size(16)
let aligned = sized; .wrap(true)
.width(76)
.add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS); .set_alignment(wrapped, Axis::X, AxisAlign::POS);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS); .set_alignment(wrapped, Axis::Y, AxisAlign::POS);
let stack = Stack { let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
size: StackSize::Child(0), size: StackSize::Child(0),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(root); h.set_root(root);
vec![ vec![plain.id(), wrapped.id(), stack.id(), root.id()]
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
} }
/// The first frame does not reach the layout a second one does, so "cold" is /// The first frame does not reach the layout a second one does, so "cold" is
@@ -230,7 +253,7 @@ fn one_frame_is_enough() {
let first = h.region(&ids[1]).unwrap(); let first = h.region(&ids[1]).unwrap();
for _ in 0..3 { for _ in 0..3 {
for &id in &ids { for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id); h.rsc.widgets_mut().mark_for_redraw(id);
} }
h.frame(); h.frame();
} }
@@ -252,46 +275,30 @@ fn repainting_everything_moves_nothing() {
let mut warm = Harness::new((640, 900)); let mut warm = Harness::new((640, 900));
let ids = plant(&mut warm); let ids = plant(&mut warm);
for &id in &ids { for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id); warm.rsc.widgets_mut().mark_for_redraw(id);
} }
warm.frame(); warm.frame();
let mut cold = Harness::new((640, 900)); let mut cold = Harness::new((640, 900));
let cold_ids = plant(&mut cold); let cold_ids = plant(&mut cold);
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box /// Four widgets, shrunk from 905. Everything inside the declared 189x176 box
/// is the same size whatever the output is, so a resize may not change any of /// is the same size whatever the output is, so a resize may not change any of
/// it -- but the text comes out 3.92px narrower warm than cold. /// it -- but the text comes out 3.92px narrower warm than cold.
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> { fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves"; let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG); .set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc); let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![text.id(), inner.id(), filler.id(), root.id()]
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
} }
#[test] #[test]
@@ -306,17 +313,10 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
let cold_ids = plant_fixed(&mut cold); let cold_ids = plant_fixed(&mut cold);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by /// Three widgets, shrunk from 486. A span's two children are swapped: warm by
/// moving them, cold by growing them that way. Same widgets, same sizes, one /// moving them, cold by growing them that way. Same widgets, same sizes, one
/// ends up 29.9px from where the other does. /// ends up 29.9px from where the other does.
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) { fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
@@ -340,16 +340,11 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
gap: Px::ZERO, gap: Px::ZERO,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let span_handle = span;
let aligned = span;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(span, Axis::X, AxisAlign::CENTER); .set_alignment(span, Axis::X, AxisAlign::CENTER);
h.state.root = Some(aligned.add_strong(&mut h.rsc)); h.state.root = Some(span.add_strong(&mut h.rsc));
( (vec![wrapped.id(), plain.id(), span.id()], span)
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
span_handle,
)
} }
#[test] #[test]
@@ -364,17 +359,10 @@ fn swapping_two_children_lands_where_growing_them_that_way_does() {
let (cold_ids, _) = plant_pair(&mut cold, true); let (cold_ids, _) = plant_pair(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its /// Seven widgets, shrunk from 80. The scroll decides how wide to make its
/// content from what the content says, and hands that box down through a /// content from what the content says, and hands that box down through a
/// pass-through; the span under it was given that box once, so nothing at its /// pass-through; the span under it was given that box once, so nothing at its
/// own edge says the box was its own answer. /// own edge says the box was its own answer.
@@ -393,8 +381,7 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
gap: Px::ZERO, gap: Px::ZERO,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let block = rect(Color::RED).add(&mut h.rsc); let fixed = rect(Color::RED).width(87).add(&mut h.rsc);
let fixed = block.width(87).add(&mut h.rsc);
let mut outer_children: Vec<StrongWidget> = let mut outer_children: Vec<StrongWidget> =
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)]; vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
if swapped { if swapped {
@@ -416,7 +403,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
text.id(), text.id(),
filler.id(), filler.id(),
inner.id(), inner.id(),
block.id(),
fixed.id(), fixed.id(),
outer.id(), outer.id(),
through.id(), through.id(),
@@ -440,14 +426,7 @@ fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
let (cold_ids, _) = plant_scrolled(&mut cold, true); let (cold_ids, _) = plant_scrolled(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Reports a width derived from the box it is asked in. Reading through the /// Reports a width derived from the box it is asked in. Reading through the
@@ -471,11 +450,10 @@ impl Widget for Wider {
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) { fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
let content = Wider { extra }.add(&mut h.rsc); let content = Wider { extra }.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
let root = scroll;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(scroll, Axis::X, AxisAlign::NEG); .set_alignment(scroll, Axis::X, AxisAlign::NEG);
h.set_root(root); h.set_root(scroll);
(content, scroll.id()) (content, scroll.id())
} }
@@ -567,14 +545,7 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
let (cold_ids, _) = plant_boundary(&mut cold, true); let (cold_ids, _) = plant_boundary(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed /// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
@@ -616,20 +587,13 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
let mut warm = Harness::new((900, 1200)); let mut warm = Harness::new((900, 1200));
let ids = plant_nested_scrolls(&mut warm); let ids = plant_nested_scrolls(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(ids[0]); warm.rsc.widgets_mut().mark_for_redraw(ids[0]);
warm.frame(); warm.frame();
let mut cold = Harness::new((900, 1200)); let mut cold = Harness::new((900, 1200));
let cold_ids = plant_nested_scrolls(&mut cold); let cold_ids = plant_nested_scrolls(&mut cold);
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220 /// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
@@ -720,14 +684,7 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
let (cold_ids, _) = plant_under_a_node(&mut cold, true); let (cold_ids, _) = plant_under_a_node(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \ const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
@@ -1067,3 +1024,32 @@ fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() {
assert_same_regions(&warm, &ids, &cold, &cold_ids); 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"
);
}
}
+5 -29
View File
@@ -11,8 +11,6 @@
//! The instances are two pixels wide so that vertex work dominates; a chain //! The instances are two pixels wide so that vertex work dominates; a chain
//! walk that does not show up against small quads will not show up against //! walk that does not show up against small quads will not show up against
//! anything. //! anything.
//!
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
use iris::prelude::*; use iris::prelude::*;
use iris_core::{ use iris_core::{
@@ -21,6 +19,9 @@ use iris_core::{
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
#[path = "gpu/mod.rs"]
mod gpu;
const SIZE: u32 = 1024; const SIZE: u32 = 1024;
const INSTANCES: usize = 200_000; const INSTANCES: usize = 200_000;
const FRAMES: u32 = 20; const FRAMES: u32 = 20;
@@ -29,18 +30,7 @@ const FRAMES: u32 = 20;
const BATCHES: u32 = 8; const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue, f32)> { fn gpu() -> Option<(Device, Queue, f32)> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle()); let adapter = gpu::adapter()?;
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
if !adapter.features().contains(Features::TIMESTAMP_QUERY) { if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
println!("no timestamp queries on {:?}", adapter.get_info().name); println!("no timestamp queries on {:?}", adapter.get_info().name);
return None; return None;
@@ -55,20 +45,6 @@ fn gpu() -> Option<(Device, Queue, f32)> {
Some((device, queue, period)) Some((device, queue, period))
} }
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// A chain `depth` slots long, and instances that all resolve through its end. /// A chain `depth` slots long, and instances that all resolve through its end.
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) { fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
let kind = ui.primitives.kind::<RectPrimitive>(); let kind = ui.primitives.kind::<RectPrimitive>();
@@ -103,7 +79,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
/// Nanoseconds the pass took on the GPU, best of `BATCHES`. /// Nanoseconds the pass took on the GPU, best of `BATCHES`.
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 { fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm; let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format)); let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
let mut ui = UiData::default(); let mut ui = UiData::default();
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth); fill(&mut ui, &mut render, depth);
+5 -34
View File
@@ -13,10 +13,6 @@
//! That is how `PrimitiveRender` was measured against a match in the renderer: //! That is how `PrimitiveRender` was measured against a match in the renderer:
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording //! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
//! one. //! one.
//!
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
//! when the last one drops, which can fault as a thread that used it exits --
//! and every test runs on a spawned thread.
use std::time::Instant; use std::time::Instant;
@@ -27,6 +23,9 @@ use iris_core::{
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
#[path = "gpu/mod.rs"]
mod gpu;
const SIZE: u32 = 1024; const SIZE: u32 = 1024;
const FRAMES: u32 = 200; const FRAMES: u32 = 200;
/// Reported as the best of this many batches, since the mean moves by more /// Reported as the best of this many batches, since the mean moves by more
@@ -34,39 +33,11 @@ const FRAMES: u32 = 200;
const BATCHES: u32 = 8; const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue)> { fn gpu() -> Option<(Device, Queue)> {
// Probed rather than assumed: there may be no Vulkan adapter, and GL is let adapter = gpu::adapter()?;
// what is left when there is not.
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
// Leaked rather than dropped: see the note at the top of the file.
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
println!("adapter: {:?}", adapter.get_info()); println!("adapter: {:?}", adapter.get_info());
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok() pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
} }
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// Every layer draws all three primitives, so the renderer takes a different /// Every layer draws all three primitives, so the renderer takes a different
/// path for each list it walks -- which is the case a single-primitive layer /// path for each list it walks -- which is the case a single-primitive layer
/// would never exercise. Images are bound per instance, so there are few. /// would never exercise. Images are bound per instance, so there are few.
@@ -136,7 +107,7 @@ fn fill(
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 { fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm; let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format)); let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
let mut ui = UiData::default(); let mut ui = UiData::default();
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
let _handles = fill(&mut ui, &mut render, layers, per_layer); let _handles = fill(&mut ui, &mut render, layers, per_layer);
+51 -50
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
@@ -25,15 +25,22 @@ fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4) env("IRIS_GENERATED_DEPTH", 4)
} }
/// The seeds the ordinary tests take. Seven that have never failed; 86, /// The seeds the ordinary tests take: a corpus rather than a set of
/// which a `Scroll` fixed point once settled differently on; and 20, which /// regression cases, since a seed names a tree only for as long as the
/// caught a locally redrawn widget being placed twice in the box its parent /// generator draws the same things in the same order. Adding images to the
/// had already placed it in. /// leaves moved every one of them, so 20 and 86 -- which once caught a widget
/// placed twice in a box its parent had already placed it in, and a `Scroll`
/// fixed point settling differently -- no longer grow those trees. Both
/// defects are pinned by the shrunk fixtures in `cases/unsettled.rs`, which
/// are trees rather than numbers.
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 +51,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,
/// which nobody runs by hand.
macro_rules! cases {
($($name:ident = $case:expr,)*) => {
$(
#[test] #[test]
fn $name() { fn $name() {
for seed in SEEDS { for seed in SEEDS {
check(seed, depth(), $case); 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() {
@@ -108,7 +108,7 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
} }
#[test] #[test]
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"] #[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
fn a_long_run_of_seeds_agrees() { fn a_long_run_of_seeds_agrees() {
let depth = depth(); let depth = depth();
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED") let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
@@ -119,8 +119,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);
} }
}); });
} }
+40
View File
@@ -0,0 +1,40 @@
//! The adapter and the surface configuration the GPU measurement rigs share,
//! so the two cannot probe for a device in two different ways.
use wgpu::*;
/// An adapter on whatever this machine has, or `None` where there is none.
///
/// Probed rather than assumed: there may be no Vulkan adapter, and GL is what
/// is left when there is not.
///
/// The instance is leaked deliberately. A Vulkan loader may unload the driver
/// when the last one drops, which can fault as a thread that used it exits --
/// and every test runs on a spawned thread.
pub fn adapter() -> Option<Adapter> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()
}
pub fn config(format: TextureFormat, size: u32) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: size,
height: size,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
+28 -6
View File
@@ -22,6 +22,30 @@ use std::time::Instant;
const OUTPUT: (f32, f32) = (1920.0, 1200.0); const OUTPUT: (f32, f32) = (1920.0, 1200.0);
/// A scroll whose content fits is the same drawing in every box it still
/// fits in, so a longer or shorter one relays out nothing. Where the content
/// sits in that box is decided by placing its answer in the whole of it,
/// which is a fraction of the box and holds at every length -- so the
/// contract must not turn on the alignment. It did, and at the default
/// alignment, which is the middle, every box change redrew the scroll.
#[cfg(feature = "layout-diagnostics")]
#[test]
fn a_fitting_scroll_holds_for_every_box_its_content_fits_in() {
use iris::core::layout_diagnostics as diag;
for align in [Align::TOP_LEFT, Align::CENTER, Align::BOT_RIGHT] {
let mut harness = Harness::new((400, 200));
let inner = rect(Color::RED).height(50).add(&mut harness.rsc);
harness.set_root(inner.scrollable().align(align));
harness.frame();
let _ = diag::take();
// Still far longer than the 50 the content needs.
harness.resize((400, 180));
harness.frame();
assert_eq!(diag::take().distinct_widgets(), 0, "{align:?}");
}
}
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
#[test] #[test]
fn a_selected_widget_retains_its_layout_events() { fn a_selected_widget_retains_its_layout_events() {
@@ -37,8 +61,8 @@ fn a_selected_widget_retains_its_layout_events() {
diagnostics::trace_widget(leaf.id()); diagnostics::trace_widget(leaf.id());
let _ = diagnostics::take(); let _ = diagnostics::take();
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id()); harness.rsc.widgets_mut().mark_for_redraw(root.id());
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id()); harness.rsc.widgets_mut().mark_for_redraw(leaf.id());
harness.frame(); harness.frame();
let report = diagnostics::take(); let report = diagnostics::take();
@@ -195,7 +219,6 @@ fn layout_cost() {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take(); let _ = iris::core::layout_diagnostics::take();
run("cold", 1, &mut harness, |_, _| {}); run("cold", 1, &mut harness, |_, _| {});
drop(tree);
} }
if selected("repaint") { if selected("repaint") {
@@ -203,7 +226,7 @@ fn layout_cost() {
trace_selected(&tree); trace_selected(&tree);
let leaf = tree.ids[0]; let leaf = tree.ids[0];
run("repaint", frames, &mut harness, move |harness, _| { run("repaint", frames, &mut harness, move |harness, _| {
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf); harness.rsc.widgets_mut().mark_for_redraw(leaf);
}); });
} }
@@ -218,7 +241,7 @@ fn layout_cost() {
println!("marking {} of {} widgets", dirty.len(), tree.ids.len()); println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
run("many", frames, &mut harness, move |harness, _| { run("many", frames, &mut harness, move |harness, _| {
for &id in &dirty { for &id in &dirty {
harness.rsc.widgets_mut().get_dyn_mut(id); harness.rsc.widgets_mut().mark_for_redraw(id);
} }
}); });
} }
@@ -251,6 +274,5 @@ fn layout_cost() {
run("resize", frames, &mut harness, |harness, frame| { run("resize", frames, &mut harness, |harness, frame| {
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1)); harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
}); });
drop(tree);
} }
} }
+3 -1
View File
@@ -194,7 +194,9 @@ fn text_memory() {
h.frame(); h.frame();
} }
report("after 40 resizes"); report("after 40 resizes");
// Settled: the output holds still and one leaf repaints per frame. // Settled: the output holds still and one leaf repaints per frame. Marked
// by taking it mutably because the revision at the top of this file has no
// `mark_for_redraw`, and the same source has to build against both.
for _ in 0..10 { for _ in 0..10 {
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]); let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
h.frame(); h.frame();
+24 -19
View File
@@ -13,7 +13,7 @@
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build}; use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, Tree, build};
use std::collections::HashMap; use std::collections::HashMap;
/// A seed per thread but one, since a seed grows, lays out and drops its tree /// A seed per thread but one, since a seed grows, lays out and drops its tree
@@ -190,7 +190,7 @@ impl Case {
fn mark(warm: &mut Harness, tree: &Tree, step: usize) { fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
for &id in tree.ids.iter().step_by(step) { for &id in tree.ids.iter().step_by(step) {
warm.rsc.widgets_mut().get_dyn_mut(id); warm.rsc.widgets_mut().mark_for_redraw(id);
} }
} }
@@ -198,27 +198,32 @@ fn a_len(rng: &mut Rng) -> Option<LayoutLen> {
Some(LayoutLen::px(20.0 + rng.below(180) as f32)) Some(LayoutLen::px(20.0 + rng.below(180) as f32))
} }
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens { fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules {
let lens = [a_len(rng), a_len(rng)]; let lens = SizeRules {
x: a_len(rng).into(),
y: a_len(rng).into(),
};
warm.rsc warm.rsc
.widgets_mut() .widgets_mut()
.set_size_rules(tree.sized[idx], lens[0], lens[1]); .set_size_rules(tree.sized[idx], lens.x, lens.y);
lens lens
} }
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns { fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Align {
let side = |rng: &mut Rng| match rng.below(4) { let side = |rng: &mut Rng| match rng.below(4) {
0 => None, 0 => None,
1 => Some(AxisAlign::NEG), 1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER), 2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS), _ => Some(AxisAlign::POS),
}; };
let align = [side(rng), side(rng)]; let align = Align {
x: side(rng),
y: side(rng),
};
let id = tree.aligned[idx]; let id = tree.aligned[idx];
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { let taken = RegionAlign::from(align);
warm.rsc for axis in Axis::BOTH {
.widgets_mut() warm.rsc.widgets_mut().set_alignment(id, axis, taken[axis]);
.set_alignment(id, axis, align.unwrap_or_default());
} }
align align
} }
@@ -406,8 +411,8 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
fn record(id: WidgetId, h: &Harness) -> String { fn record(id: WidgetId, h: &Harness) -> String {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
format!( format!(
"frame {} ask {} box {} size {}", "rel_base {} region {} placement {} size {}",
active.frame, active.part, active.extent, active.size, active.rel_base, active.region, active.placement, active.size,
) )
} }
@@ -444,12 +449,6 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
cold.state.root = Some(root); cold.state.root = Some(root);
cold.frame(); cold.frame();
let places: HashMap<WidgetId, usize> = tree
.ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let mut drawn = 0; let mut drawn = 0;
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() { for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
@@ -457,6 +456,12 @@ 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();
+17 -31
View File
@@ -1,5 +1,5 @@
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is //! Traces the four-widget trees in `unsettled.rs`, to see what box their text
//! actually drawn in on a first frame against a settled one. //! is actually drawn in on a first frame against a settled one.
#![cfg(feature = "layout-diagnostics")] #![cfg(feature = "layout-diagnostics")]
@@ -9,30 +9,25 @@ use iris::prelude::*;
fn plant(h: &mut Harness) -> Vec<WidgetId> { fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes")
let sized = wrapped.width(76).add(&mut h.rsc); .size(16)
let aligned = sized; .wrap(true)
.width(76)
.add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS); .set_alignment(wrapped, Axis::X, AxisAlign::POS);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS); .set_alignment(wrapped, Axis::Y, AxisAlign::POS);
let stack = Stack { let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
size: StackSize::Child(0), size: StackSize::Child(0),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![plain.id(), wrapped.id(), stack.id(), root.id()]
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
} }
fn dump(label: &str, report: &diag::Report, text: WidgetId) { fn dump(label: &str, report: &diag::Report, text: WidgetId) {
@@ -42,12 +37,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 => {
@@ -81,7 +76,7 @@ fn what_box_the_text_is_drawn_in() {
for _ in 0..2 { for _ in 0..2 {
for &id in &ids { for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id); h.rsc.widgets_mut().mark_for_redraw(id);
} }
let _ = diag::take(); let _ = diag::take();
h.frame(); h.frame();
@@ -93,23 +88,14 @@ fn what_box_the_text_is_drawn_in() {
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> { fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves"; let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG); .set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc); let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![text.id(), inner.id(), filler.id(), root.id()]
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
} }
#[test] #[test]