97 lines
5.2 KiB
Markdown
97 lines
5.2 KiB
Markdown
# 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).
|