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:
1 parent
6840edf61e
commit
a999bd106a
2 files changed
+88
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user