Compare commits

...
Author SHA1 Message Date
iris-ai e44dea34b4 Record which widget is drawing a subtree that changed hands
A subtree can be reused whole under a different parent -- same box, same
layer, same region node, clean -- and nothing in the drawing says it
moved. Two things read who its parent is, and both were wrong after one
of these.

The old parent still listed it as a child, and a parent's next draw
undraws whatever is missing from that list: two spans under one root,
with the root swapping which of them it holds, drew the subtree under
the new span and then erased it when the old one drew. The move is
recorded on both sides where `draw_inner` already writes what the ask
decided, rather than guarded at each reader.

Its depth was also the one it had under the old parent, which is what
the settling walk orders by, so a change made under it afterwards
settled at the wrong point in the frame. `try_reuse` re-walks the
subtree's depths, and only where the top of it moved, which is what
makes that free in the ordinary case.

Two tests: one shape where the subtree's box does not move and the span
it left erases it, one where it changes depth and the change made under
it has to reach the span it moved to. Each fails without one half.
2026-09-17 13:05:15 -04:00
iris-ai a0693acc56 Let a resize settle through the walk, and drop the stale-answer guard
A resize drew the root outside `redraw_updates`, top-down over a tree
with dirty widgets still in it, which is the one entry point
`dirty_size_under` was guarding: since `a92c6ac` settles a frame strictly
bottom-up, no fuzzer could tell whether that guard still did anything
anywhere else. Closing the entry point retires the guard rather than
keeping a check for a hole reasoned rather than measured.

The root is marked instead, and only where the new output falls outside
what its answer holds for. That range is the intersection of everything
under it, so admitting the new output says the whole tree still stands,
and nothing above the root moved -- the window is no entry to rewrite.
Marking it unconditionally would have cost the root its own `Holds`: a
leaf root that scales with its box was drawn again on every resize.

`dirty_size_under` goes at both call sites. `resize` takes `Widgets`
because a mark is what it now leaves behind.
2026-09-17 13:00:32 -04:00
iris-ai 25e456e0b5 Say what the fuzzers can no longer tell about the stale-answer guard
Dropping `dirty_size_under` from it now passes every run there is. It stays
for the one entry the bottom-up ordering does not reach -- `update` draws
the root for a resize before `redraw_updates` runs -- which is a hole
reasoned rather than measured, and the note says which.
2026-09-17 05:12:44 -04:00
iris-ai 53b00c68e9 Find a span's leftover boundary through the inverse it already has
The decision used a rounded division, `total.px.div(fixed)`, where the room
the children get is a floored multiply, so the boundary and the drawing it
guards were two expressions for one length and disagreed at the edge of it.
`room` is that length as a `Len`, `room.to_px` is the multiply, and
`Holds::through` is its exact preimage -- so ask `room` whether anything is
left and hand the answer back through the same expression.

The three branches go with the division. They were the sign of `1 - rel`:
the fixed parts growing slower than the box, faster, or exactly with it, and
`through` reads that sign already. Forty lines become twelve, one `div`
leaves layout, and the boundary is the drawing's own.

Green on the suite, the shrinker at 400 seeds of depth 5, the oracle at 1000
seeds of depth 6 and 120 in debug, and 2000 seeds at depth 4 over all
fifteen cases. `tabs`, `view`, `minimal` and `random` byte-identical.
2026-09-17 05:02:45 -04:00
iris-ai a92c6acdbf Settle a frame strictly bottom-up rather than escalating into a parent
The queue was already deepest-first, but a widget that could not settle
where it was called `redraw` on its parent from inside itself. That drew a
shallow widget while dirty widgets deeper in other subtrees were still
pending, and a parent drawing over a subtree that has not settled reads
answers about to move: the one that settles does so inside the parent's
draw, where its mark comes off and nothing compares what it now answers.
Seed 564 was exactly that, and it is the second time this shape has been
found.

So a widget that cannot settle defers instead. It marks its parent, stays
marked itself, and waits in `deferred` until the walk down the depths
reaches the parent -- which cannot be before everything deeper has settled,
because the walk always takes the deepest widget that is not waiting. The
category stops being something to check for. (Bryan, 2026-09-17.)

`dirty_size_under` stays in `draw_inner` for now: `update` draws the root
for a resize before `redraw_updates` runs at all, so the ordering does not
cover that entry.

Green on the suite, the shrinker at 400 seeds of depth 5, the oracle at 1000
seeds of depth 6, and 2000 seeds at depth 4 over all fifteen cases. Drawn
widgets, widget draws and primitive writes are unchanged on every rig phase;
`many` pays 51 queue pops for 27 and 1059 depth reads for 410, which is the
deferring and nothing else.
2026-09-17 04:29:41 -04:00
iris-ai c8beca5753 Give the text example's aligned labels the width to align in
All three sat in the middle of a box the width of the widest of them, so
left, centred and right were the same picture. `text_align` puts the glyphs
somewhere in the box the text is given, and a text that reports the width of
its own glyphs is given exactly that -- there is nowhere for it to sit.
Declaring `rel(1.0)` on each hands it the row instead. (Bryan, 2026-09-17.)
2026-09-17 03:21:17 -04:00
iris-ai 4bd8607968 Report the step at or above a text's longest line
A wrapping text reported the width it used rounded to the nearest step,
which is under the line it measured half the time. A parent that sizes
itself from that report then hands the text back a box its own longest line
does not fit in, and breaking there is a different break -- one line more.

Two tolerances were hiding it and both go. `TextBuffer::shape` answered a
width up to 0.05 px under the longest line from the break in hand, which is
a structural decision taken on a hair's breadth: it kept a warm tree
self-consistent while a cold tree at the same width broke differently, and
0.05 px is fifty steps of the grid. The `Holds` range the text declares
started at the nearest step to its longest line for the same reason, so it
admitted boxes the line does not fit in. Both are the line itself now,
exactly, because the report no longer lands under it.

Found by seeds 1121 and 1839 at depth 4, which fail on `ea6dbae` and every
commit before it: a defect older than anything on this branch, reached by
running 2000 seeds at a depth the long runs do not use. Shrunk to the eight
widgets `a_text_is_given_back_a_box_the_line_it_measured_fits_in` builds.
2000 seeds at depth 4 over all fifteen cases are clean now, as are the
three long runs.

`text` is the one reference render that moves: its lower paragraph shifts a
pixel, the box being a step wider and its left edge crossing a snap
boundary. Same words, same lines, same breaks; `tabs`, `view`, `minimal`
and `random` are byte-identical.
2026-09-17 03:18:53 -04:00
iris-ai ffd79f32d3 Read a child's report as a fraction of the containing widget
`rel(0.5)` is half the span whatever else is in it and wherever the child
sits among them (Bryan, 2026-09-17). It was half of what the span had left
at the point it asked, because a report came back composed through the box
it was offered and a span offers each child the room from its cursor -- so
a nested span taking half of what it was given took a quarter of a row
whose first half was already spoken for, where the same half written as a
rule on the child took half the row.

The offer stays the remainder: a text has to wrap at the width actually
there, and `a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row`
pins that. What separates from it is the base a report's fractions are of,
which the ask now carries. It is the box the child was given wherever that
box is the child's whole area -- a pad's inset, a stack child, a scroll's
content -- and a span passes its own extent along the row.

`widget_decided` becomes `widget_at`, which says both things about an ask
rather than one of them; `widget_within` is still the sugar for neither.

Two spans asking for half each now take the whole row between them and a
third overflows, which the rewritten
`a_span_reads_a_child_report_as_a_fraction_of_the_row` states outright.
The five reference renders are byte-identical at 1920x1200 and `random`
live-resized still matches a cold render, so nothing that exists reports a
fraction to a span today.
2026-09-17 02:56:51 -04:00
iris-ai 0e0d4af326 Refuse a retained answer while something the widget measured is dirty
`draw_inner` took an answer from `try_reuse`, which checks only whether the
widget itself is marked, where `retained_answer` beside it also refused one
while anything the widget read a size from was dirty. A widget whose drawing
happened to be reusable therefore handed back the answer it gave before that
descendant changed.

Nothing puts that right afterwards. The comparison that tells a reader its
child's answer moved is in `redraw`, and a widget settled inside its parent's
own draw never goes through it -- the placing ask redraws the subtree, the
descendant's mark is cleared there, and the parent keeps a number the tree no
longer agrees with. So the check is not the optimization its comment claimed;
it is what makes the answer an answer, and both retained routes are answers,
so it is asked once in `draw_inner` rather than by one of them.

Found by the generated oracle at seed 564, depth 6, `shuffle-every-other`,
while reading a child's report as a fraction of the containing widget: that
reading lets a span overflow itself, which makes the two asks' boxes differ
far enough for the placing one to redraw.

Twenty-five rig work counters are unchanged on `cold`, `repaint`, `scroll`,
`resize` and `size`; `many` makes 18 fewer reuse attempts, 17 of which
already reported "dirty". Both long fuzzers green.
2026-09-17 02:46:25 -04:00
iris-aiandClaude Opus 5 ea6dbae0dc Hand a redrawn widget the mask it inherited, not its own
`ActiveData::mask` is the mask a widget's drawing is clipped to, which is
either one it set itself or the one it inherited. `redraw` passed it back as
the *inherited* mask, so a `Masked` widget settled on its own was handed its
own mask and `set_mask` asserted -- a panic on any local redraw of one, for
as long as there has been a local-redraw path. The two are separate facts, so
`parent_mask` keeps the second.

That also states the question `remap_subtree` was asking. It compared a
widget's mask with the one threaded down from its parent to find out whether
the widget owned it; the comparison is now between the two fields on the
widget, which is the same question asked where the answer lives, and the
parameter goes.

Checked: fmt, clippy, 87 suite tests including the new one, which panics
without this; 18 core unit tests; the release oracle at 100 seeds; the
fifteen shrinker cases at 400 seeds of depth 5; and `tabs` renders
byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:18:39 -04:00
iris-aiandClaude Opus 5 32542d0c0b Thread a box in pixels down the draw, one multiply from its parent's
A box in pixels was composed back up the move chain, on a grid fine enough
that the walk rounded once, while a widget's offer was threaded down through
its ancestors' offers. Two routes to one length, which is what
`Holds::through` allowed for -- and the offer's route broke at a region node.
`offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved
that against the node's slot entry, which holds the box its parent *placed*
the node in. Under a `Scroll` that is as long as the content rather than the
viewport, so everything below was re-asked at a width its own answer had
produced and the old answer confirmed itself: shrinker seed 220 on `reorder`
left a widget 290px out.

`ActiveData` now keeps a widget's box as lengths of its parent's box --
`given_len`, and `offer_len` for the box it was first asked about -- and
`DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a
time: the box its parent gave it, then the part of that box its own answer
placed the drawing in, which `placed_lens` states once for both `placed_box`
and the walk. `Painter::px_size` and `px_len` read that value, and
`UiRenderState::asked_px` takes the same steps back up the parent chain where
a local redraw starts part-way down the tree. Neither chain has a coordinate
frame in it, so neither can break at a region node, and warm and cold reach
every length by the same expression.

Three things follow. `Holds::through` is the exact preimage of
`px + floor(rel * box)` -- two divisions, no allowance, the whole of a box
mapping back to itself. A local redraw asks in the box its parent gave it and
only where that box is as long as the offer, which retires `redraw`'s third
ask and the region-node exception beside it; `draw_inner` places the answer
inside that box itself. And symbolic regions are left to the GPU, hit testing
and remaps, where `Moves::resolve` is the only walk: `wide.rs`,
`Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region`
and `slot_wide` are gone, 252 lines of `core/` net.

`px` is deliberately not stored beside those lengths. A resize every widget's
`Holds` admits redraws nothing, so a stored pixel length would be stale on
every widget in the tree with nothing on it to say so, and refreshing it costs
a walk down every reused subtree on the resize path.

Instructions:u, medians of 21 runs, seed 1 at depth 8:

| phase | before | after | |
| --- | ---: | ---: | ---: |
| `cold`, 200 frames | 313.1M | 312.9M | -0.04% |
| `resize` | 408.1M | 405.6M | -0.61% |
| `many` | 1,924M | 1,756M | -8.75% |
| `scroll` | 357.3M | 323.4M | -9.49% |
| `repaint` | 363.3M | 315.4M | -13.18% |

`cold` and `resize` have all twenty-five work counters identical, so those
two rows say the draw path costs the same threaded as composed. The other
three do less work: `repaint` goes from 23 draw requests and 13 widget draws
a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and
186 to 207 and 157. Primitive writes are unmoved in every phase.

Verified: `view`, `minimal`, `random`, `tabs` and `text` render
byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch
replay before and after the gesture, and a live resize of `random` to
1280x800 is identical both to the old head's and to a cold render at that
size. The oracle passes 100 seeds in release and 120 in debug -- the debug
run is the one that exercises the `Holds` assertion -- and the fifteen
shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`,
which needs both halves of this to fail: the old chain with the old allowance
passes it, and the old chain with the exact preimage does not.

`AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the
400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the
resize path re-expressing a part as a fraction of a box that changed length,
not a length reached two ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:12:56 -04:00
iris-aiandClaude Fable 5.1 5b7800264d Read a child's answer in the asker's frame, and drop the root move entry
A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.

That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.

The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.

Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:00:15 -04:00
iris-aiandClaude Opus 5 5f16617511 Carry the composed box down the draw, rather than walking back up for it
Every widget that reads its box in pixels was making `Moves` compose its
slot's chain again, a mean of 2.8 levels, about eight hundred times a frame.
A draw already descends past every one of those entries on its way in, so
`DrawInfo` carries what the slot composes to and `draw_at` steps it one box
further -- which is a select where it was a walk. `Moves::size_of` and
`compose` are left for `redraw`, which starts mid-tree with nothing above it
in flight.

Measured on the fixed-shape fixture, seed 1 depth 8, 500 frames of `many`,
medians of 25 runs, twenty-five work counters identical throughout:

| | instructions | cycles |
| --- | ---: | ---: |
| `d21a215`, before exact composition | 1,908M | 760M |
| `45a7176`, composing on the fine grid | 1,880M | 755M |
| this | **1,840M** | **735M** |

So exact composition ends up 3.6% fewer instructions and 3.3% fewer cycles
than the rounding-per-level walk it replaced, and the widening it needed was
paid for twice over by not doing the walk.

`Holds::through`'s allowance does not move: two half steps is where shrinker
seed 220 pins it, not where the arithmetic does. `Painter` still composes a
child's region into its own on the grid before asking for it in pixels, which
is the last narrow step in that path; taking it out needs the child's region
as its parent stated it, which `draw_inner` is not handed.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400
seeds of depth 5, and `tabs`, `view`, `minimal`, `text`, `random` and the tab
replay byte-identical at 1920x1200 against `45a7176`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 21:49:25 -04:00
iris-aiandClaude Opus 5 45a717695b Compose a box down its chain once, not once a level
Bryan's call, 2026-09-16, for correctness. `Moves` walked the move chain in
`Len`, so every level's four multiplies landed back on the grid before the
next started and the residue grew with the depth of the tree. `WideLen`
carries a length through the walk on a grid twenty-four bits of a box and
twenty-two of a pixel finer, and rounds once at the end.

What it buys, measured rather than argued: `Holds::through`'s allowance for
the two routes to a length drops from three half steps to two, and the whole
of a box now maps back to a range one step wide rather than one step per
level of nesting. One half step further is arithmetically available -- the
`Holds` assertion is quiet there and the whole-box case becomes an exact
identity -- and it is **not taken**, because shrinker seed 220 then lays out
differently warm than cold. Too narrow is meant to cost a redraw and no more;
there it re-breaks a wrapping text, whose reported width moves a `Branch`
onto its other subtree. That is the unsettled-text family, and closing it is
what would let this go lower. The note is in `through`.

`Moves` now answers three questions instead of one, and they are different
questions: `size_of` for how long a box is, which is what reads a box in
pixels; `compose` for where both of its ends are, which is what compares two
boxes; and `resolve`, unchanged, for the `Len` walk the vertex shader does
again in floats. A length composes on its own in two multiplies a level
rather than four, since where the parent sits falls out of the difference --
which is most of why this is not slower.

Measured on the fixed-shape fixture, seed 1 depth 8, 500 frames of `many`,
medians of 25 runs with all twenty-five work counters identical between the
two: 1,880M instructions and 755M cycles against 1,908M and 760M. So it is
free, and a little better on instructions. Three things were tried on the way
and two kept: composing the length alone rather than both ends (-111M
instructions), taking the pixel term's fraction on the ordinary grid so it
stays in an `i64` (-2M instructions, -8M cycles), and skipping a parent that
spans its own box, which **cost** 18M instructions and is not here -- the
same verdict a short-circuit got in `UiSpan::within`.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400
seeds of depth 5, and `tabs`, `view`, `minimal`, `text` and `random`
byte-identical at 1920x1200 against `d21a215`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 21:30:45 -04:00
iris-aiandClaude Opus 5 d21a21524f Rename WidgetPtr to Wrapper and give it a builder
Bryan's call, 2026-09-16: a length and an alignment are properties of one
widget, so a widget cannot both be 100 wide and take two shares of a row --
that needs two widgets, and the second one should do as little as possible.
`WidgetPtr` already was that widget: it draws its child in the whole of its
box and reports what the child said. It only lacked a name that says so and
a way to make one around an existing widget.

`Wrapper` rather than `Wrap` so it cannot be read as the text setting, and
`.wrapper()` rather than `.wrapped()` for the same reason. Its child stays
optional, since being a swappable slot is what it was written for and what
the tab bar still uses it as.

`set_ptr` is deleted rather than renamed. It had no caller, and putting a
widget into an existing wrapper is what `Wrapper::set` already does.

`tabs` draws its centred square again: `.sized((100, 100)).center()
.wrapper().width(leftover(2))` is two widgets where the chain without
`.wrapper()` was one, and `.width` was overwriting what `.sized` set. That
was the last of the three ways `tabs` had drifted from canonical `main`
unnoticed; what is left between them is the truncated multiply's antialiased
edges and the widget count itself.

`widget_trait!` takes no attributes, so `.wrapper()` carries an ordinary
comment and the explanation lives on `Wrapper`.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle
at 100 seeds, and `tabs` rendered at 1920x1200 against `main`'s own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:48:07 -04:00
26 changed files with 1048 additions and 509 deletions

No files matched your search

+37
View File
@@ -109,6 +109,18 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
}) })
} }
/// The first step at or above `v`, where [`Self::from_f32`] takes the
/// nearest one and is below it half the time. For a bound that has to
/// admit the value it came from: a measurement rounded down is a bound
/// that leaves out the thing it was measured from.
pub const fn ceil_from_f32(v: f32) -> Self {
let nearest = Self::from_f32(v);
match nearest.to_f32() < v {
true => nearest.next_up(),
false => nearest,
}
}
/// From a number as it is written in source -- `16`, `1.5` -- which is /// From a number as it is written in source -- `16`, `1.5` -- which is
/// the other place a value enters the grid. /// the other place a value enters the grid.
pub fn from_num(v: impl UiNum) -> Self { pub fn from_num(v: impl UiNum) -> Self {
@@ -372,6 +384,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y)) Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
} }
/// The first step at or above each part, for a measurement reported as a
/// box: what it occupies is not less than what was measured.
pub fn ceil_from_f32(v: Vec2) -> Self {
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
}
pub fn to_f32(self) -> Vec2 { pub fn to_f32(self) -> Vec2 {
Vec2::new(self.x.to_f32(), self.y.to_f32()) Vec2::new(self.x.to_f32(), self.y.to_f32())
} }
@@ -488,6 +506,25 @@ mod tests {
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200)); assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
} }
/// The bound a greedy line break needs: the width it was measured at is
/// not on the grid, and the narrowest box the break still holds for is
/// the step at or above it, never the one below.
#[test]
fn a_ceiling_never_lands_below_the_number_it_came_from() {
let step = 1.0 / (1 << PX_SHIFT) as f32;
for n in 0..64 {
let v = 189.0 + n as f32 * step / 3.0;
let up = Px::ceil_from_f32(v);
assert!(up.to_f32() >= v, "{up:?} is below {v}");
assert!(
up.to_f32() - v < step,
"{up:?} is more than a step above {v}"
);
}
// An exact step is its own ceiling.
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
}
#[test] #[test]
fn a_number_from_outside_is_clamped_to_the_grid() { fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::from_f32(1e12), Px::MAX); assert_eq!(Px::from_f32(1e12), Px::MAX);
+1 -2
View File
@@ -84,8 +84,7 @@ pub struct RegionAlign {
} }
impl RegionAlign { impl RegionAlign {
/// Both axes at the near edge. What a container passes as an override for /// Both axes at the near edge: the start of a box in its own orientation.
/// a child it is going to position itself.
pub const NEAR: Self = Self { pub const NEAR: Self = Self {
x: AxisAlign::NEG, x: AxisAlign::NEG,
y: AxisAlign::NEG, y: AxisAlign::NEG,
+19
View File
@@ -125,6 +125,13 @@ impl Size {
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
} }
impl LayoutLen { impl LayoutLen {
@@ -151,6 +158,18 @@ impl LayoutLen {
Len::from_parts(self.rel.add(share), self.px) Len::from_parts(self.rel.add(share), self.px)
} }
/// This length, given as a part of a box `len` long, as a part of the
/// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len);
Self {
px: part.px,
rel: part.rel,
leftover: self.leftover,
}
}
pub fn px(px: impl UiNum) -> Self { pub fn px(px: impl UiNum) -> Self {
Self { Self {
px: Px::from_num(px), px: Px::from_num(px),
+1 -1
View File
@@ -219,7 +219,7 @@ impl Len {
} }
} }
pub fn within_len(&self, len: Len) -> Self { pub const fn within_len(&self, len: Len) -> Self {
self.within(&UiSpan { self.within(&UiSpan {
start: Len::ZERO, start: Len::ZERO,
end: len, end: len,
+9 -12
View File
@@ -107,13 +107,6 @@ impl Default for TextAttrs {
} }
} }
/// How far below the longest line a width may fall and still be answered by
/// the break in hand. A parent that offers a child the length it reported
/// composes that length back through the box chain, so the two differ in the
/// last bits -- and at exactly the longest line, that decides whether a line
/// fits. Sub-pixel, so no break it admits is one a reader could see.
const BREAK_EPSILON_PX: f32 = 0.05;
/// Keeps text and its corresponding layout from getting out of sync. /// Keeps text and its corresponding layout from getting out of sync.
pub struct TextBuffer { pub struct TextBuffer {
text: String, text: String,
@@ -200,15 +193,19 @@ impl TextBuffer {
// A greedy break at one width is the same break at every width down // A greedy break at one width is the same break at every width down
// to the longest line it produced: each line still fits, and none can // to the longest line it produced: each line still fits, and none can
// take a word that would not fit in the wider box. So the layout in // take a word that would not fit in the wider box. So the layout in
// hand already answers, and re-breaking would only be a chance to // hand already answers, and re-breaking would only be work.
// disagree with itself -- which is what happens when a parent offers //
// a child the length that child just reported, and the two land // At the longest line exactly, with no margin below it. A narrower
// either side of a float. // width really does break differently, so answering one from the
// break in hand is how a warm tree keeps lines a cold tree would
// never produce. The margin was here because a text reports the
// width it used and a parent hands that back; the report is the step
// at or above its longest line now, so what comes back fits.
if let Some(key) = &self.layout_key if let Some(key) = &self.layout_key
&& key.attrs == *attrs && key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width) && let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at && want <= broke_at
&& want + BREAK_EPSILON_PX >= self.layout.width() && want >= self.layout.width()
{ {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits); diag::bump(Counter::TextShapeHits);
+8 -4
View File
@@ -35,6 +35,10 @@ struct MoveOffset {
// belongs to the pixel above it. Flooring the product instead drops a pixel // belongs to the pixel above it. Flooring the product instead drops a pixel
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of // wherever a fraction divides a window exactly: a fifth of 1920 comes out of
// `REL_STEP` as 383.99998, and five tabs each lose their last column. // `REL_STEP` as 383.99998, and five tabs each lose their last column.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
fn snap_floor(v: vec2<f32>) -> vec2<f32> { fn snap_floor(v: vec2<f32>) -> vec2<f32> {
return floor(v + PX_STEP * 0.5); return floor(v + PX_STEP * 0.5);
} }
@@ -147,8 +151,8 @@ fn vs_main(
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel); let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_px = vec2(r.x.end.px, r.y.end.px); let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px); let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px); let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
let size = bot_right - top_left; let size = bot_right - top_left;
let uv = vec2<f32>( let uv = vec2<f32>(
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
let br = vec2(m.x.end.rel, m.y.end.rel); let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px); let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px); let top_left = snap_floor(tl * window.dim + tl_px);
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px); let bot_right = snap_floor(br * window.dim + br_px);
let pos = in.clip_position.xy; let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0; return color * 0.0;
+35 -14
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
UiRegion, WidgetId, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -11,11 +11,20 @@ pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// The box its drawing is in, in `parent_move`'s coordinates. /// The box its drawing is in, in `parent_move`'s coordinates.
pub region: UiRegion, pub region: UiRegion,
/// The box its parent first asked about it in, as a part of the box the /// The box its parent gave it, in the same coordinates: what it was
/// parent was itself asked in. Any later box it was given was decided /// asked about, before its own answer placed its drawing inside it.
/// knowing its answer, so this is where a question about it is asked /// `region` is that placement, and a local redraw asks here.
/// again -- and it is kept relative so that it follows the parent's. pub given: UiRegion,
pub offer: UiRegion, /// The same box as lengths of its parent's box, which is the one route
/// to a box in pixels: a draw threads these down a level at a time, and
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
pub given_len: UiVec2,
/// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
/// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it.
pub offer_len: UiVec2,
/// What it answered there: the size and what that held for. /// What it answered there: the size and what that held for.
pub answer: (Size, [Holds; 2]), pub answer: (Size, [Holds; 2]),
/// What the widget said it used of its box, the last time it drew. /// What the widget said it used of its box, the last time it drew.
@@ -41,18 +50,22 @@ pub struct ActiveData {
/// A change to one moves a box this widget cannot fix by drawing again, /// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so. /// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2], pub declared: [Option<LayoutLen>; 2],
/// The alignment its parent asked it with. A local redraw repeats that /// The axes along which its parent chose its box from its own answer,
/// question, including an override chosen by a container. /// so a local redraw asks the question its parent asked.
pub align: RegionAlign, pub decided: [bool; 2],
/// Whether that alignment was the parent's override rather than the /// Its alignment when it was last drawn, which a change to the property
/// widget's own property. /// is found against.
pub align_override: bool,
/// Its own alignment when it was last drawn. A change to the property is
/// found against this even when its parent overrode the alignment.
pub own_align: RegionAlign, pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses. /// The movable region whose coordinates `region` uses.
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
pub mask: MaskIdx, pub mask: MaskIdx,
/// That inherited one. The two differ exactly where the widget set a
/// mask of its own, which is the one it owns and the one a move rewrites
/// -- and the one a redraw of it must not be handed back, since setting
/// a mask asserts there is none.
pub parent_mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
@@ -61,4 +74,12 @@ impl ActiveData {
pub fn holds_at(&self, px: crate::PxVec2) -> bool { pub fn holds_at(&self, px: crate::PxVec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y) self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
} }
/// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its
/// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
let (_, holds) = self.answer;
holds[0].contains(px.x) && holds[1].contains(px.y)
}
} }
+43 -54
View File
@@ -1,4 +1,4 @@
use crate::{Len, Px, REL_SHIFT, Rel, fixed::div_toward, fixed::narrow}; use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for: /// The lengths of a box, in pixels, that one drawing of a widget holds for:
@@ -10,9 +10,9 @@ use std::ops::RangeInclusive;
/// ///
/// The ends are lengths on the grid rather than floats with a tolerance /// The ends are lengths on the grid rather than floats with a tolerance
/// around them: a box offered back at the length a widget reported comes back /// around them: a box offered back at the length a widget reported comes back
/// as the same number, so a range means what it says. What widening there is /// as the same number, so a range means what it says. The one place a range
/// belongs to [`Self::through`], which has a rounding to undo, and is derived /// is wider than the length it came from is [`Self::through`], and what it is
/// from that rounding rather than chosen. /// wider by is the floor that inverting a fraction undoes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holds { pub struct Holds {
pub lo: Px, pub lo: Px,
@@ -41,51 +41,29 @@ impl Holds {
} }
/// What a box has to be for a part of it, `len` of the box long, to stay /// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range. A part with no relative extent is a fixed length: it /// in this range: the exact preimage of `px + floor(rel * box)`, which is
/// was drawn at that length and any box keeps it there. /// the one way a box in pixels is reached. A part with no relative extent
/// is a fixed length -- it was drawn at that length and any box keeps it
/// there.
/// ///
/// The way in is `px + rel * box` taken to the nearest step, so a part /// The answer is an interval even where this range is a single length,
/// of exactly `lo` came from anything within half a step of it and the /// because the multiply on the way in drops to the step below and many
/// answer is an interval even where this range is one length. Inverting /// boxes therefore give one length. That is a floor rather than an
/// the length alone instead gives a point that need not even contain the /// allowance: inverting it is two divisions and nothing else, and the
/// box the part was drawn in, which is a range excluding the drawing it /// whole of a box maps back to itself.
/// was made for.
pub const fn through(self, len: Len) -> Self { pub const fn through(self, len: Len) -> Self {
let rel = len.rel.raw() as i64; let rel = len.rel.raw() as i64;
if rel == 0 { if rel == 0 {
return Self::ANY; return Self::ANY;
} }
// In half steps, and no more than the arithmetic needs: too wide a
// range admits reusing a drawing where it does not hold, and too
// narrow a one leaves out the box a drawing was made in, which the
// `Holds` assertion in `draw_at` catches. `ROUTES` is at that floor
// -- two half steps fires it -- and tightening both ends moved not
// one of the rig's work counters, so the slack is not buying reuse.
//
// `ROUTES` covers a box composed down the chain against the same box
// measured against the window: two routes to one number, each
// rounding where the other does not. `way_in` covers the multiply
// this inverts, which drops a step and only ever downward, so it
// belongs at the top of the range alone -- and the whole of a box has
// no multiply in it, however many pixels were added to it, since
// multiplying by one is exact and taking the pixels off again is too.
// Allowing for it there anyway compounded, a step a level down a
// chain of widgets each taking the whole of its parent.
//
// Shifted by half of what a `Rel` counts in, to divide by the
// fraction: exact until the division takes it back to the grid.
const ROUTES: i64 = 3;
let px = len.px.raw() as i64; let px = len.px.raw() as i64;
let half_rel = REL_SHIFT - 1; // `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
let way_in = match rel == Rel::ONE.raw() as i64 { // `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
true => 0, let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
false => 2, let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
}; // Dividing by a negative fraction turns the ends around, so which
let lo = ((self.lo.raw() as i64 - px) * 2 - ROUTES) << half_rel; // bound each comes from is decided before dividing rather than by
let hi = ((self.hi.raw() as i64 - px) * 2 + ROUTES + way_in) << half_rel; // taking the min and max of four divisions.
// Dividing by a negative turns the ends around, so which end each
// bound comes from is decided before dividing rather than by taking
// the min and max of four divisions.
match rel > 0 { match rel > 0 {
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)), true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)), false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
@@ -136,26 +114,37 @@ mod tests {
} }
/// A widget handed the whole of its parent's box, with or without pixels /// A widget handed the whole of its parent's box, with or without pixels
/// taken off it, brings no multiply of its own: only the two routes to /// taken off it, has no fraction to invert: multiplying by one is exact
/// the same length are left to allow for, and not a rounding that did /// and taking the pixels off again is too, so the box maps back to
/// not happen. Widening for it as well grew the interval a level at a /// itself. Allowing for anything here compounded a step a level down a
/// time down a chain of them. Three half steps come back as one whole /// chain of widgets each taking the whole of its parent.
/// one, since dividing by a whole box is dividing by one.
#[test] #[test]
fn the_whole_of_a_box_widens_by_the_routes_alone() { fn the_whole_of_a_box_maps_back_to_itself() {
let at = Px::from_int(956); let at = Px::from_int(956);
let one_step = |len: Px| Holds { assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
lo: len - Px::STEP,
hi: len + Px::STEP,
};
assert_eq!(Holds::at(at).through(Len::FULL), one_step(at));
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8)); let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
assert_eq!( assert_eq!(
Holds::at(at).through(less_eight), Holds::at(at).through(less_eight),
one_step(at + Px::from_int(8)) Holds::at(at + Px::from_int(8))
); );
} }
/// The range is the exact preimage at both ends, so a box one step
/// outside it really does give a length outside this range. What a wider
/// range costs is a drawing reused where it does not hold.
#[test]
fn a_box_one_step_outside_the_range_is_outside_it() {
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
let at = Px::from_int(300);
let holds = Holds::at(at).through(part);
for inside in [holds.lo, holds.hi] {
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
}
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
}
}
/// A truncating multiply only ever drops, so the step it needs allowing /// A truncating multiply only ever drops, so the step it needs allowing
/// for on the way in belongs at the top of the range and not the bottom. /// for on the way in belongs at the top of the range and not the bottom.
#[test] #[test]
+12 -6
View File
@@ -68,17 +68,24 @@ impl Moves {
} }
} }
/// Composes a region held in `idx`'s coordinates down the chain, which is /// The same walk the vertex shader does, in the same `Len` the shader is
/// the same walk the vertex shader does. /// handed, for asking where a drawing will actually land -- hit testing,
/// and nothing layout decides on. Layout threads its lengths down the
/// draw instead, so no box it compares is composed back up this chain.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion { pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local; let mut region = local;
self.walk(idx, |entry| region = region.within(entry));
region
}
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
let mut at = idx; let mut at = idx;
for _ in 0..CHAIN_LIMIT { for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE { if at == MoveIdx::NONE {
return region; return;
} }
let entry = self.arena[at.idx()]; let entry = &self.arena[at.idx()];
region = region.within(&entry.region); step(&entry.region);
at = entry.parent; at = entry.parent;
} }
debug_assert!( debug_assert!(
@@ -86,7 +93,6 @@ impl Moves {
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \ "a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth" and the shader stops at the same depth"
); );
region
} }
/// How many slots a region in `idx` is composed through, which is what /// How many slots a region in `idx` is composed through, which is what
+118 -63
View File
@@ -1,7 +1,7 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget, Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
WidgetId, Widgets, WidgetId, Widgets,
render::{ render::{
@@ -19,6 +19,11 @@ pub struct Painter<'a> {
/// This widget's box, in the coordinates of `move_idx`. /// This widget's box, in the coordinates of `move_idx`.
pub(super) region: UiRegion, pub(super) region: UiRegion,
/// That box in pixels, which its children's are a length of: threaded
/// down from the box this widget was given rather than composed back up
/// the chain, so every length in layout is one multiply from its
/// parent's and [`Holds::through`] inverts exactly that.
pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
@@ -26,10 +31,12 @@ pub struct Painter<'a> {
/// The children asked about so far, so the first box each was asked in /// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer. /// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>, pub(super) offered: Vec<WidgetId>,
/// The box this widget was first asked about in, in pixels. /// The lengths of the box this widget was first asked about in, in
/// pixels. Its children's offers are a fraction of it.
pub(super) offered_px: PxVec2, pub(super) offered_px: PxVec2,
/// Whether this draw is in that box, which makes the questions it asks /// Whether this draw is in a box of those lengths, which makes the
/// the ones a cold layout asks and their answers the ones to keep. /// questions it asks the ones a cold layout asks and their answers the
/// ones to keep.
pub(super) at_offer: bool, pub(super) at_offer: bool,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
@@ -132,30 +139,35 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, None) self.widget_at(id, region, region.size(), [false; 2])
} }
/// Draws a widget with an alignment chosen by its container rather than /// Draws a widget in `region`, saying what the answer means.
/// the widget's property. Containers use this when the box they hand down ///
/// already expresses the size they report around the child. /// `reports_of` is what a fraction the child reports is a fraction of, as
pub fn widget_aligned<'s, W: ?Sized>( /// lengths of this widget's own box. It is the box the child was given
/// wherever that box is the child's whole area -- a pad's inset, a stack
/// child, a scroll's content -- and a span passes its own extent along
/// the row instead: it offers each child the room left from its cursor,
/// because a text has to wrap at the width actually there, while
/// `rel(0.5)` still means half the span wherever the child sits in it.
///
/// A `decided` axis is one where this box was chosen from the widget's
/// own answer. On those the answer is not placed inside the box again: it
/// already is the box, and a fraction taken of it a second time would
/// shrink it twice. A container uses that where it hands back exactly
/// what a child asked for -- a span placing a child at the length it
/// reported, a scroll giving its content the content's own length.
pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
align: RegionAlign, reports_of: UiVec2,
) -> DrawResult<'s, 'a, W> { decided: [bool; 2],
self.widget_at(id, region, Some(align))
}
fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align_override: Option<RegionAlign>,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id())); let align = self.rsc.widgets().alignment(id.id());
// Composing `FULL` through a box is not quite the identity in f32, // Composing `FULL` through a box is not quite the identity in f32,
// so a child with nothing declared keeps the box it would have had. // so a child with nothing declared keeps the box it would have had.
let local = match declared.iter().any(Option::is_some) { let local = match declared.iter().any(Option::is_some) {
@@ -176,11 +188,20 @@ impl<'a> Painter<'a> {
self.children.push(id.id()); self.children.push(id.id());
} }
let first_ask = self.offer(id.id()); let first_ask = self.offer(id.id());
let offer = match first_ask { let given_len = local.size();
true => local, let offer_len = match first_ask {
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer), true => given_len,
false => self
.state
.active
.get(&id.id())
.map_or(given_len, |a| a.offer_len),
}; };
let answers_offer = self.at_offer && local == offer; let px = given_len.to_px(self.px);
let offered_px = offer_len.to_px(self.offered_px);
// Whether this ask is the child's offer question, which is a question
// about lengths: the same lengths somewhere else is the same question.
let answers_offer = self.at_offer && px == offered_px;
// The answer and what it holds for, both about the box asked in. The // The answer and what it holds for, both about the box asked in. The
// child's record may say something else once its drawing has been // child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box. // placed: a drawing made again in its placed box holds for that box.
@@ -194,9 +215,11 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
offer, given_len,
offered_px: self.px_within_offer(offer), offer_len,
align: align_override, px,
offered_px,
decided,
}, },
None, None,
self.rsc, self.rsc,
@@ -212,7 +235,7 @@ impl<'a> Painter<'a> {
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size, size: in_parent_frame(size, reports_of, declared),
} }
} }
@@ -246,25 +269,26 @@ impl<'a> Painter<'a> {
/// A child's length in the box it is about to be offered, if it can be /// A child's length in the box it is about to be offered, if it can be
/// had without drawing it: from its hint, or from a drawing it already /// had without drawing it: from its hint, or from a drawing it already
/// has that holds for that box. /// has that holds for that box. `reports_of` is what a fraction in the
/// answer is a fraction of, as it is for [`Self::widget_at`].
pub fn known_len<W: ?Sized>( pub fn known_len<W: ?Sized>(
&mut self, &mut self,
child: &StrongWidget<W>, child: &StrongWidget<W>,
axis: Axis, axis: Axis,
region: UiRegion, region: UiRegion,
reports_of: UiVec2,
) -> Option<LayoutLen> { ) -> Option<LayoutLen> {
let declared = self.declared_lens(child); let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id()); let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align); let local = declared_box(region, declared, align);
let within = local.within(&self.region);
let first_ask = self.offer(child.id()); let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) { if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer = local; active.offer_len = local.size();
} }
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return Some(hint);
} }
let px = self.state.px_of(self.move_idx, within); let px = local.size().to_px(self.px);
let (size, holds) = let (size, holds) =
self.state self.state
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?; .retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
@@ -278,7 +302,7 @@ impl<'a> Painter<'a> {
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); *under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
Some(size.axis(axis)) Some(in_parent_frame(size, reports_of, declared).axis(axis))
} }
/// Whether this is the first box a child is asked about in during a draw /// Whether this is the first box a child is asked about in during a draw
@@ -292,15 +316,6 @@ impl<'a> Painter<'a> {
true true
} }
/// The pixel size of a part of the box this widget was asked in.
fn px_within_offer(&self, local: UiRegion) -> PxVec2 {
let size = local.size();
PxVec2::new(
size.x.to_px(self.offered_px.x),
size.y.to_px(self.offered_px.y),
)
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) { if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id()); self.size_deps.push(child.id());
@@ -382,25 +397,25 @@ impl<'a> Painter<'a> {
/// near edge. A container that reports one child's size gives every child /// near edge. A container that reports one child's size gives every child
/// this, so what it draws is inside what it says it occupies. /// this, so what it draws is inside what it says it occupies.
pub fn box_of(&self, size: Size) -> UiRegion { pub fn box_of(&self, size: Size) -> UiRegion {
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2]) let lens = placed_lens(size, [None; 2], [false; 2]);
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
} }
/// This widget's box in pixels. Reading it makes the drawing one that /// This widget's box in pixels. Reading it makes the drawing one that
/// holds for this box only, until `holds` says how far it goes. /// holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 { pub fn px_size(&mut self) -> PxVec2 {
let px = self.state.px_of(self.move_idx, self.region); for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) {
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
} }
} }
px self.px
} }
/// One axis of this widget's box in pixels. Prefer this to /// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px { pub fn px_len(&mut self, axis: Axis) -> Px {
let len = self.state.px_of(self.move_idx, self.region).axis(axis); let len = self.px.axis(axis);
let own = &mut self.own[axis as usize]; let own = &mut self.own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
@@ -415,7 +430,7 @@ impl<'a> Painter<'a> {
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) { pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)), holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box", "'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(), self.label(),
self.id self.id
@@ -506,6 +521,24 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// A child's answer as lengths of the parent's own box. A widget reports a
/// fraction, and `reports_of` is the length that fraction is of: the box the
/// child was given wherever that is the child's whole area, and the parent's
/// own extent wherever the box is a positional remainder, as a span's is
/// after an earlier child. Pixels come through untouched either way, being
/// that many pixels wherever they end up. A declared axis is already the
/// parent's: it resolved the rule in its own box, and the rule is what the
/// report says.
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis));
}
}
size
}
/// What a widget declares a length of its box to be. `leftover` is not one: a /// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one, /// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead. /// so it passes up in the size instead.
@@ -526,31 +559,53 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
}) })
} }
/// The box a drawing occupies: the size the widget reported, on the side of /// Whether what a widget reported along an axis is the whole of the box it
/// the box it was asked in that its alignment says. An axis reported as a /// is in rather than a part to be placed inside it. A share fills, because a
/// share fills, because a share is a length only to whoever divides one, and /// share is a length only to whoever divides one, and whoever did is the one
/// whoever did is the one that handed down this box. A declared axis is /// that handed down this box. A declared axis does too: `declared_box`
/// left alone too: `declared_box` already placed it, in the parent's box, /// already placed it, in the parent's box, and the rule's length is what the
/// and the rule's length is what the widget reports there. /// widget reports there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// What of the box it was given a widget's drawing occupies, as lengths of
/// that box: the size it reported wherever that is a part to be placed, and
/// the whole of the box wherever the answer fills it.
/// ///
/// A reported fraction is a fraction of the box the widget drew in, where a /// A reported fraction is a fraction of the box the widget drew in, where a
/// declared one is a fraction of the box its parent handed down -- a span /// declared one is a fraction of the box its parent handed down -- a span
/// reporting `rel(1.0)` means all of what it was given, whatever that was a /// reporting `rel(1.0)` means all of what it was given, whatever that was a
/// fraction of. So this scales by the box rather than composing into it. /// fraction of. So this is a length of the box rather than a length composed
pub(crate) fn placed_box( /// into it, and a box in pixels is this step from the given box's pixels.
region: UiRegion, pub(crate) fn placed_lens(
size: Size, size: Size,
align: RegionAlign,
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
) -> UiRegion { decided: [bool; 2],
let mut placed = region; ) -> UiVec2 {
for (axis, declared) in AXES.into_iter().zip(declared) { let mut lens = UiVec2::FULL_SIZE;
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
let reported = size.axis(axis); let reported = size.axis(axis);
if reported.leftover != Weight::ZERO || declared.is_some() { if !fills(reported, declared, decided) {
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
}
}
lens
}
/// Where that drawing sits: those lengths taken of the box the widget was
/// asked in, on the side of it that the widget's alignment says.
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
let mut placed = region;
for axis in AXES {
// The whole of the box is already where it sits, and the arithmetic
// below is the identity for it.
if lens.axis(axis) == Len::FULL {
continue; continue;
} }
let span = placed.axis_mut(axis); let span = placed.axis_mut(axis);
let len = span.len().scale(reported.rel) + Len::from_parts(Rel::ZERO, reported.px); let len = lens.axis(axis).within_len(span.len());
span.start += (span.len() - len).scale(align.axis(axis).rel()); span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len; span.end = span.start + len;
} }
+285 -270
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, placed_box}; use crate::ui::painter::{declared_box, declared_lens, placed_box, placed_lens};
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight, PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
WidgetId, Widgets, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -20,13 +20,20 @@ pub(super) struct DrawInfo {
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
pub region_node: bool, pub region_node: bool,
pub mask: MaskIdx, pub mask: MaskIdx,
/// The box it was first asked about in, as a part of its parent's, and /// The box its parent gave it, as lengths of the parent's own box, and
/// that box in pixels. /// the lengths of the box it was first asked about in the same form.
pub offer: UiRegion, /// Both describe the box the *parent* stated, so the second, placing ask
/// carries them unchanged while its own region is the placement inside.
pub given_len: UiVec2,
pub offer_len: UiVec2,
/// This ask's box in pixels, and the offer's: one multiply from the
/// parent's own, which is where every pixel length in layout comes from.
pub px: PxVec2,
pub offered_px: PxVec2, pub offered_px: PxVec2,
/// A container's answer for where the widget sits. `None` uses the /// The axes along which the parent chose this box from the widget's own
/// widget's own property. /// answer, so the answer is not placed inside it again. See
pub align: Option<RegionAlign>, /// [`Painter::widget_at`].
pub decided: [bool; 2],
} }
pub struct UiRenderState { pub struct UiRenderState {
@@ -35,10 +42,10 @@ pub struct UiRenderState {
pub(super) output_size: PxVec2, pub(super) output_size: PxVec2,
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
/// The slot every chain bottoms out in, holding the output as a box.
root_move: MoveIdx,
/// Whether the output has changed since the last update. A frame is /// Whether the output has changed since the last update. A frame is
/// owed for that whether or not anything has to be drawn again. /// owed for that whether or not anything has to be drawn again: every
/// fraction becomes pixels against the output, in the shader's uniform
/// as well as here.
resized: bool, resized: bool,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw /// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot. /// replaces that while its children go on pointing at the slot.
@@ -50,6 +57,9 @@ pub struct UiRenderState {
/// Whether this frame contains a declared-length change, so any dirty /// Whether this frame contains a declared-length change, so any dirty
/// dependent replaces its answer too. /// dependent replaces its answer too.
replace_answers: bool, replace_answers: bool,
/// Widgets waiting for an ancestor to draw them, so the walk down the
/// depths does not pick one up again at its own depth.
deferred: crate::util::HashSet<WidgetId>,
pub moves: Moves, pub moves: Moves,
} }
@@ -63,50 +73,61 @@ impl UiRenderState {
slots: Default::default(), slots: Default::default(),
answer_invalid: Default::default(), answer_invalid: Default::default(),
replace_answers: false, replace_answers: false,
deferred: Default::default(),
moves: Default::default(), moves: Default::default(),
root_move: MoveIdx::NONE,
resized: false, resized: false,
} }
} }
/// The window as a box, so a chain bottoms out in one rather than in a
/// multiplication applied after it. Composing through a box held in
/// pixels leaves everything below it in pixels, which is why nothing
/// downstream has to know the output's size to resolve a position.
fn write_root(&mut self) {
let region = UiRegion::new(
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.x)),
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.y)),
);
match self.root_move == MoveIdx::NONE {
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
false => self.moves.set(self.root_move, region),
}
}
/// The window, in whatever the platform measures it in, onto the grid /// The window, in whatever the platform measures it in, onto the grid
/// everything below it is decided on. /// everything below it is decided on. No move entry holds it: a chain
pub fn resize(&mut self, size: impl Into<Vec2>) { /// bottoms out in `MoveIdx::NONE`, which is the window, and the window's
/// size is applied where a fraction becomes pixels -- here in `to_px`,
/// and in the shader by its uniform. A resize therefore rewrites no
/// retained entry at all.
///
/// The root is the only widget a resize marks, and only where the new
/// output falls outside what its answer holds for: that range is the
/// intersection of everything under it, so admitting the new output says
/// the whole tree still stands. Where it does not, the ordinary walk
/// draws the root, and each widget's own range decides how far down the
/// new length reaches.
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
let size = PxVec2::from_f32(size.into()); let size = PxVec2::from_f32(size.into());
if size == self.output_size { if size == self.output_size {
return; return;
} }
self.output_size = size; self.output_size = size;
self.write_root();
self.resized = true; self.resized = true;
let Some(root) = self.old_root else { return };
let stands = self
.active
.get(&root)
.is_some_and(|active| active.answers_at(active.given_len.to_px(size)));
if !stands {
widgets.needs_redraw.insert(root);
}
} }
fn root_info(&self) -> DrawInfo { /// The root is asked about in the output: the window is where a fraction
/// becomes pixels rather than a box of its own, so the root's box is the
/// first length threaded down. Its own rules narrow that box, and where
/// they do the narrowed box is also the offer -- nothing above it chose
/// anything else.
fn root_info(&self, region: UiRegion) -> DrawInfo {
let px = region.size().to_px(self.output_size);
DrawInfo { DrawInfo {
layer: 0, layer: 0,
parent: None, parent: None,
depth: 1, depth: 1,
parent_move: self.root_move, parent_move: MoveIdx::NONE,
region_node: false, region_node: false,
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
offer: UiRegion::FULL, given_len: region.size(),
offered_px: self.output_size, offer_len: UiVec2::FULL_SIZE,
align: None, px,
offered_px: px,
decided: [false; 2],
} }
} }
@@ -139,17 +160,6 @@ impl UiRenderState {
if self.root_changed(root) { if self.root_changed(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
} else if let Some(root) = root
&& self.resized
{
// The output is the root's box, so a resize is that box changing
// length, found the way every other box change is found. Before
// anything dirty settles, so that whatever a new output draws
// again is drawn once, in the box it will have.
let info = self.root_info();
let region = Self::root_region(root.id(), rsc.widgets());
let answer = self.draw_inner(root.id(), region, info, None, rsc);
self.active.get_mut(&root.id()).unwrap().answer = answer;
} }
self.resized = false; self.resized = false;
if rsc.widgets().has_updates() { if rsc.widgets().has_updates() {
@@ -163,11 +173,9 @@ impl UiRenderState {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::FullLayout); let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc); self.clear(rsc);
// free all resources & cache
self.write_root();
if let Some(id) = root { if let Some(id) = root {
let info = self.root_info();
let region = Self::root_region(id.id(), rsc.widgets()); let region = Self::root_region(id.id(), rsc.widgets());
let info = self.root_info(region);
self.draw_inner(id.id(), region, info, None, rsc); self.draw_inner(id.id(), region, info, None, rsc);
} }
} }
@@ -191,88 +199,94 @@ impl UiRenderState {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::DrawRequests); diag::bump(Counter::DrawRequests);
diag::draw_request( diag::draw_request(id, info.parent, region, info.px, info.region_node);
id,
info.parent,
region,
self.px_of(info.parent_move, region),
info.region_node,
);
} }
let own_align = rsc.widgets().alignment(id); let align = rsc.widgets().alignment(id);
let align = info.align.unwrap_or(own_align); // Nothing this widget measured can be dirty while it draws: layout is
let replace_answer = self.answer_invalid.remove(&id) // one bottom-up walk, so anything deeper has settled or deferred to
|| (self.replace_answers // its own parent, and a deferred one leaves that parent marked.
&& (rsc.widgets().needs_redraw.contains(&id) let stale = rsc.widgets().needs_redraw.contains(&id);
|| self.dirty_size_under(id, rsc.widgets()))); let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
let retained = match replace_answer { let retained = match replace_answer || stale {
true => None, true => None,
false => self false => self
.retained_answer(id, region, info, rsc.widgets()) .retained_answer(id, info)
.or_else(|| self.try_reuse(id, region, info, rsc)), .or_else(|| self.try_reuse(id, region, info, rsc)),
}; };
let answer = retained.unwrap_or_else(|| { let answer = retained.unwrap_or_else(|| {
if old.is_none() { if old.is_none() {
old = self.remove(id, false, rsc); old = self.remove(id, false, rsc);
} }
self.draw_at(id, region, info, align, old.take(), rsc) self.draw_at(id, region, info, old.take(), rsc)
}); });
let declared = declared_lens(rsc.widgets(), id); let declared = declared_lens(rsc.widgets(), id);
// A near-edge override means the caller already chose this box from // The second, final ask is in a box chosen from the answer on both
// the child's answer. Applying the answer again would compound the // axes, which is also what makes it terminate.
// placement; it is also how the second, final ask terminates. let lens = placed_lens(answer.0, declared, info.decided);
let placed = match info.align == Some(RegionAlign::NEAR) { let placed = placed_box(region, lens, align);
true => region,
false => placed_box(region, answer.0, align, declared),
};
let placed_info = DrawInfo { let placed_info = DrawInfo {
align: Some(RegionAlign::NEAR), px: lens.to_px(info.px),
decided: [true; 2],
..info ..info
}; };
// The symbolic box can be unchanged while its parent slot changed self.place(id, placed, placed_info, rsc);
// pixel size. Reuse checks the resolved box even in that case.
if self.try_reuse(id, placed, placed_info, rsc).is_none() {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc);
}
// The answer is only reusable while both parts of the operation are: // The answer is only reusable while both parts of the operation are:
// what the widget reported in the offered box, and what it drew in // what the widget reported in the box it was asked in, and what it
// the box its report selected. Express the latter's contract back in // drew in the box its report selected. Express the latter's contract
// terms of the offered box before handing it to the parent. // back in terms of the box asked in before handing it to the parent.
let drawing_holds = self.active[&id].holds; let drawing_holds = self.active[&id].holds;
let mut settled = answer; let mut settled = answer;
for axis in AXES { for axis in AXES {
let reported = answer.0.axis(axis);
let placed_len =
match reported.leftover != Weight::ZERO || declared[axis as usize].is_some() {
true => Len::FULL,
false => Len::from_parts(reported.rel, reported.px),
};
settled.1[axis as usize] = settled.1[axis as usize] =
settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len)); settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis)));
} }
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.offer = info.offer; // Whoever asked owns how the box was reached: the box it stated, and
// what of that box the answer then took. A local redraw asks the
// same question again from these.
active.given = region;
active.given_len = info.given_len;
active.offer_len = info.offer_len;
active.answer = settled; active.answer = settled;
active.align = align; active.decided = info.decided;
active.align_override = info.align.is_some(); active.own_align = align;
active.own_align = own_align; // A subtree can be reused whole under a different parent -- same box,
active.depth = info.depth; // same layer, same region node -- and nothing in the drawing says it
// changed hands. Two things read who its parent is: a deferral, which
// marks whoever has it to draw, and the old parent's list of children,
// which its next draw undraws whatever is missing from.
let old_parent = std::mem::replace(&mut active.parent, info.parent);
if old_parent != info.parent
&& let Some(old_parent) = old_parent
&& let Some(old_parent) = self.active.get_mut(&old_parent)
{
old_parent.children.retain(|child| *child != id);
}
settled settled
} }
/// Draws a widget in the final box its answer chose, reusing the drawing
/// already there where its retained contract holds for that box. The
/// symbolic box can be unchanged while the box it sits in changed pixel
/// length, so what reuse checks is the box in pixels.
fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
if self.try_reuse(id, placed, info, rsc).is_none() {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, placed, info, old, rsc);
}
}
/// Calls a widget's `draw` and keeps what it drew in `region`. /// Calls a widget's `draw` and keeps what it drew in `region`.
fn draw_at( fn draw_at(
&mut self, &mut self,
id: WidgetId, id: WidgetId,
region: UiRegion, region: UiRegion,
info: DrawInfo, info: DrawInfo,
align: RegionAlign,
old: Option<ActiveData>, old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) { ) -> (Size, [Holds; 2]) {
@@ -293,12 +307,17 @@ impl UiRenderState {
None => (Vec::new(), None), None => (Vec::new(), None),
}; };
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
let px = self.px_of(move_idx, local); // A box of the offered lengths asks the offer's question wherever it
let at_offer = same_px(px, info.offered_px); // sits, since what a drawing depends on is its lengths -- and
// equality is the comparison, these being counts of a step rather
// than floats to be compared for nearness.
let px = info.px;
let at_offer = px == info.offered_px;
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
region: local, region: local,
px,
mask: info.mask, mask: info.mask,
layer: info.layer, layer: info.layer,
own_layer: info.layer, own_layer: info.layer,
@@ -332,6 +351,7 @@ impl UiRenderState {
state: _, state: _,
rsc: _, rsc: _,
region: _, region: _,
px: _,
mask, mask,
textures, textures,
primitives, primitives,
@@ -401,9 +421,11 @@ impl UiRenderState {
parent_move: move_idx, parent_move: move_idx,
region_node: false, region_node: false,
mask, mask,
offer: UiRegion::FULL, given_len: UiVec2::FULL_SIZE,
offer_len: UiVec2::FULL_SIZE,
px,
offered_px: px, offered_px: px,
align: None, decided: [false; 2],
}, },
rsc, rsc,
); );
@@ -414,7 +436,12 @@ impl UiRenderState {
let active = ActiveData { let active = ActiveData {
id, id,
region, region,
offer: info.offer, // The box a placing ask draws in is a part of the one its parent
// gave, which `draw_inner` writes back over these once the
// placement is done.
given: region,
given_len: info.given_len,
offer_len: info.offer_len,
// Whoever asked writes the answer, if this was the asking. // Whoever asked writes the answer, if this was the asking.
answer: old_answer.unwrap_or((size, holds)), answer: old_answer.unwrap_or((size, holds)),
size, size,
@@ -427,12 +454,12 @@ impl UiRenderState {
children, children,
size_deps, size_deps,
declared: declared_lens(rsc.widgets(), id), declared: declared_lens(rsc.widgets(), id),
align, decided: info.decided,
align_override: info.align.is_some(),
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
move_idx, move_idx,
parent_move: info.parent_move, parent_move: info.parent_move,
mask, mask,
parent_mask: info.mask,
layer: info.layer, layer: info.layer,
}; };
rsc.on_draw(&active); rsc.on_draw(&active);
@@ -460,20 +487,6 @@ impl UiRenderState {
} }
} }
/// The pixel size of a region held in `slot`'s coordinates.
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 {
self.moves
.resolve(slot, region)
.size()
.to_px(self.output_size)
}
/// Where a region held in `slot`'s coordinates lands on screen, which is
/// the walk the vertex shader does.
fn px_region(&self, slot: MoveIdx, region: UiRegion) -> PixelRegion {
self.moves.resolve(slot, region).to_px(self.output_size)
}
/// A clean widget's retained answer, if that answer holds for a box of /// A clean widget's retained answer, if that answer holds for a box of
/// `px`. This does not move its drawing, which may already be in the box /// `px`. This does not move its drawing, which may already be in the box
/// that answer placed it in. /// that answer placed it in.
@@ -484,7 +497,7 @@ impl UiRenderState {
parent_move: MoveIdx, parent_move: MoveIdx,
widgets: &Widgets, widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> { ) -> Option<(Size, [Holds; 2])> {
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) { if widgets.needs_redraw.contains(&id) {
return None; return None;
} }
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
@@ -498,17 +511,9 @@ impl UiRenderState {
/// The answer to an ask can be retained independently of where its /// The answer to an ask can be retained independently of where its
/// drawing ended up. Alignment is exactly that case: the first box is the /// drawing ended up. Alignment is exactly that case: the first box is the
/// question and the smaller placed box holds the drawing. /// question and the smaller placed box holds the drawing. Whether the
fn retained_answer( /// answer is stale at all is its caller's question, asked once there.
&self, fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, [Holds; 2])> {
id: WidgetId,
region: UiRegion,
info: DrawInfo,
widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> {
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
return None;
}
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
let has_region_node = active.move_idx != active.parent_move; let has_region_node = active.move_idx != active.parent_move;
if !active.drawn if !active.drawn
@@ -517,48 +522,42 @@ impl UiRenderState {
{ {
return None; return None;
} }
let px = self.px_of(info.parent_move, region); active.answers_at(info.px).then_some(active.answer)
let (size, holds) = active.answer;
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds))
} }
/// Whether anything whose size this widget's own size was read from is /// The pixel lengths of the box a widget was given and of the box it was
/// dirty. Not needed for the answer to come right -- a changed size /// first asked about, which is what a local redraw needs to ask the
/// reaches its reader in any order -- but a reader that asks first /// question its parent asked.
/// lays out once rather than twice. ///
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool { /// Both are threaded down from the window a length of a box at a time,
self.active.get(&id).is_some_and(|active| { /// and this takes the same steps back up: a widget's box is a length of
active.size_deps.iter().any(|child| { /// the box its parent drew in, and its offer a length of the box its
widgets.needs_redraw.contains(child) || self.dirty_size_under(*child, widgets) /// parent was itself offered. Neither chain has a coordinate frame in it,
}) /// so neither breaks at a region node -- and both land on the numbers a
}) /// cold layout computes, rather than near them.
} fn asked_px(&self, id: WidgetId) -> (PxVec2, PxVec2) {
/// The first box a widget was asked about, re-expressed in the coordinate
/// space its drawing uses. Keeping the relative box and composing it
/// again avoids rebuilding a shifted box from rounded pixel lengths.
fn offered_region(&self, id: WidgetId) -> UiRegion {
let active = &self.active[&id]; let active = &self.active[&id];
let parent_region = match active.parent.and_then(|id| self.active.get(&id)) { // Nothing above the root: the window is where a fraction becomes
Some(parent) if parent.move_idx == active.parent_move => { // pixels, which is also the whole of the box the root is given.
if parent.move_idx == parent.parent_move { let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) {
self.offered_region(parent.id) Some(parent) => {
} else { let (given, offer) = self.asked_px(parent.id);
UiRegion::FULL let lens = placed_lens(parent.answer.0, parent.declared, parent.decided);
} (lens.to_px(given), offer)
} }
_ => UiRegion::FULL, None => (self.output_size, self.output_size),
};
let mut offered = match active.offer == UiRegion::FULL {
true => parent_region,
false => active.offer.within(&parent_region),
}; };
let px = active.given_len.to_px(parent_px);
let mut offered = active.offer_len.to_px(parent_offer);
for axis in AXES { for axis in AXES {
// A declared length is resolved by whoever drew the widget, in
// the box that widget drew in, so the box it has is the box it
// was asked about however the offer above it moved.
if active.declared[axis as usize].is_some() { if active.declared[axis as usize].is_some() {
*offered.axis_mut(axis) = *active.region.axis(axis); *offered.axis_mut(axis) = px.axis(axis);
} }
} }
offered (px, offered)
} }
/// Reuses the actual drawing in a new box if its retained contract holds /// Reuses the actual drawing in a new box if its retained contract holds
@@ -615,10 +614,10 @@ impl UiRenderState {
} }
return None; return None;
} }
// In pixels, because `region` is a fraction of a slot's box and that // In pixels, because `region` is a fraction of the box its parent
// box may be what changed -- an unchanged fraction of a box half the // drew in and that box may be what changed -- an unchanged fraction
// size is half the widget. // of a box half the size is half the widget.
if !active.holds_at(self.px_of(info.parent_move, region)) { if !active.holds_at(info.px) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::ReuseOutside); diag::bump(Counter::ReuseOutside);
@@ -627,24 +626,22 @@ impl UiRenderState {
return None; return None;
} }
let moved = active.region != region; let moved = active.region != region;
let (answer, old_region, slot, mask) = ( let (answer, old_region, slot) =
(active.size, active.holds), ((active.size, active.holds), active.region, active.move_idx);
active.region,
active.move_idx,
info.mask,
);
if moved { if moved {
if has_region_node { if has_region_node {
self.moves.set(slot, region); self.moves.set(slot, region);
} else { } else {
let remap = RegionRemap::new(old_region, region)?; let remap = RegionRemap::new(old_region, region)?;
self.remap_subtree(id, &remap, info.parent_move, mask, rsc); self.remap_subtree(id, &remap, info.parent_move, rsc);
} }
} }
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.region = region; active.region = region;
active.offer = info.offer; active.given = region;
active.depth = info.depth; active.given_len = info.given_len;
active.offer_len = info.offer_len;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
match (moved, has_region_node) { match (moved, has_region_node) {
@@ -668,6 +665,24 @@ impl UiRenderState {
Some(answer) Some(answer)
} }
/// A reused subtree keeps its shape, so every widget in it moves by the
/// same amount -- and where the top of it did not move, none of it did,
/// which is what makes this free in the ordinary case.
fn redepth(&mut self, id: WidgetId, depth: usize) {
let Some(active) = self.active.get_mut(&id) else {
return;
};
if active.depth == depth {
return;
}
active.depth = depth;
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
self.redepth(child, depth + 1);
}
}
/// Re-expresses an ordinary retained subtree in a new parent region. /// Re-expresses an ordinary retained subtree in a new parent region.
/// An independently movable descendant needs only its own region changed; /// An independently movable descendant needs only its own region changed;
/// its contents stay in that region's coordinate space. /// its contents stay in that region's coordinate space.
@@ -676,10 +691,10 @@ impl UiRenderState {
id: WidgetId, id: WidgetId,
remap: &RegionRemap, remap: &RegionRemap,
parent_move: MoveIdx, parent_move: MoveIdx,
inherited_mask: MaskIdx,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) { ) {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.given = remap.apply(active.given);
if active.move_idx != parent_move { if active.move_idx != parent_move {
let region = remap.apply(active.region); let region = remap.apply(active.region);
active.region = region; active.region = region;
@@ -691,16 +706,18 @@ impl UiRenderState {
*region = remap.apply(*region); *region = remap.apply(*region);
} }
active.region = remap.apply(active.region); active.region = remap.apply(active.region);
let mask = active.mask; let own_mask = (active.mask != active.parent_mask).then_some(active.mask);
let children = active.children.len(); let children = active.children.len();
if mask != inherited_mask && mask != MaskIdx::NONE { // A mask the widget set itself moves with it; one it inherited
let mask = rsc.ui_mut().masks.get_mut(mask); // belongs to the widget that set it, and moves there or not at all.
if let Some(idx) = own_mask {
let mask = rsc.ui_mut().masks.get_mut(idx);
debug_assert_eq!(mask.move_idx, parent_move); debug_assert_eq!(mask.move_idx, parent_move);
mask.region = remap.apply(mask.region); mask.region = remap.apply(mask.region);
} }
for index in 0..children { for index in 0..children {
let child = self.active[&id].children[index]; let child = self.active[&id].children[index];
self.remap_subtree(child, remap, parent_move, mask, rsc); self.remap_subtree(child, remap, parent_move, rsc);
} }
} }
@@ -775,7 +792,9 @@ impl UiRenderState {
ActiveData { ActiveData {
id, id,
region: UiRegion::FULL, region: UiRegion::FULL,
offer: UiRegion::FULL, given: UiRegion::FULL,
given_len: UiVec2::FULL_SIZE,
offer_len: UiVec2::FULL_SIZE,
answer: (size, [Holds::ANY; 2]), answer: (size, [Holds::ANY; 2]),
size, size,
holds: [Holds::ANY; 2], holds: [Holds::ANY; 2],
@@ -788,11 +807,11 @@ impl UiRenderState {
size_deps: Vec::new(), size_deps: Vec::new(),
move_idx: info.parent_move, move_idx: info.parent_move,
declared: [None; 2], declared: [None; 2],
align: RegionAlign::default(), decided: [false; 2],
align_override: false,
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
parent_move: info.parent_move, parent_move: info.parent_move,
mask: info.mask, mask: info.mask,
parent_mask: info.mask,
layer: info.layer, layer: info.layer,
}, },
); );
@@ -808,7 +827,6 @@ impl UiRenderState {
self.answer_invalid.clear(); self.answer_invalid.clear();
self.replace_answers = false; self.replace_answers = false;
self.moves.clear(); self.moves.clear();
self.root_move = MoveIdx::NONE;
self.layers.clear(); self.layers.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
self.free(rsc); self.free(rsc);
@@ -829,18 +847,33 @@ impl UiRenderState {
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::IncrementalLayout); let _layout = diag::timer(TimerKind::IncrementalLayout);
// Deepest first: a reader whose children have all settled asks each // Deepest first, and strictly: a widget that cannot settle where it
// once, where any other order has it lay out again for whatever // is defers to its parent rather than drawing the parent from
// settles under it afterwards. Equal-depth widgets are independent, // inside itself. It marks the parent, stays marked, and waits here
// so their order does not matter. // until the walk reaches its parent's depth.
while let Some(id) = { //
let dirty = rsc.widgets().needs_redraw.iter().copied(); // What that buys is that nothing shallower is ever drawn while
dirty.max_by_key(|&id| self.depth(id)) // anything deeper is still dirty. A parent drawing can therefore
} { // trust every answer it reads without descending to check whether
// something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved.
loop {
let next = rsc
.widgets()
.needs_redraw
.iter()
.copied()
.filter(|id| !self.deferred.contains(id))
.max_by_key(|&id| self.depth(id));
let Some(id) = next else { break };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops); diag::bump(Counter::QueuePops);
self.redraw(id, rsc); if !self.redraw(id, rsc) {
self.deferred.insert(id);
}
} }
self.deferred.clear();
} }
fn depth(&self, id: WidgetId) -> usize { fn depth(&self, id: WidgetId) -> usize {
@@ -909,20 +942,29 @@ impl UiRenderState {
} }
/// Where a widget is on screen: its box composed through the boxes it /// Where a widget is on screen: its box composed through the boxes it
/// sits within. `None` for one that is not drawn. /// sits within, the same walk the vertex shader does. `None` for one that
/// is not drawn.
///
/// This is for asking where a drawing landed: hit testing, and a test
/// reading a box back. Layout decides on the lengths threaded down the
/// draw instead, and a position is not one of its inputs.
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> { pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?; let active = self.active.get(&id.id())?;
active active.drawn.then(|| {
.drawn self.moves
.then(|| self.px_region(active.parent_move, active.region)) .resolve(active.parent_move, active.region)
.to_px(self.output_size)
})
} }
/// Settles a dirty widget: asks it again where its parent asked, and /// Settles a dirty widget: asks it again where its parent asked, and
/// tells the parent if the answer changed. /// tells the parent if the answer changed. `false` where the question is
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { /// its parent's rather than its own, which leaves it marked for the
/// parent to draw when the walk reaches that depth.
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> bool {
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
let Some(active) = self.active.get(&id) else { let Some(active) = self.active.get(&id) else {
return; return true;
}; };
// Its parent resolved its declared lengths into its box and decided // Its parent resolved its declared lengths into its box and decided
// whether to draw it at all, so a change to either is the parent's // whether to draw it at all, so a change to either is the parent's
@@ -942,91 +984,74 @@ impl UiRenderState {
at = self.active[&next].parent; at = self.active[&next].parent;
} }
} }
// Both stay marked: the parent because it has this to draw, and
// this because the parent must draw it rather than keep what it
// has. The mark comes off in `draw_at`, where the parent draws.
rsc.widgets_mut().needs_redraw.insert(id); rsc.widgets_mut().needs_redraw.insert(id);
self.redraw(parent, rsc); rsc.widgets_mut().needs_redraw.insert(parent);
// Whatever the parent did not draw again is nothing it holds now. return false;
rsc.widgets_mut().needs_redraw.remove(&id);
return;
} }
if !active.drawn { if !active.drawn {
return; return true;
} }
let region = active.region; // Nothing above the root resolved its rules or its alignment, so its
let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id); // box is its own to work out again against the output. Every other
let asked_in = match active.parent { // widget was given one.
Some(_) => self.offered_region(id), let Some(parent) = active.parent else {
None => Self::root_region(id, rsc.widgets()), let region = Self::root_region(id, rsc.widgets());
let info = DrawInfo {
mask: active.parent_mask,
..self.root_info(region)
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
self.draw_inner(id, region, info, old, rsc);
return true;
}; };
let offered_px = self.px_of(active.parent_move, asked_in); let (given_px, offered_px) = self.asked_px(id);
// Whole boxes rather than lengths: an offer as long as the final box // Asked again in the box its parent gave it, which is the question
// but somewhere else is a different box, and a region node drawing at // its parent asked only while that box is as long as the offer. Any
// its offer writes the box it drew in into its own entry. // other box is a different question, so the parent asks it, with the
let at_offer = same_pixel_region( // mark left on. Lengths and not whole boxes: what a drawing depends
self.px_region(active.parent_move, region), // on is its lengths, so the same lengths elsewhere is one question.
self.px_region(active.parent_move, asked_in), if given_px != offered_px {
);
let parent_must_place =
active.parent.is_some() && (!region_node || active.align_override) && !at_offer;
// An independently positioned region node can redraw at its offer
// and move its slot to its own placement. Every other widget needs
// its parent to reproduce a different final position.
if let Some(parent) = active.parent
&& parent_must_place
{
rsc.widgets_mut().needs_redraw.insert(id); rsc.widgets_mut().needs_redraw.insert(id);
self.redraw(parent, rsc); rsc.widgets_mut().needs_redraw.insert(parent);
rsc.widgets_mut().needs_redraw.remove(&id); return false;
return;
} }
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: active.parent, parent: active.parent,
depth: active.depth, depth: active.depth,
parent_move: active.parent_move, parent_move: active.parent_move,
region_node, region_node: rsc.widgets().is_region_node(id),
mask: active.mask, mask: active.parent_mask,
offer: active.offer, given_len: active.given_len,
offer_len: active.offer_len,
px: given_px,
offered_px, offered_px,
align: active.align_override.then_some(active.align), decided: active.decided,
}; };
let (was_answer, was) = (active.answer, (active.size, active.holds)); let (given, was_answer) = (active.given, active.answer);
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
let answer = self.draw_inner(id, asked_in, info, old, rsc); // `draw_inner` places the answer inside that box itself, which is the
self.active.get_mut(&id).unwrap().answer = answer; // ask that leaves the widget where its parent put it.
let Some(parent) = info.parent else { let answer = self.draw_inner(id, given, info, old, rsc);
return;
};
if answer != was_answer { if answer != was_answer {
// Left where it was asked: the parent lays out again and chooses // Its parent chose its box knowing the old answer, so it lays out
// its final box. // again and chooses the box the new one asks for.
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::SizeChanges); diag::bump(Counter::SizeChanges);
diag::bump(Counter::ReaderEdges); diag::bump(Counter::ReaderEdges);
} }
rsc.widgets_mut().needs_redraw.insert(parent); rsc.widgets_mut().needs_redraw.insert(parent);
return;
}
if at_offer {
return;
}
// Then in the final box its parent chose from that answer. That box
// is already placed, so the near edge goes with it: applying the
// widget's own alignment to it again would place its content twice,
// the way it did for a region node under a `Stack` once the stack
// stopped overriding every child's alignment.
let placed_info = DrawInfo {
align: Some(RegionAlign::NEAR),
..info
};
self.draw_inner(id, region, placed_info, None, rsc);
let active = &self.active[&id];
if (active.size, active.holds) != was {
rsc.widgets_mut().needs_redraw.insert(parent);
} }
true
} }
} }
@@ -1039,16 +1064,6 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
} }
/// The same box is the same number of steps, both of these being lengths on
/// the grid rather than floats to be compared for nearness.
fn same_px(a: PxVec2, b: PxVec2) -> bool {
a == b
}
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right)
}
/// A retained region rewritten from one parent box into another. A fixed /// A retained region rewritten from one parent box into another. A fixed
/// source extent can be translated but cannot recover fractions for a resize. /// source extent can be translated but cannot recover fractions for a resize.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
+5 -1
View File
@@ -20,10 +20,14 @@ impl DefaultAppState for Client {
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
( (
// The square is one widget and the two shares of the row it
// sits centred in are another: a length is a property of a
// widget, so `.width` here would overwrite the `.sized`.
rrect rrect
.color(Color::RED) .color(Color::RED)
.sized((100, 100)) .sized((100, 100))
.center() .center()
.wrapper()
.width(leftover(2)), .width(leftover(2)),
( (
rrect.color(Color::ORANGE), rrect.color(Color::ORANGE),
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(rsc);
let main = WidgetPtr::new().add(rsc); let main = Wrapper::new().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new()))); let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| { let mut switch_button = |color, to: WeakWidget, label| {
+7 -3
View File
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
.pad(16) .pad(16)
.background(panel()); .background(panel());
// Each one takes the whole width, because `text_align` puts the
// glyphs somewhere in the box the text is given and a text that
// reports the width of its own glyphs is given exactly that.
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
let aligned = ( let aligned = (
wtext("left").size(24).text_align(Align::LEFT), label("left", Align::LEFT),
wtext("centred").size(24).text_align(Align::CENTER), label("centred", Align::H_CENTER),
wtext("right").size(24).text_align(Align::RIGHT), label("right", Align::RIGHT),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.gap(8) .gap(8)
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height)); render.resize((size.width, size.height), rsc.widgets_mut());
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
+3 -3
View File
@@ -144,9 +144,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test // bound that comes with `SyncSender` is far past anything a test
// leaves unread. // leaves unread.
let (send, updates) = sync_channel(1024); let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send))); let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize(size); render.resize(size, rsc.widgets_mut());
Self { Self {
rsc, rsc,
render, render,
@@ -161,7 +161,7 @@ impl Harness {
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size); self.render.resize(size, self.rsc.widgets_mut());
} }
/// Changes a length rule after the fact, the way `.width()` sets one. /// Changes a length rule after the fact, the way `.width()` sets one.
+2 -2
View File
@@ -1,15 +1,15 @@
mod image; mod image;
mod mask; mod mask;
mod position; mod position;
mod ptr;
mod rect; mod rect;
mod text; mod text;
mod trait_fns; mod trait_fns;
mod wrapper;
pub use image::*; pub use image::*;
pub use mask::*; pub use mask::*;
pub use position::*; pub use position::*;
pub use ptr::*;
pub use rect::*; pub use rect::*;
pub use text::*; pub use text::*;
pub use trait_fns::*; pub use trait_fns::*;
pub use wrapper::*;
+3 -2
View File
@@ -14,7 +14,8 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis); let container_len = painter.px_len(self.axis);
// Draw in the whole container only when its scrolling-axis length is // Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset. // not already known, then draw it at the scrolled offset.
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) { let whole = UiRegion::FULL;
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) {
Some(len) => len, Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis), None => painter.widget(&self.inner).size().axis(self.axis),
}; };
@@ -63,7 +64,7 @@ impl Widget for Scroll {
region = region.offset(offset); region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
} }
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR); painter.widget_at(&self.inner, region, region.size(), [true; 2]);
// 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
+33 -37
View File
@@ -20,9 +20,15 @@ impl Widget for Span {
span.flip(); span.flip();
} }
let region = UiRegion::from_axis(axis, span, UiSpan::FULL); let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
let len = match painter.known_len(child, axis, region) { // Offered the room left from the cursor, because a text has to
// wrap at the width actually there, but reporting a fraction of
// the whole row: `rel(0.5)` is half the span whatever else is in
// it and wherever this child sits among them.
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
Some(len) => len, Some(len) => len,
None => painter.widget_within(child, region).len(axis), None => painter
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
.len(axis),
}; };
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
@@ -40,43 +46,26 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// What is left for the shares to divide: the box less everything
// fixed, as a length of the box rather than a number of pixels.
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. The room to // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// divide is `len * fixed - total.px`, and the length where it runs // itself, and answered back through the same expression, so the
// out is exactly the box a parent sizing itself from this answer // boundary is the drawing's own and not a second way of finding it:
// hands back -- which is why this used to need a margin either side // the three cases a rounded division needed -- the fixed parts
// of the boundary, and why it does not now: that box and this sum are // growing slower than the box, faster, or exactly with it -- are the
// whole counts of the same step, and both routes to it land on the // sign of `room.rel`, which `through` already reads. What the
// same count. What the generated oracle checks is the consequence, // generated oracle checks is the consequence, since which children
// since which children exist at all turns on this. // exist at all turns on this.
let fixed = Rel::ONE - total.rel;
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
let current = painter.px_len(axis); shares = room.to_px(painter.px_len(axis)) > Px::ZERO;
let holds = if fixed > Rel::ZERO { let holds = match shares {
// The box length the fixed parts alone fill. true => Holds::from(Px::STEP..=Px::MAX),
let full = total.px.div(fixed); false => Holds::from(Px::MIN..=Px::ZERO),
shares = current > full;
match shares {
true => Holds::from(full.next_up()..=Px::MAX),
false => Holds::from(Px::MIN..=full),
}
} else if fixed < Rel::ZERO {
// The relative parts grow faster than the box does, so here
// a shorter box is the one that leaves room.
let full = total.px.div(fixed);
shares = current < full;
match shares {
true => Holds::from(Px::MIN..=full.next_down()),
false => Holds::from(full..=Px::MAX),
}
} else {
// The relative parts take exactly the box, whatever it is, so
// the only room is what negative pixels leave.
shares = total.px < Px::ZERO;
Holds::ANY
}; };
painter.holds(axis, holds); painter.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
@@ -93,7 +82,6 @@ impl Widget for Span {
// row. // row.
let mut fixed = Len::rel_min(); let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
let mut start = Len::rel_min(); let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for (child, len) in self.children.iter().zip(&lens) {
@@ -119,7 +107,15 @@ impl Widget for Span {
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
region.flip(axis); region.flip(axis);
} }
let placed = painter.widget_within(child, region); // Along the row this box is the child's own answer, so the answer
// is not placed in it again; across it the child sits where its
// alignment says.
let placed = painter.widget_at(
child,
region,
UiVec2::FULL_SIZE,
[axis == Axis::X, axis == Axis::Y],
);
if shrinks { if shrinks {
let used = placed.len(!axis); let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
+1 -1
View File
@@ -35,7 +35,7 @@ impl Widget for Stack {
// child is handed a box that owes nothing to its own answer, and // child is handed a box that owes nothing to its own answer, and
// where it sits in one bigger than itself is its own business. // where it sits in one bigger than itself is its own business.
match sizing == Some(i) { match sizing == Some(i) {
true => painter.widget_aligned(child, region, RegionAlign::NEAR), true => painter.widget_at(child, region, region.size(), [true; 2]),
false => painter.widget_within(child, region), false => painter.widget_within(child, region),
}; };
} }
+12 -2
View File
@@ -55,8 +55,13 @@ impl TextView {
// line up to the one it was made at: each line still fits, and none // line up to the one it was made at: each line still fits, and none
// could take a word that did not fit in the wider box. A line too // could take a word that did not fit in the wider box. A line too
// long to fit at all says nothing about narrower boxes. // long to fit at all says nothing about narrower boxes.
//
// The step at or above that longest line rather than the nearest
// one, since the shaper measures in floats: the nearest step is
// under the line half the time, and a range starting there admits a
// box the line does not fit in, where the break is not this one.
if let Some(width) = width { if let Some(width) = width {
painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width); painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
} }
text text
} }
@@ -78,7 +83,12 @@ impl TextView {
let tex = self.render(painter); let tex = self.render(painter);
let region = tex.size.align(align); let region = tex.size.align(align);
let size = Size::px(tex.size); // The step at or above what the shaper measured, so a parent that
// hands back the length this reports hands back a box the longest
// line fits in. Rounded to the nearest step it is half the time a
// hair under that line, and the break made in it is not the break a
// cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
let within = region.within(&painter.region()); let within = region.within(&painter.region());
painter.glyphs(tex, within); painter.glyphs(tex, within);
(region, size) (region, size)
+7 -3
View File
@@ -134,9 +134,13 @@ widget_trait! {
|state| self.add(state) |state| self.add(state)
} }
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) { // Named for the type it makes rather than as `wrapped`, which would read
let id = self.add_strong(state); // as the text setting. `widget_trait!` takes no attributes, so what it is
state.ui_mut().widgets[ptr].inner = Some(id); // for is on `Wrapper` itself.
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|state| Wrapper {
inner: Some(self.add_strong(state)),
}
} }
} }
+12 -4
View File
@@ -1,11 +1,19 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::Unsize; use std::marker::Unsize;
pub struct WidgetPtr { /// One widget in a box of its own, doing as little as possible on the way:
/// it draws its child in the whole of its box and reports back what the child
/// said. It exists because a length and an alignment are properties of one
/// widget, so a widget cannot both be 100 wide and take two shares of a row
/// -- the two lengths need two widgets, and this is the smaller one.
///
/// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for.
pub struct Wrapper {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
} }
impl Widget for WidgetPtr { impl Widget for Wrapper {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
match &self.inner { match &self.inner {
Some(id) => painter.widget(id).size(), Some(id) => painter.widget(id).size(),
@@ -14,7 +22,7 @@ impl Widget for WidgetPtr {
} }
} }
impl WidgetPtr { impl Wrapper {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -35,7 +43,7 @@ impl WidgetPtr {
} }
} }
impl Default for WidgetPtr { impl Default for Wrapper {
fn default() -> Self { fn default() -> Self {
Self::empty() Self::empty()
} }
+87 -9
View File
@@ -20,6 +20,85 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
assert_corners!(h, right, (100, 0), (400, 200)); assert_corners!(h, right, (100, 0), (400, 200));
} }
/// A span offers each child the room left after the one before, because a
/// text has to wrap at the width actually there, but reads what the child
/// reports as a fraction of the whole row. So two children asking for half
/// each take the whole row between them, however much of it was left when
/// each was asked, and a third overflows.
#[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported and drawn there
// once more; half of that final box is what its own child takes.
assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (300, 100));
assert_corners!(h, tail, (400, 0), (500, 100));
}
/// The same fraction either way round: after a 100 px child in a 400 px row,
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
/// drew half of what it was offered and reported that. Half the row, not half
/// of the 300 px left of it.
#[test]
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
let mut declaring = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(declaring, declared, (100, 0), (300, 100));
let mut reporting = Harness::new((400, 100));
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(reporting, reported, (100, 0), (300, 100));
}
/// What the fraction a child reports is of and what box it is offered are
/// two different lengths, and only the first is the whole row: a text still
/// wraps at the room actually left after its neighbour, so the same
/// paragraph is taller where less of the row is left for it.
#[test]
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
let paragraph = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer.";
let height_after = |head_width: i32| {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
let region = h.region(&text).unwrap();
(region.bot_right.y - region.top_left.y).to_f32()
};
let (crowded, whole_row) = (height_after(300), height_after(0));
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// The same reading through a pad: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two.
#[test]
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
// Ruled to the window: a root reporting a fraction of it is otherwise
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
}
#[test] #[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() { fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -225,21 +304,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
h.set_root((bar, buried).span(Dir::RIGHT)); h.set_root((bar, buried).span(Dir::RIGHT));
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1, "only the root region"); assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
h.rsc.widgets_mut().set_region_node(buried, true); h.rsc.widgets_mut().set_region_node(buried, true);
h.frame(); h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!( assert_eq!(
h.render.moves.depth(move_idx), h.render.moves.depth(move_idx),
2, 1,
"the opted-in widget's region and the root region" "the opted-in widget's region alone"
); );
h.rsc.widgets_mut().set_region_node(buried, false); h.rsc.widgets_mut().set_region_node(buried, false);
h.frame(); h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move; let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1); assert_eq!(h.render.moves.depth(move_idx), 0);
} }
/// A span that sizes from its children passes their `leftover` weight up /// A span that sizes from its children passes their `leftover` weight up
@@ -341,16 +420,15 @@ fn a_row_of_equal_shares_fills_it_exactly() {
} }
} }
/// Where the shader puts an edge: the two parts of a scalar are floored /// Where the shader puts an edge: the fraction resolved against the window
/// apart, so a fraction and a pixel offset snap independently, and each is /// plus the pixel offset, taken to the boundary it composes to within half
/// taken to the boundary it composes to within half a step of. Kept in step /// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
/// with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region); let region = h.render.moves.resolve(active.parent_move, active.region);
let dim = h.size().axis(axis); let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor(); let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32()); let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis); let span = region.axis(axis);
(edge(span.start), edge(span.end)) (edge(span.start), edge(span.end))
} }
+124
View File
@@ -613,3 +613,127 @@ fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
assert_ne!(layer(front.id()), layer(background.id())); assert_ne!(layer(front.id()), layer(background.id()));
assert_eq!(draws.get(), 1); assert_eq!(draws.get(), 1);
} }
/// A widget's own mask is not the one it inherited, and a redraw of it
/// inherits the second: handing back the first is handing it its own mask to
/// set a second time, which `set_mask` asserts against.
#[test]
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = inner.masked().add(&mut h.rsc);
let other = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((other, masked).span(Dir::RIGHT));
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_corners!(h, inner, (100, 0), (400, 200));
}
/// The two spans a subtree changes hands between, and the branch that is not
/// in the tree yet -- kept alive by the test until it is.
struct Handover {
leaf: WidgetId,
first: WeakWidget<Span>,
second: WeakWidget<Span>,
root: WeakWidget<Span>,
spare: StrongWidget,
}
/// A subtree that changes hands while its box does not move, so nothing about
/// reusing its drawing says it changed parents. `deeper` puts a span between
/// the root and `second`, so it changes depth by changing hands as well.
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
let leaf = rect(Color::RED).add(&mut h.rsc);
let sized = leaf.width(width).add(&mut h.rsc);
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
let first = Span {
children: match moved {
true => Vec::new(),
false => vec![holder.add_strong(&mut h.rsc)],
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let second = Span {
children: match moved {
true => vec![holder.add_strong(&mut h.rsc)],
false => Vec::new(),
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let branch = match deeper {
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
false => second.add_strong(&mut h.rsc),
};
let (in_tree, spare) = match moved {
true => (branch, first.add_strong(&mut h.rsc)),
false => (first.add_strong(&mut h.rsc), branch),
};
let root = Span {
children: vec![in_tree],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
Handover {
leaf: sized.id(),
first,
second,
root,
spare,
}
}
/// Moves the subtree and swaps the branch it sits in for the one it left.
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
let holder = h.rsc[tree.first].children.remove(0);
h.rsc[tree.second].children.push(holder);
h.rsc[tree.root].children.clear();
h.rsc[tree.root].children.push(tree.spare);
h.frame();
tree.leaf
}
#[test]
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, false, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, false, 40.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it left still listed it and undrew it"
);
}
#[test]
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, true, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
// After it has changed hands, so what has to reach the new parent is a
// change made under the subtree it now holds.
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
warm.frame();
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, true, 90.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it moved to is the one the change has to reach"
);
}
+170 -10
View File
@@ -3,13 +3,16 @@
//! frame that had not settled: a wrapping text shaped at a width it was //! frame that had not settled: a wrapping text shaped at a width it was
//! measured in rather than the one it was given. The rest are a widget //! measured in rather than the one it was given. The rest are a widget
//! measured again in a box its own answer had decided, where the old answer //! measured again in a box its own answer had decided, where the old answer
//! is a fixed point whatever the content now says. The last two are neither: //! is a fixed point whatever the content now says. The last three are
//! one box length, composed two ways, landing either side of the boundary //! neither: one box length, composed two ways, landing either side of the
//! that decided whether a child was drawn at all, and one box as long as the //! boundary that decided whether a child was drawn at all, and two boxes
//! box a widget was offered but somewhere else. //! reached through a region node's own entry rather than through the offer
//! that node was given. The last is a wrapping text handed back the width
//! it measured, rounded to a step below the line it measured there.
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch;
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is /// the tree changes -- every widget is marked for redraw and the frame is
@@ -428,12 +431,12 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()] vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
} }
/// A local redraw asks a dirty widget in the box its parent asked it in, and /// A local redraw asks a dirty widget in the box its parent gave it, and only
/// then again in the box its parent chose from that answer. Skipping the /// where that box is as long as the one it was offered; anything else is a
/// second ask because the two boxes are the same *length* left this inner /// question its parent has to ask. This inner scroll's offer is the outer
/// scroll, which owns a region node, drawn at its offer. The offer is the /// scroll's whole viewport and the box it was given is 24px shorter -- the
/// outer scroll's whole viewport and the final box is 24px above it -- the /// height of the sized child the outer scroll snaps to the end of -- so what
/// height of the sized child the outer scroll snaps to the end of -- so the /// it must not do is settle itself. It was drawn at its offer once, and the
/// inner scroll and its text stayed 24px too low. /// inner scroll and its text stayed 24px too low.
#[test] #[test]
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
@@ -454,3 +457,160 @@ fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
} }
assert!(wrong.is_empty(), "{}", wrong.join("\n")); assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
/// to. The pad owns a movable region and is the scroll's content, so the box
/// the scroll places it in is as long as that content while the box it was
/// offered is the viewport -- and with no padding to tell those two apart,
/// the span inside it looked like it was still at its offer. So everything
/// under the pad was asked again in the *placed* box, the offer resolving
/// against the node's own entry, which holds that box: the texts kept the
/// widths they had, the content stayed the length those widths make, and the
/// old answer confirmed itself. What the branch adds is a tree that differs
/// rather than a box that moved, since a probe measured at the wrong width
/// takes the other side.
fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
let probe = rect(Color::RED).add(&mut h.rsc);
let wide = rect(Color::GREEN).add(&mut h.rsc);
let narrow = rect(Color::BLUE).add(&mut h.rsc);
let branch = Branch {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold: 213.0,
}
.add(&mut h.rsc);
let wrapped = wtext(
"Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.",
)
.size(16)
.wrap(true)
.add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let row = |h: &mut Harness, mut children: Vec<StrongWidget>| {
if swapped {
children.rotate_left(1);
}
Span {
children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc)
};
let texts: Vec<StrongWidget> =
vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
let inner = row(h, texts);
let pair: Vec<StrongWidget> = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
let outer = row(h, pair);
let pad = Pad {
padding: Padding::ZERO,
inner: outer.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(pad.id(), true);
let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.set_root(root);
(
vec![
probe.id(),
wide.id(),
narrow.id(),
branch.id(),
wrapped.id(),
plain.id(),
inner.id(),
outer.id(),
pad.id(),
root.id(),
],
[outer, inner],
)
}
#[test]
fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
let mut warm = Harness::new((900, 1200));
let (ids, spans) = plant_under_a_node(&mut warm, false);
warm.frame();
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
box leaves room for, so a paragraph's height is an answer and not a setting.";
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
/// the span takes its width from the longest line of the texts in it, and
/// the text below the span is then wrapped at that width -- so a width the
/// shaper measured comes back to it as the box to break in.
fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget<Span>, WidgetId) {
let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc);
let mut inner = Span::empty(Dir::UP);
inner.children = match swapped {
true => swapped_in(h),
false => vec![first],
};
let inner = inner.height(142).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let stack = Stack {
children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::DOWN).width(195));
(inner, text.id())
}
/// What the span holds once its children have been swapped, which is what
/// the warm tree is changed to and what the cold one is grown with.
fn swapped_in(h: &mut Harness) -> Vec<StrongWidget> {
let paragraph = |h: &mut Harness| -> StrongWidget {
wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc)
};
vec![
paragraph(h),
rect(Color::YELLOW).add_strong(&mut h.rsc),
paragraph(h),
]
}
/// A text handed back the width it measured breaks there the way it broke
/// when it measured it. The width the shaper answers is not on the grid, and
/// a report rounded to the nearest step is under the longest line half the
/// time: a warm tree then keeps a break made in a wider box while a cold one
/// makes a narrower break in the same box, and the paragraph gains a line.
#[test]
fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
let mut warm = Harness::new((900, 1200));
let (inner, text) = plant_a_measured_width(&mut warm, false);
warm.frame();
warm.rsc[inner].children = swapped_in(&mut warm);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (_, cold_text) = plant_a_measured_width(&mut cold, true);
cold.frame();
assert_eq!(warm.region(&text), cold.region(&cold_text));
}
+13 -5
View File
@@ -42,11 +42,19 @@ const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0); const INNER: (f32, f32) = (640.0, 900.0);
const STILL: (f32, f32) = (900.0, 1200.0); const STILL: (f32, f32) = (900.0, 1200.0);
/// The same box, to a step of the grid per level of nesting between the two /// The same box, to two steps of the grid between the two ways of reaching
/// ways of reaching it. A move, a repaint and a row of shares land on the /// it. A move, a repaint, a row of shares and every length in pixels land on
/// same number; what is left is a box centred in a fraction of its parent /// the same number. What needs the slack is a position: a box centred in a
/// against the same box centred in its own pixels. A step is a thousandth of /// fraction of its parent against the same box centred in its own pixels,
/// a pixel, where this was a twentieth of one before any of it was on a grid. /// and a box re-expressed as a fraction of a parent that changed length.
/// A step is a thousandth of a pixel, where this was a twentieth of one
/// before any of it was on a grid.
///
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
/// stopped being composed: it passes the 100-seed oracle and fails the
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
/// what is left here is the resize path's own rounding rather than a length
/// reached two ways.
const AGREE_STEPS: i32 = 2; const AGREE_STEPS: i32 = 2;
/// A way of changing what a span holds. Each is a shape worth its own case: /// A way of changing what a span holds. Each is a shape worth its own case: