LAYOUT.md: Iris's single-draw preference, recorded before design

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-04 21:59:38 -04:00
1 parent 4400966928
commit d194d73439
1 file changed
+96
+96
View File
@@ -0,0 +1,96 @@
# iris: one `draw` that reports a size
Preference stated by Iris, 2026-09-04, on the `rustify` branch. Recorded before
any design or code so that it survives a cleared session. **Status: a
requirement with a design to be written below it. Nothing implemented.**
## What Iris asked for
> I don't like that widgets need both a draw and size functions. I'd much
> rather them have a single draw that reports a size, and if it needs to be
> moved then that can be done after the fact efficiently, or resized just
> done after as well. This should be done efficiently like everything else
> tries to do right now.
She added, a few minutes later: "single draw is not a requirement. It
just seems more efficient from what I've heard. Feel free to override any
decision I've made if you can find a genuinely better & still clean
alternative." So the single-draw model is the default to design against,
and the design below may reject it, but only with a written comparison
showing the alternative does less work per frame and is no harder to use.
Standing constraints from RUST.md still apply: no DSL, plain Rust, do as
little processing as possible per frame, but the model must cover every
layout need a real app has (the transcript's virtualised list, wrapped
text whose height depends on width, rows and columns that size to their
children, overlays, masks).
## What exists today
`Widget` (`iris/core/src/widget/mod.rs`) has three methods: `draw(&mut
self, &mut Painter)`, `desired_width(&mut self, &mut SizeCtx) -> Len` and
`desired_height`. A parent asks `SizeCtx::width/height` for a child, which
is memoised per widget id and axis in `Cache.size` keyed on the outer
size, then places the child with `Painter::widget_within(region)`. So a
child is visited twice (sized, then drawn), every widget implements sizing
twice (one per axis), and a widget whose size depends on what it draws
(wrapped text, a laid-out paragraph) does the layout in the size pass and
again in the draw pass unless it caches by hand.
Primitives are already positioned by `UiRegion` values whose scalars have
a `rel` and an `abs` part, resolved against the window in the vertex
shader (`core/src/render/shader.wgsl`), and `Primitives::region_mut`
exists to rewrite one instance's region in place. That is the mechanism a
"move after the fact" can build on.
## What the design must answer
1. **Parent-before-child ordering.** A row has to know each child's width
to place the next one, but under "one draw" the child's size only
exists after it has drawn. The answer is meant to be: the child draws
at a provisional origin, reports its size, and the parent *moves* it.
The move must be O(1) per moved subtree, not O(primitives in the
subtree). One way: every instance carries an index into a small
per-widget offset buffer, so moving a widget writes one entry and the
vertex shader adds it. Other ways may be better; the design should say
what was considered.
2. **Move vs resize are different costs and must be kept apart.** A move
never re-runs `draw`. A resize re-runs `draw` for exactly the widgets
whose size input changed, and a widget whose output does not depend on
its size (an icon, a fixed rect) must be able to say so and be skipped.
3. **Size-dependent content.** Wrapped text is the hard case: its height
is a function of its width. A single `draw` receives the available
size (what `SizeCtx.outer` is today) and reports what it used, so the
two-pass "measure then draw" collapses into one for the common case.
The design must say what happens when a parent wants the child's
height *before* deciding the width it will offer (rare; say whether it
is supported, or is done by drawing twice as an explicit, opt-in cost).
4. **Caching.** Today's `Cache.size` memoises by (id, axis, outer). The
replacement should memoise the whole draw result by (id, available
size) so that an unchanged subtree costs nothing on the next frame,
which is what makes a virtualised list cheap.
5. **Everything currently written against `desired_width`/`desired_height`
moves over in one change**, per the code rules: two names for one
concept is not an intermediate state to leave behind. The widgets are
in `iris/src/widget/` (`ptr`, `mask`, `image`, `rect`, `trait_fns`, and
whatever else is there when the change is made).
## Order relative to the texture work
TEXTURES.md's redesign touches the render core (shader, `GpuTextures`,
`Primitives`, `Painter`'s texture calls). This change touches the widget
trait, `SizeCtx`, `Cache`, `Painter`'s widget calls, and any offset
mechanism the vertex shader needs. They overlap in `Painter` and the
shader, so they are done **in sequence, textures first**, and the layout
design here is written (not implemented) while the texture work is in
progress, then implemented on top of it.
## Design (to be written by the agent that takes this)
Not yet written. When it is, it replaces this heading and records: the
new `Widget` trait, how a move is expressed and what it costs, how a
resize is scoped, how the result is cached and invalidated, what was
rejected and why, and the pass conditions the implementation is checked
against (the existing examples under `iris/examples` still render the
same, and the per-frame work for an unchanged tree is measured, not
assumed).