Files
ai-app/docs/IRIS_TODO.md
T

116 lines
6.6 KiB
Markdown

# iris: known problems and things still to build
Iris's own list for the library, recorded 2026-09-04 in her words where it
matters, so the work in RUST.md picks these up in a sensible order rather
than rediscovering them. Each item says where it sits in the order and
what "done" looks like.
**Only open items live here.** An item is deleted when it lands, not
ticked: a list of finished work is context every future session pays for,
and what a change did belongs at the code it changed. Fifty closed items
and six phone-report sections went on 2026-09-08 for that reason.
## Fix
- [ ] **Colours are not in a defined colour space. Fix this before a
styling pass.** Both render backends prefer an sRGB surface
(`default/render.rs` and `android/render.rs`), while `fs_main` returns
`unpack4x8unorm` palette and image bytes unchanged. An sRGB attachment
treats those values as linear and encodes them again: Mocha Crust
(17,17,27) became (73,73,91) in a desktop screenshot measured on
2026-09-06.
The earlier entry called this desktop-only because the Android picture
looked right. That was not a measurement: neither the startup line nor
the diagnostics report records the selected surface format, and the
Android backend contains the identical preference and shader path. A
device exposing only a non-sRGB surface can happen to hide the bug; it
does not make the pipeline correct.
Done means defining one convention for palette bytes, decoded images,
colour emoji and the clear colour, then converting exactly once for the
selected target. Record the selected format in diagnostics, and add a GPU
test that draws known non-black, non-white pixels into an sRGB target and
reads the stored bytes back; screenshots from desktop and Android then
confirm the same Catppuccin values rather than serving as the definition.
## Build (for the port)
Widgets `RUST.md`'s "The port, in order (decided 2026-09-05)" needs and
iris does not have yet, one entry per gap, named against the P-step that
first needs it. Move an entry up to "Fix" if it becomes a current defect;
delete it once built rather than duplicating it there.
- [ ] **Selectable, read-only text.** P0's report and P1's transcript rows
use `TextEdit` because `Selectable` is implemented only for it. That
makes prose focusable and opens the IME over text that cannot be edited.
A display widget needs the same selection geometry and clipboard path
without a text-input accessibility role or keyboard focus.
- [ ] **Overflow ellipsis with an explicit retained end.** `TextAttrs` can
only wrap or clip, so a tool summary is cut with no mark. Parley has no
ellipsis primitive; use its line breaker to find the cut, but keep source
and displayed strings distinct with one byte mapping shared by spans,
links, selection and editing. Replace `wrap: bool` with an enum that can
say wrap, clip, head ellipsis and tail ellipsis—the caller must choose
because a command is identified by its head and a path by its tail.
- [ ] **Expose the distance from a `LazySpan` viewport to its unloaded
edge.** (**P1**.) `viewport_len` and the visible extents are already
measured internally, but a paging caller cannot ask whether it is within
the Compose app's six-viewport `HISTORY_SCREENS` cushion. The API should
answer in pixels or viewport multiples, never rows: a row ranges from one
line to a screen, so a fixed row count is not a distance.
- [ ] **Let an image fit a bounded box while preserving its aspect ratio.**
(**P1**.) `Image` currently always reports and draws the decoded texture's
natural pixel size. Decoding and fetching a server-produced attachment
belong in `app-rust`; iris only owes the generic fit/scale widget used to
draw its thumbnail.
- [ ] **Per-range backgrounds for rich text.** (**P1**.) Inline code is
already monospace and coloured, but matching Compose's chip also needs
the glyph run's boxes so a surface can be drawn behind exactly that byte
range. `TextEdit` already computes the same geometry internally for its
selection highlight; expose one shared primitive rather than giving the
app a second text-layout path.
- [ ] **A modal/dialog primitive.** (**P1**, reused by **P3** and
**P5**.) Needed for the session settings dialog, `UsageDialog`'s
equivalent, and the delete-with-`deleteForeign` confirmation with its
toggle switch. Build once, wherever it is first needed, rather than
once per screen that wants one.
- [ ] **A horizontal gauge/bar widget.** (**P1**.) For
`SessionUsageBar`'s equivalent — a bounded fill reflecting a fraction,
nothing fancier.
- [ ] **A toggle switch.** (**P3**.) For the delete dialog's
`deleteForeign` control; iris has no switch/checkbox widget yet as far
as this pass found.
## Later
- [ ] **Property/content animations.** Cosmetic, so after correctness and
parity. Keep them modular, like input; scrolling already animates through
`Widget::tick` and `UiData::animate`. A widget that does not opt in must
pay nothing and import nothing for them.
- [ ] **Remove `WidgetView` unless a real composite adopts it first.** The
layout change this decision was waiting for has landed. Every composite
in `app-rust/src/ui` now uses ordinary child handles plus a returned root;
`WidgetView` and its derive are used only by `iris/examples/view.rs`.
Today it demonstrates itself rather than shortening production code, so
deletion is the concrete default—not another parallel composition style.
- [ ] **A `Stack` that chooses its mask the way it chooses its size
(Iris, 2026-09-08).** She asked whether `masked_by` deserves to exist:
"a method that just does 2 separate things you can already easily do
does not deserve to exist." For a square-cornered surface it is indeed
redundant -- `.background(rect(BAR_FILL)).masked()` was measured
against `.masked_by(rect(BAR_FILL))` on the composer at the phone's own
size and density and the two are identical to the pixel. What the pair
cannot express is a clip that is not a box: `Painter::set_mask` writes
a `RectPrimitive::color(Color::NONE)` at the widget's own region, with
no radius, so `.background(rect(fill).radius(r)).masked()` draws a
rounded panel and then cuts its content square. Both other call sites
(`row.rs`'s fence, `tool.rs`'s raw output) are rounded, which is why
the method stands for now.
Let `Stack` name the mask child the way `StackSize::Child(n)` names the
sizing child. Then `.background(x)` remains the one way to add a surface
and clipping to it is a stack property; the named mask child must have
drawn before any child that uses it. Once that exists, delete
`masked_by` and `Masked::shape` rather than retaining two APIs.