Files
ai-app/LAYOUT.md
T

5.2 KiB

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).