From d194d734393220689f3e1a59a6db080cd9960e9f Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 4 Sep 2026 21:59:38 -0400 Subject: [PATCH] LAYOUT.md: Iris's single-draw preference, recorded before design Co-Authored-By: Claude Fable 5.1 --- LAYOUT.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 LAYOUT.md diff --git a/LAYOUT.md b/LAYOUT.md new file mode 100644 index 0000000..86a4e8b --- /dev/null +++ b/LAYOUT.md @@ -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).