Compare commits

..
Author SHA1 Message Date
iris-ai a2cb4f05da WIP: a stack does not take its sizing child's fraction twice
Every child gets the whole of the stack's box rather than `box_of(size)`,
and `widget_at` does not resolve a rule into a box already chosen from it.
Fixes half a row becoming a quarter, which no oracle can see because warm
and cold shrink alike. Pinned by
`a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice`.

Not landed. Seed 1091 at depth 4, `shuffle-swap-for-three`, disagrees by
three steps of the grid -- warm 1053.9971 against cold 1054 -- where the
oracle tolerates two. Not a structural divergence: `box_of` was also making
a child's placement exact, by handing it a box of exactly the length it
asked for, and giving it the whole box instead puts a rounding back at each
nesting level. Two nested stacks is three steps. Widening AGREE_STEPS is
not the answer; finding the composition that went from exact to rounded is.

Everything else is green: suite, shrinker at 400 seeds of depth 5, oracle at
1000 seeds of depth 6, and the rest of 2000 seeds at depth 4.

Also carries examples/text.rs's top panel taking the full width.
2026-09-17 04:50:00 -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
24 changed files with 933 additions and 470 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;
+27 -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,
} }
+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
+128 -71
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,34 +139,49 @@ 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());
// A rule this box was already chosen from is not resolved into it a
// second time. The box is that rule's length already, so resolving
// it again takes the fraction twice -- a widget declaring half of a
// stack, in the stack its own answer made half a row, is a quarter
// of the row. Pixels survive it, being the same length wherever they
// are taken from, which is why only a share ever shrank.
let resolve = AXES.map(|axis| match decided[axis as usize] {
true => None,
false => declared[axis as usize],
});
// Composing `FULL` through a box is not quite the identity in f32, // 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 resolve.iter().any(Option::is_some) {
true => declared_box(region, declared, align), true => declared_box(region, resolve, align),
false => region, false => region,
}; };
let within = match local == UiRegion::FULL { let within = match local == UiRegion::FULL {
@@ -176,11 +198,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 +225,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 +245,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 +279,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 +312,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 +326,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());
@@ -378,29 +403,21 @@ impl<'a> Painter<'a> {
.is_some() .is_some()
} }
/// The part of this widget's box that something of `size` takes, at the
/// 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.
pub fn box_of(&self, size: Size) -> UiRegion {
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
}
/// This widget's box in pixels. Reading it makes the drawing one that /// 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 +432,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 +523,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 +561,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;
} }
+244 -245
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,8 +42,6 @@ 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.
resized: bool, resized: bool,
@@ -50,6 +55,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 +71,46 @@ 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
/// 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.
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
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;
} }
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],
} }
} }
@@ -146,8 +150,8 @@ impl UiRenderState {
// length, found the way every other box change is found. Before // length, found the way every other box change is found. Before
// anything dirty settles, so that whatever a new output draws // anything dirty settles, so that whatever a new output draws
// again is drawn once, in the box it will have. // 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 region = Self::root_region(root.id(), rsc.widgets());
let info = self.root_info(region);
let answer = self.draw_inner(root.id(), region, info, None, rsc); let answer = self.draw_inner(root.id(), region, info, None, rsc);
self.active.get_mut(&root.id()).unwrap().answer = answer; self.active.get_mut(&root.id()).unwrap().answer = answer;
} }
@@ -163,11 +167,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 +193,87 @@ 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 has is an answer while something it measured
let replace_answer = self.answer_invalid.remove(&id) // is dirty: settling that changes what it would report, and a widget
|| (self.replace_answers // settled inside its parent's draw tells nobody -- the comparison
&& (rsc.widgets().needs_redraw.contains(&id) // that marks a reader is in `redraw`, which is not what asked here.
|| self.dirty_size_under(id, rsc.widgets()))); // Both retained routes are an answer, so the question is asked once
let retained = match replace_answer { // rather than by each of them.
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 || 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;
active.depth = info.depth; active.depth = info.depth;
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 +294,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 +338,7 @@ impl UiRenderState {
state: _, state: _,
rsc: _, rsc: _,
region: _, region: _,
px: _,
mask, mask,
textures, textures,
primitives, primitives,
@@ -401,9 +408,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 +423,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 +441,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 +474,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.
@@ -498,17 +498,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,15 +509,16 @@ impl UiRenderState {
{ {
return None; return None;
} }
let px = self.px_of(info.parent_move, region);
let (size, holds) = active.answer; let (size, holds) = active.answer;
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds)) (holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds))
} }
/// Whether anything whose size this widget's own size was read from is /// Whether anything whose size this widget's own size was read from is
/// dirty. Not needed for the answer to come right -- a changed size /// dirty, which makes what it would answer not yet known. It also keeps
/// reaches its reader in any order -- but a reader that asks first /// a reader that asks first from laying out twice, which is all it was
/// lays out once rather than twice. /// here for while a changed size was thought to reach its reader in any
/// order; it does not, where the change settles inside the reader's own
/// draw.
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool { fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
self.active.get(&id).is_some_and(|active| { self.active.get(&id).is_some_and(|active| {
active.size_deps.iter().any(|child| { active.size_deps.iter().any(|child| {
@@ -534,31 +527,39 @@ impl UiRenderState {
}) })
} }
/// The first box a widget was asked about, re-expressed in the coordinate /// The pixel lengths of the box a widget was given and of the box it was
/// space its drawing uses. Keeping the relative box and composing it /// first asked about, which is what a local redraw needs to ask the
/// again avoids rebuilding a shifted box from rounded pixel lengths. /// question its parent asked.
fn offered_region(&self, id: WidgetId) -> UiRegion { ///
/// Both are threaded down from the window a length of a box at a time,
/// and this takes the same steps back up: a widget's box is a length of
/// the box its parent drew in, and its offer a length of the box its
/// 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) {
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)
} }
} None => (self.output_size, self.output_size),
_ => UiRegion::FULL,
};
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 +616,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,23 +628,21 @@ 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);
} }
} }
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.given_len = info.given_len;
active.offer_len = info.offer_len;
active.depth = info.depth; active.depth = info.depth;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
@@ -676,10 +675,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 +690,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 +776,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 +791,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 +811,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,19 +831,34 @@ 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 {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -909,20 +926,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 +968,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)
}; };
let offered_px = self.px_of(active.parent_move, asked_in); #[cfg(feature = "layout-diagnostics")]
// Whole boxes rather than lengths: an offer as long as the final box diag::bump(Counter::LocalRedraws);
// but somewhere else is a different box, and a region node drawing at let old = self.remove(id, false, rsc);
// its offer writes the box it drew in into its own entry. self.draw_inner(id, region, info, old, rsc);
let at_offer = same_pixel_region( return true;
self.px_region(active.parent_move, region), };
self.px_region(active.parent_move, asked_in), let (given_px, offered_px) = self.asked_px(id);
); // Asked again in the box its parent gave it, which is the question
let parent_must_place = // its parent asked only while that box is as long as the offer. Any
active.parent.is_some() && (!region_node || active.align_override) && !at_offer; // other box is a different question, so the parent asks it, with the
// An independently positioned region node can redraw at its offer // mark left on. Lengths and not whole boxes: what a drawing depends
// and move its slot to its own placement. Every other widget needs // on is its lengths, so the same lengths elsewhere is one question.
// its parent to reproduce a different final position. if given_px != offered_px {
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 +1048,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| {
+8 -3
View File
@@ -26,12 +26,17 @@ impl DefaultAppState for State {
.wrap(true) .wrap(true)
.text_align(Align::LEFT) .text_align(Align::LEFT)
.pad(16) .pad(16)
.width(rel(1.0))
.background(panel()); .background(panel());
// Each one takes the whole width, because `text_align` puts the
// 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)
+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
+17 -3
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;
@@ -119,7 +125,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
+28 -17
View File
@@ -13,31 +13,42 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
}; };
// Whichever child sizes the stack decides the box every child gets. // Every child gets the whole of this stack's box, the sizing one
// The stack reports that size, so a child given a longer box would // included, and the stack is then handed a box of the length that
// draw outside what the stack says it occupies. // child asked for. Not the part of the box that length takes: the
// stack's own box becomes that length, and taking the fraction of it
// again is the fraction twice -- a child asking for half of a stack
// that is already half a row would have a quarter of the row.
//
// It cannot be told apart by asking whether this box is the answer
// yet, either. A drawing has to be a function of the box alone, since
// moving the stack into the box it asked for reuses the drawing by
// scaling it, and a drawing made a fraction of one box is right in
// any other. So: fractions of this box throughout, and the move is
// the whole of the difference.
let region = UiRegion::FULL;
// Whichever child sizes the stack is asked here and not again below,
// on the layer it ends up on: a retained drawing belongs to the layer
// it was made on, so measuring it anywhere else costs a second
// drawing of it. Its box is its own answer, so the answer is not
// placed inside it again.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained
// drawing belongs to the layer it was made on.
Some((i, child)) => { Some((i, child)) => {
painter.child_layer_at(i); painter.child_layer_at(i);
painter.widget(child).size() painter
.widget_at(child, region, region.size(), [true; 2])
.size()
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
}; };
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i); painter.child_layer_at(i);
// The sizing child placed its own content in the box its answer // A box that owes nothing to this child's own answer: where it
// decided, and this box was derived from that answer, so applying // sits in one bigger than itself is its own business.
// its alignment again here would place it twice. Every other painter.widget_within(child, region);
// 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.
match sizing == Some(i) {
true => painter.widget_aligned(child, region, RegionAlign::NEAR),
false => painter.widget_within(child, region),
};
} }
size size
} }
+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()
} }
+112 -9
View File
@@ -20,6 +20,110 @@ 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}");
}
/// A stack takes its size from one child and gives every child that size, so
/// a child asking for half of it is asking for half of what it is itself the
/// size of. Once the stack has been placed at the length it reported that
/// length is the box, and taking the fraction of it again takes it twice:
/// half a row became a quarter, and a further stack around it a further half.
/// Nothing pinned it because a pixel is the same length wherever it is taken
/// from, so only a share ever shrank -- and warm and cold shrink alike, so no
/// oracle saw it either.
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
/// 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 +329,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 +445,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))
} }
+15
View File
@@ -613,3 +613,18 @@ 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));
}
+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: