docs: masks with a shape (LAYOUT.md, decided 2026-09-07) and the orchestrator queue in RUST.md

Iris: masks should carry a shape, rounded rectangle first, or take a
container widget as the mask, with corner alpha multiplied rather than
cut. Design: the mask evaluates the same SDF draw_rounded_rect uses,
nested masks chain and multiply like moves, and a rounded Rect's
.masked() makes the container the mask with one radius by construction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 12:34:19 -04:00
1 parent 6840edf61e
commit a999bd106a
2 files changed
+88

No files matched your search

+69
View File
@@ -947,3 +947,72 @@ When this lands, copy this entry into `IRIS.md` (newest first):
> `SizeCtx` and `Cache` are gone with it — see `LAYOUT.md` for the full
> design, the move-offset mechanism this shipped alongside, and the file
> list.
## Masks with a shape (decided 2026-09-07, not yet built)
Iris, on the code block's scrolling: "the code block scrolling currently
masks in an inner rectangle. Ideally masks should have a shape
associated with them, rounded rectangle being one of them, and/or
another widget you can select, so that the mask becomes the parent
container with rounded edges. Make sure alpha works properly with it,
eg. on the corners where alpha should be decreased / multiplied."
**What exists.** `Mask` in `shader.wgsl`/`data.rs` is two `UiSpan`s and
a `move_idx`; `fs_main` resolves it and does `color *= 0.0` outside the
rectangle -- a hard cut on a pixel boundary. `Masked` (`widget/mask.rs`)
sets the painter's mask to its own region. Separately, `draw_rounded_rect`
already produces an anti-aliased rounded edge from
`distance_from_rect(pos, center, corner, radius)` with a half-pixel
`smoothstep`, and the border variant multiplies a second coverage in.
**Design.**
1. **A mask is a shape, and the shape is the same SDF the `Rect`
primitive draws with.** `Mask` gains `radius: f32` (one uniform
corner radius, matching `Rect.radius`; per-corner radii only when a
concrete need appears). The fragment stage computes coverage as
`1.0 - smoothstep(-min(edge, radius), edge, distance_from_rect(...))`
-- the *same expression* `draw_rounded_rect` uses, factored into one
function both call -- and does `color.a *= coverage`. So a mask whose
region and radius equal a rounded container's are clipped to exactly
the pixels that container fills, corner alpha included, because they
are the same arithmetic. `radius = 0` becomes a half-pixel
anti-aliased edge instead of today's hard cut, which is what `Rect`
does already, so a masked rect and an unmasked one look the same.
2. **Nested masks chain and multiply, like moves.** Today one
`mask_idx` per primitive; nesting two rectangles could be handled by
intersecting spans on the CPU, but the intersection of two rounded
rectangles is not a rounded rectangle. So `Mask` gains `parent: u32`
(the enclosing mask's slot, or the sentinel), the shader walks the
chain multiplying coverage, and the walk is bounded the way
`resolve_move` is (`MOVE_CHAIN_LIMIT`'s sibling; assert on overflow
in debug, print the chain). The painter's `set_mask` records the
current mask as the parent. Alpha multiplies rather than takes a
minimum, so a pixel in two feathered corners is dimmed by both --
that is what "alpha should be multiplied" asks for and what a real
compositor does.
3. **The widget API: the container is the mask.** `Masked` takes a
`MaskShape` (`Rect`, `Rounded(radius)`); and the rounded `Rect`
widget, the thing a code block or card is already inside, gets a
`.masked()` builder that wraps its children in a `Masked` carrying
*its own* radius. One value, by construction, never a radius on the
container and a second one on the mask to keep in sync. The code
block in `transcript-ui/src/row.rs` (`.masked()` at the inner
rectangle) moves to masking at the rounded container instead.
4. **Hit-testing keeps the rectangle.** Input outside the rounded
corner but inside the box is a few pixels; not worth a second SDF
walk on the CPU. State this in the `Masked` doc so nobody "fixes" it.
**Rejected.** A stencil buffer (a second pass per mask level and no
anti-aliasing); the scissor rectangle (rectangles only, no alpha);
rendering a masked subtree to an offscreen texture and compositing
(a texture allocation per mask, every frame it scrolls, on the phone).
**Pass conditions.** A headless test draws a rounded container with a
masked child that overhangs all four sides and asserts the child's
coverage at a corner pixel equals the container's own coverage there
(same SDF, so exactly equal, not approximately); a nested-mask test
asserts the product at a pixel inside both feathers; a
`run-headless.sh --phone` screenshot of a scrolled code block shows
rounded corners with no square pixels poking out at the top and bottom
of the scrolled content. Record the commands in RUST.md when it lands.
+19
View File
@@ -43,6 +43,25 @@ gated on her verdict**, so this pass works the P0 defects and the pure
prerequisites in this order. Each item is ticked here by the agent that
closes it.
### Queue, 2026-09-07 (orchestrator)
In order; two builders at a time. Each is ticked here by the agent that
closes it.
- [ ] Test rig, layers 1 and 2 ("Three test layers" below). Running.
- [ ] Fling parity with Compose, and the phone's keyboard push-up, with
insets shown in the diagnostics overlay. Running, in a worktree.
- [ ] Phone logging through Dev Updater (Iris has no logcat; see
docs/TODO.md and the memory note): research how Dev Updater shows an
app's runtime log, design the smallest route (the app keeps its own
recent log; a debug button copies it; Dev Updater reads it), write
the decision in docs/DECISIONS.md, build it.
- [ ] Masks with a shape -- docs/LAYOUT.md "Masks with a shape (decided
2026-09-07)". Rounded masks through the `Rect` SDF, chained and
multiplied, the container as the mask.
- [ ] Compose app: the `Reversed range` crash in `ToolInput.highlighted`
(docs/TODO.md). Main branch, not rustify.
### Desktop and phone share the code (Iris, 2026-09-07)
Iris plans to develop a desktop app as well, and asked that most code be