Bring LAYOUT.md §2 and §3 to what shipped
§2 described `Painter::place`, `move_offsets`, `resolved_region` and `set_instance`; three of those four names no longer exist. What shipped is the opt-in region node, a box rather than a translation, and remapping for everything that did not opt in. §3 described `Widget::on_resize` and its `Scale`/`Redraw`/`Translate` answers, which are gone: the `Holds` interval says the same thing per drawing rather than per widget type, and says how far. The 0.05 px comparison it quoted is equality on the grid now. The trait in §1 has two methods rather than three, and the line numbers it cited have all moved; they are dropped rather than corrected, since the names are enough to find. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
617331f913
commit
2d860587a4
1 file changed
+82
-89
+82
-89
@@ -4,9 +4,10 @@ A widget draws once and records its size on the `Painter`. Reading a child
|
|||||||
`DrawResult::size()` records a retained size dependency; drawing the child
|
`DrawResult::size()` records a retained size dependency; drawing the child
|
||||||
without reading that result does not make the parent's size depend on it.
|
without reading that result does not make the parent's size depend on it.
|
||||||
|
|
||||||
§1 and §2 have landed in Iris (#16 and #18) and the notes below have been
|
§1, §2 and §3 have landed in Iris (#16 and #18) and the notes below have been
|
||||||
brought to what shipped rather than what was proposed; §3 to §6 describe the
|
brought to what shipped rather than what was proposed; §4 to §6 describe the
|
||||||
same design as it stands. `docs/HANDOFF.md` has the invariants
|
same design as it stands, and name types that have since been replaced where
|
||||||
|
they were written before it. `docs/HANDOFF.md` has the invariants
|
||||||
the code now rests on and what is still to do.
|
the code now rests on and what is still to do.
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
@@ -36,11 +37,12 @@ pub trait Widget: Any {
|
|||||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||||
|
|
||||||
fn size_hint(&self, axis: Axis) -> Option<Len> { None }
|
fn size_hint(&self, axis: Axis) -> Option<Len> { None }
|
||||||
|
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize { OnResize::Redraw }
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Two methods, not three: `on_resize` was proposed here and shipped, and §3
|
||||||
|
below replaced it with the `Holds` interval a widget declares while drawing.
|
||||||
|
|
||||||
A widget returns what it used of the box it was given. A child
|
A widget returns what it used of the box it was given. A child
|
||||||
draw returns a `DrawResult` that keeps the painter borrowed; calling `.size()`
|
draw returns a `DrawResult` that keeps the painter borrowed; calling `.size()`
|
||||||
on that result reads the child's retained size and records that the current
|
on that result reads the child's retained size and records that the current
|
||||||
@@ -48,15 +50,14 @@ widget depends on it. Dropping the result without reading it draws the child
|
|||||||
without making the parent's own size depend on the child's.
|
without making the parent's own size depend on the child's.
|
||||||
|
|
||||||
No `available` parameter: `Painter` already carries the region the parent
|
No `available` parameter: `Painter` already carries the region the parent
|
||||||
handed down (`Painter::region()`, `core/src/ui/painter.rs:137`) and already
|
handed down (`Painter::region()`) and already exposes the pixel-resolved form
|
||||||
exposes the pixel-resolved form (`px_size()`, `:156`) and the output surface
|
(`px_size()`) and the output surface size (`output_size()`). Passing it again
|
||||||
size (`output_size()`, `:152`). Passing it again would be the same value
|
would be the same value under a second name. `desired_width`/`desired_height`
|
||||||
under a second name. `desired_width`/`desired_height` (`core/src/widget/mod.rs:20-21`)
|
and `WidgetAxisFns::desired_len` are deleted outright — not
|
||||||
and `WidgetAxisFns::desired_len` (`:24-35`) are deleted outright — not
|
|
||||||
deprecated, not kept as a fallback — because a widget that implements both
|
deprecated, not kept as a fallback — because a widget that implements both
|
||||||
`draw` and `desired_*` for the same thing is exactly the "two names for one
|
`draw` and `desired_*` for the same thing is exactly the "two names for one
|
||||||
concept" the code rules call out, and it is what today's `Span::desired_ortho`
|
concept" the code rules call out, and it is what today's `Span::desired_ortho`
|
||||||
(`iris/src/widget/position/span.rs:98-152`) already complains about in its
|
(as it was then) already complains about in its
|
||||||
own comment: "this literally copies draw so that the lengths are correctly
|
own comment: "this literally copies draw so that the lengths are correctly
|
||||||
set in the context, which makes this slow and not cool." Folding sizing into
|
set in the context, which makes this slow and not cool." Folding sizing into
|
||||||
`draw` deletes that duplicate simulation, not just moves it.
|
`draw` deletes that duplicate simulation, not just moves it.
|
||||||
@@ -67,97 +68,89 @@ lying hint fails a debug assertion when the widget is drawn.
|
|||||||
|
|
||||||
### 2. O(1) subtree movement
|
### 2. O(1) subtree movement
|
||||||
|
|
||||||
Every active widget owns a slot in `UiData::move_offsets`. A slot stores an
|
A widget opts into one independently movable region with `.region_node()`, or
|
||||||
absolute-pixel delta and its parent slot; each primitive instance stores the
|
`Widgets::set_region_node` at runtime; `.scrollable()` sets it once as its
|
||||||
slot of the widget that drew it. The vertex shader walks this bounded chain
|
convenient default. A node holds a whole **box** -- a `UiRegion` in its parent
|
||||||
and adds the accumulated translation. Moving a subtree therefore writes one
|
node's coordinates, `UiRegion::FULL` being the identity -- and each primitive
|
||||||
slot instead of rewriting every descendant primitive.
|
instance names the node it was drawn under. Moving a subtree through a node
|
||||||
|
writes one entry. A widget without the property shares the nearest ancestor's
|
||||||
|
node, and moving it remaps its retained primitive, mask and active regions
|
||||||
|
instead, stopping at any descendant node after rewriting that one entry.
|
||||||
|
|
||||||
The parent chain is required for independently movable nested subtrees, such
|
A box rather than a translation, because a pixel-space offset would scale a
|
||||||
as a swipeable row inside a scrolling list. A flat offset table would require
|
child that has to keep its pixel length; the fraction and the offset in a
|
||||||
rewriting the row whenever an ancestor moved and would restore the very
|
`UiScalar` are what tell the two apart. The parent chain is what makes nested
|
||||||
O(subtree) work this design removes. Chain depth is bounded in both Rust and
|
movable subtrees work -- a swipeable row inside a scrolling list -- and a flat
|
||||||
WGSL.
|
table would rewrite the row whenever an ancestor moved, which is the
|
||||||
|
`O(subtree)` work this removes. `CHAIN_LIMIT` bounds the walk at 64 in both
|
||||||
|
Rust (`core/src/ui/mod.rs`) and WGSL, so a malformed cycle resolves the same
|
||||||
|
way on each side.
|
||||||
|
|
||||||
`Painter::place` draws a child whose box its parent decides and may decide
|
`Moves::resolve` performs the same walk on the CPU for hit testing,
|
||||||
again, and gives that child a slot of its own; `widget` and `widget_within` do
|
accessibility and window-coordinate queries, and the shader's `resolve_move`
|
||||||
not, and share the nearest ancestor's. A slot carries a whole **box**, not a
|
mirrors it. Coordinates cross as whole counts of `1/1024` px and `1/2^24` of
|
||||||
translation: a pixel-space scale and offset would scale a child that has to
|
a box, which the shader decodes from constants the Rust side prepends: the
|
||||||
keep its pixel length, and the `rel`/`abs` pair is what distinguishes the two.
|
grid is stated once. Masks carry their own node and resolve it independently,
|
||||||
(That slots carry translation only was an agent's choice on 2026-09-04, never
|
so a stationary viewport clips content that moves inside it.
|
||||||
asked for, and #18 replaced it.)
|
|
||||||
|
|
||||||
`UiRenderState::resolved_region` performs the same chain walk on the CPU for
|
Nodes follow `ActiveData`'s lifecycle. Removing one retires its entry only
|
||||||
hit-testing, accessibility, and public window-coordinate queries. Masks store
|
after every descendant has migrated, since reusing the index sooner would
|
||||||
the move slot of their owning widget and resolve it independently in the
|
make an old parent look current. Changing the property redraws the subtree
|
||||||
fragment shader, so a stationary viewport can clip moving content.
|
once, to rebuild the coordinate boundary; it belongs to widget identity,
|
||||||
|
which is safe because a widget has one parent.
|
||||||
Slots follow `ActiveData`'s lifecycle. Removing a widget recursively retires
|
|
||||||
its slot only after descendants are gone, and a reused arena slot is reset
|
|
||||||
before new primitives can reference it. `Primitives::set_instance` also
|
|
||||||
cancels a dirty mark when provisional layout restores the original bytes, so
|
|
||||||
CPU-only measurement positions are never uploaded.
|
|
||||||
|
|
||||||
### 3. Resize scope
|
### 3. Resize scope
|
||||||
|
|
||||||
A resize is "the region a widget's parent offers it changes such that the
|
A resize is "the region a widget's parent offers it changes such that the
|
||||||
widget's draw might produce different output" — as opposed to a move, which
|
widget's draw might produce different output" -- as opposed to a move, which
|
||||||
by construction cannot (§2 is scoped to pure translation). Two independent
|
by construction cannot. Two independent narrowings apply, and both are
|
||||||
narrowings apply, and both are real, measured properties of the code as it
|
measured properties of the code rather than new machinery:
|
||||||
stands rather than new machinery:
|
|
||||||
|
|
||||||
**(a) A window resize does not, by itself, require touching most widgets.**
|
**(a) A window resize does not, by itself, require touching most widgets.**
|
||||||
`shader.wgsl` recomputes every primitive's pixel position from `window.dim`
|
The shader recomputes every primitive's position from `window.dim` and the
|
||||||
and the primitive's stored `rel`/`abs` pair every frame, already, on the GPU.
|
primitive's stored fraction and offset every frame, already, on the GPU. A
|
||||||
A widget laid out purely in `rel`/`abs` terms is therefore already correct
|
widget laid out purely in those terms is therefore correct after a resize
|
||||||
after a resize with zero CPU work. Calls to `Painter::px_size` and
|
with no CPU work at all.
|
||||||
`Painter::output_size` mark both concrete-pixel axes; `px_len(axis)` and
|
|
||||||
`output_len(axis)` mark only the axis actually read. Only widgets whose read
|
|
||||||
axes changed by more than 0.05 physical pixels become dirty. The comparison
|
|
||||||
is against each widget's last actual draw, so smaller changes accumulate
|
|
||||||
rather than disappearing event by event.
|
|
||||||
|
|
||||||
All pixel-dependent leaves are marked before layout begins, along with every
|
What decides the rest is `Holds`, one interval of box lengths per axis:
|
||||||
chain of parents that read their sizes. Resize then settles the shallowest
|
*give this widget any box in here and it draws the same thing and reports the
|
||||||
shared readers first, under the new output, so overlapping dependency paths
|
same size*. A widget that never reads its box in pixels holds for every
|
||||||
are drawn once. Ordinary content changes use the opposite order: deepest
|
length. Reading `Painter::px_len(axis)` or `px_size()` narrows the interval
|
||||||
dirty widgets first, with a changed returned size propagated one reader edge
|
to the length read, and `Painter::holds` is how a widget widens it again by
|
||||||
at a time. Re-reporting the current output size is a no-op.
|
saying what its drawing actually depends on -- a greedy line break holds from
|
||||||
|
its longest line up to the width it was made at. A parent holds for whatever
|
||||||
|
keeps every child it asked about or drew inside its own range, each child's
|
||||||
|
interval translated into lengths of the parent's box.
|
||||||
|
|
||||||
**(b) A widget's `available` (its parent's offered region) can change
|
This replaced `Widget::on_resize` and its `Scale`/`Redraw`/`Translate`
|
||||||
without the widget's *content* changing — this is what `Widget::on_resize`
|
answers, which said the same thing per widget type and could not say *how
|
||||||
answers, per axis.** When a container's own layout shifts (a sibling grew or
|
far*. There is no per-widget resize mode now: a widget that reads nothing is
|
||||||
shrank, changing this widget's offered box), a widget that says `Scale` on the
|
never redrawn for a resize, one that reads its width is redrawn when its
|
||||||
axes that changed is not redrawn: everything it drew is a fraction of its own
|
width leaves the interval it declared, and the interval is the whole of the
|
||||||
slot's box, so writing that one box moves and stretches all of it. `Span`,
|
statement. Do not restore `Translate`; a retained subtree that only moves is
|
||||||
`Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`;
|
remapped through the box chain of §2, exactly.
|
||||||
`Scroll` and `MaxSize` read their box in pixels and cannot. `Redraw`, the
|
|
||||||
default, is correct always and free never. `Translate` — an unchanged drawing
|
|
||||||
placed somewhere else in a bigger box — is reserved: nothing reads it until a
|
|
||||||
widget can say where in that box its drawing belongs, which is the alignment
|
|
||||||
work.
|
|
||||||
|
|
||||||
**Size invalidation travels upward before drawing; drawing itself travels only
|
Lengths are whole counts of `1/1024` px, so "the box changed" is equality
|
||||||
downward.** Every active widget retains the direct children whose size it read
|
rather than a tolerance: a change too small to reach the next step is not a
|
||||||
through `DrawResult::size()` or `Painter::known_len`. `redraw_updates` takes
|
change, and one that reaches it is, however little of a pixel it is worth.
|
||||||
one id from the dirty set, follows only those dependency edges upward and marks
|
|
||||||
that path dirty, then redraws its highest already-dirty ancestor. Drawing that
|
|
||||||
ancestor consumes the marks of every dirty descendant it reaches; the loop
|
|
||||||
then takes whatever remains. Drawing never synchronously invalidates or invokes
|
|
||||||
a parent, so there is no layout recursion and no provisional child draw on a
|
|
||||||
different layer.
|
|
||||||
|
|
||||||
An exact `size_hint` stops propagation when both axes still equal the retained
|
**(b) Size invalidation travels upward before drawing; drawing itself travels
|
||||||
size. Otherwise propagation is deliberately conservative: the child may have
|
only downward.** Every active widget retains the direct children whose size it
|
||||||
changed size, and only its dependent ancestors can assign the final boxes.
|
read through `DrawResult::size()` or `Painter::known_len`. `redraw_updates`
|
||||||
Unchanged descendants still take `draw_inner`'s retained skip-or-move path.
|
takes one id from the dirty set, follows only those dependency edges upward
|
||||||
An active widget also retains which offered-box and output axes flowed into
|
and marks that path dirty, then redraws its highest already-dirty ancestor.
|
||||||
the size it reported, directly or through a child size it read. A container
|
Drawing that ancestor consumes the marks of every dirty descendant it
|
||||||
may use that answer for the same prospective box when every observed input is
|
reaches; the loop then takes whatever remains. Drawing never synchronously
|
||||||
still within 0.05 physical pixels; content dirtiness anywhere in its size
|
invalidates or invokes a parent, so there is no layout recursion.
|
||||||
dependency subtree rejects the answer. This is a generic constraint rule, not
|
|
||||||
a text exception. Wrapped text is merely the common example: it reads width,
|
Dirty widgets settle deepest-first, and `dirty_size_under` stops a reader
|
||||||
so changing only height leaves its answer valid.
|
taking a retained answer while something below that answer is still dirty --
|
||||||
|
an optimisation against laying out twice rather than a second validity
|
||||||
|
mechanism. An exact `size_hint` stops propagation when both axes still equal
|
||||||
|
the retained size; otherwise propagation is deliberately conservative, since
|
||||||
|
only a dependent ancestor can assign the final boxes. This is a generic
|
||||||
|
constraint rule, not a text exception. Wrapped text is merely the common
|
||||||
|
example: it reads width, so changing only height leaves its answer valid.
|
||||||
|
|
||||||
### 4. Wrapped text, and "needs child height before choosing width"
|
### 4. Wrapped text, and "needs child height before choosing width"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user