Make Iris layout dependencies explicit
This commit is contained in:
1 parent
e5fee03da8
commit
4bc23172fd
28 files changed
+679
-227
No files matched your search
+51
-39
@@ -1,4 +1,4 @@
|
||||
# iris: one `draw` that reports a size
|
||||
# iris: one `draw` that records a size
|
||||
|
||||
Iris, 2026-09-04:
|
||||
|
||||
@@ -8,9 +8,13 @@ Iris, 2026-09-04:
|
||||
> done after as well. This should be done efficiently like everything else
|
||||
> tries to do right now.
|
||||
|
||||
**Implemented 2026-09-04.** Every widget was migrated in one change; none
|
||||
kept `desired_width`/`desired_height`. What is kept below is the design as
|
||||
it stands, the five corrections implementation forced (read those before
|
||||
**Implemented 2026-09-04; size dependencies made explicit 2026-09-09.**
|
||||
Every widget was migrated in one change; none kept
|
||||
`desired_width`/`desired_height`. `draw` no longer returns its size directly:
|
||||
it records it once on its `Painter`, and a parent that reads a child draw's
|
||||
`DrawResult::size()` records the retained dependency between them. What is
|
||||
kept below is the design as it stands, the corrections implementation forced
|
||||
(read those before
|
||||
touching `Aligned`, `Sized`, `MaxSize`, `Scroll` or the move-slot lifecycle
|
||||
in `render_state.rs` -- each is a real bug the first draft would have
|
||||
reproduced), and the two later additions that build on it. The
|
||||
@@ -25,7 +29,7 @@ having been carried out.
|
||||
|
||||
```rust
|
||||
pub trait Widget: Any {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||
fn draw(&mut self, painter: &mut Painter);
|
||||
|
||||
fn size_hint(&self, axis: Axis) -> Option<Len> { None }
|
||||
|
||||
@@ -35,6 +39,12 @@ pub trait Widget: Any {
|
||||
}
|
||||
```
|
||||
|
||||
Every implementation calls `painter.set_size(size)` exactly once. A child
|
||||
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
|
||||
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.
|
||||
|
||||
No `available` parameter: `Painter` already carries the region the parent
|
||||
handed down (`Painter::region()`, `core/src/ui/painter.rs:137`) and already
|
||||
exposes the pixel-resolved form (`px_size()`, `:156`) and the output surface
|
||||
@@ -281,22 +291,25 @@ always a leaf: `Rect`, `Image`, a fixed glyph). A widget that returns
|
||||
`false` (the default) is redrawn in full whenever `available` changes,
|
||||
which is correct always, just not free.
|
||||
|
||||
**Size propagation goes both ways in the same frame.** A resized child first
|
||||
walks upward through exactly the ancestors whose cached size changes. That
|
||||
measurement pass gives each parent the new size but necessarily drew the
|
||||
branch in its old boxes. As the recursion returns, `redraw_and_settle` revisits
|
||||
those changed widgets from the outside in, after their parents have assigned
|
||||
the final boxes. Otherwise a newly appended child can retain the provisional
|
||||
(even inverted) region it was measured in until another update happens. The
|
||||
downward work is confined to the branch that changed; unchanged descendants
|
||||
still take `draw_inner`'s retained fast path. Each downward visit is exactly
|
||||
one redraw, not another upward propagation: a wrapping child can have no fixed
|
||||
point when an ancestor shrink-wraps it (a trailing space alternated between one
|
||||
line in the offered width and two lines in its reported natural width). Feeding
|
||||
that answer back into the same branch recursively overflowed Android's native
|
||||
UI-thread stack before Rust could report a panic. A container that intends a
|
||||
wrapping child to occupy its width declares that constraint explicitly; the
|
||||
message composer does so on both sides of its vertical `ScrollArea`.
|
||||
**Size invalidation travels upward before drawing; drawing itself travels only
|
||||
downward.** Every active widget retains the direct children whose size it read
|
||||
through `DrawResult::size()` or `Painter::known_len`. Before a frame draws,
|
||||
`redraw_updates` follows only those dependency edges from each dirty child and
|
||||
marks the affected ancestors dirty. It then selects the highest dirty roots and
|
||||
draws them top-down. 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
|
||||
size. Otherwise propagation is deliberately conservative: the child may have
|
||||
changed size, and only its dependent ancestors can assign the final boxes.
|
||||
Unchanged descendants still take `draw_inner`'s retained skip-or-move path.
|
||||
For a stacking container, retained child lengths are cached per axis: a child's
|
||||
width remains reusable while the parent changes width, and its height remains
|
||||
reusable while the parent changes height. A change on the orthogonal axis does
|
||||
invalidate it in both directions. This is a generic constraint rule, not a
|
||||
text exception; wrapped text is merely the common example of height depending
|
||||
on width.
|
||||
|
||||
### 4. Wrapped text, and "needs child height before choosing width"
|
||||
|
||||
@@ -343,17 +356,15 @@ buffer. That check is kept exactly as it is; it is the caching mechanism,
|
||||
and it already operates at (id, region) granularity, which subsumes "(id,
|
||||
available size)" once size *is* what a region change means.
|
||||
|
||||
What is added: `ActiveData` gains `pub size: Size` — the value `draw`
|
||||
returned, stored the moment it is (`draw_inner`, alongside building the
|
||||
`ActiveData` struct at `:134-143`). This is what a parent placing this
|
||||
widget for a second frame without redrawing it (because nothing changed)
|
||||
reads instead of recomputing — it replaces `Cache.size`'s role of "answer a
|
||||
size question without a full draw" with "read the size of the last actual
|
||||
draw," which is always available because `draw_inner`'s skip path is only
|
||||
reachable once the widget has been drawn at least once. `Cache::remove`/
|
||||
`Cache::clear` (`cache.rs:9-17`) are deleted with the type; `ActiveData`
|
||||
already has an equivalent lifecycle (removed in `remove`/`remove_rec`,
|
||||
`render_state.rs:171-198`, freed with the widget).
|
||||
`ActiveData::size` stores the value the widget recorded with
|
||||
`Painter::set_size`. This is what a parent placing the widget for a second
|
||||
frame without redrawing it reads instead of recomputing — it replaces
|
||||
`Cache.size`'s role of "answer a size question without a full draw" with
|
||||
"read the size of the last actual draw." `ActiveData::size_dependencies`
|
||||
stores the direct children whose `DrawResult::size()` or known length the
|
||||
widget observed during that same draw; the next draw replaces the list, so a
|
||||
dependency disappears as soon as the widget stops reading it. Both fields
|
||||
have `ActiveData`'s existing lifecycle through `remove`/`remove_rec`.
|
||||
|
||||
### 6. Before / after
|
||||
|
||||
@@ -374,10 +385,10 @@ impl Widget for Rect {
|
||||
```rust
|
||||
// after
|
||||
impl Widget for Rect {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.primitive(RectPrimitive { color: self.color, radius: self.radius,
|
||||
thickness: self.thickness, inner_radius: self.inner_radius });
|
||||
Size::REST // fills whatever it was given -- used == available
|
||||
painter.set_size(Size::REST); // fills whatever it was given
|
||||
}
|
||||
fn is_size_independent(&self) -> bool { true } // content never depends on region size
|
||||
}
|
||||
@@ -408,12 +419,12 @@ impl Widget for Aligned {
|
||||
```rust
|
||||
// after
|
||||
impl Widget for Aligned {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let full = painter.region();
|
||||
// Draw once at the full region to learn the child's real size --
|
||||
// this placement is provisional and corrected below without a
|
||||
// second draw.
|
||||
let used = painter.widget_within(&self.inner, full);
|
||||
let used = painter.widget_within(&self.inner, full).size();
|
||||
let region = match self.align.tuple() {
|
||||
(Some(x), Some(y)) => used.to_uivec2().align(RegionAlign { x, y }).within(&full),
|
||||
(Some(x), None) => used.x.apply_rest().align(x).within(&full),
|
||||
@@ -421,14 +432,15 @@ impl Widget for Aligned {
|
||||
(None, None) => full,
|
||||
};
|
||||
painter.place(&self.inner, region);
|
||||
used
|
||||
painter.set_size(used);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Painter::widget_within`/`widget`/`widget_at` (`painter.rs:55-76`) change
|
||||
return type from `()` to `Size`, carrying the child's `draw` result back —
|
||||
the only signature change needed to let a parent see what its child used.
|
||||
return type from `()` to `DrawResult`. Calling `.size()` reads the size the
|
||||
child recorded on its painter and records the parent's dependency on that
|
||||
answer; leaving it unread records no dependency.
|
||||
`Painter::place` moves an already-drawn child when its used area fits the
|
||||
target box, and redraws it when the target changes its size. `SizeCtx` and
|
||||
`Painter::size_ctx`/`size`/`len_axis` (`painter.rs:141-150,
|
||||
|
||||
Reference in new issue
Block a user