docs: Iris's second 2026-09-08 phone report, and the workaround list closed

RUST.md gets the report verbatim with what each of the four defects
actually was, the tests that pin them, and two traps worth not
re-finding (a fixed-coordinate tap that "failed" by 544px because it had
toggled a tool group, and a layer-1 repro that only reproduces inside a
`List`). IRIS.md and DECISIONS.md get the design half: one `Flinger`
whose seam puts the sign convention and the content's end with the
caller, a cancel as a first-class end to a gesture, and why a row is
drawn twice on the frame its height changes.

LAYOUT.md gains the two rules those turned on, since both govern the
layout rather than this pass: padding works in any container and is an
inset or an outset depending on how tight the parent's region is (Iris's
own words), and a widget offered a box it does not fit is drawn again at
its true box in the same frame rather than the next one.

IRIS_TODO.md's "worked around in tool.rs rather than fixed here" is gone
-- Iris, 2026-09-08: "There should never be workaround code." Two of the
four entries are ticked; the two that remain are missing capabilities
rather than defects being dodged, and each now carries a diagnosis of
what building it costs instead of a workaround: an overflow ellipsis
needs `TextBuffer` to have a displayed string distinct from its source
(parley has none of its own, and every byte-offset consumer -- spans,
`byte_at`, `Selection`, `apply_delta` -- moves if the buffer is
truncated), and selectable tool-card text needs a register/unregister
lifecycle across the three routes that rebuild a card, which is where a
stale `Selection` handle panics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 15:26:12 -04:00
1 parent 02b277e7ad
commit fe7dc9c728
5 files changed
+353 -16

No files matched your search

+71 -15
View File
@@ -785,22 +785,35 @@ Iris's report, verbatim, with a screenshot. Phone: Mali-G715 (Vulkan),
## Found by P1b (2026-09-06), all with a headless repro
Each was found by looking at `iris/run-headless.sh transcript -- -p
transcript-ui` rather than at a diff, and each is worked around in
`transcript-ui/src/tool.rs` rather than fixed here. docs/RUST.md's P1b box
has the fuller account.
transcript-ui` rather than at a diff. docs/RUST.md's P1b box has the
fuller account.
- [ ] **A `Span` of `Pad`ded children inside another `Span` places those
**No entry here is worked around any more** (Iris, 2026-09-08: "All of
those should be fixed. There should never be workaround code. Do the
same for those; fix them if they're trivial, diagnose and report if
not."). Two are fixed and ticked; the two that are left are missing
*capabilities* rather than defects being dodged, and each carries its
diagnosis and what building it actually costs.
- [x] **A `Span` of `Pad`ded children inside another `Span` places those
children a slot out of step.** Each child drew its content one sibling's
height below its own box. Repro: `IRIS_TOOLS_EXPANDED=1
height below its own box. Repro was: `IRIS_TOOLS_EXPANDED=1
iris/run-headless.sh transcript --shot /tmp/x.png -- -p transcript-ui`
with `tool.rs`'s group built as `Span(DOWN)[header, Pad(Span(DOWN)
[cards]), bar]` instead of the single `Span` it uses now. Bisected:
removing the inner `Span` fixes it, and so does removing the children's
own `Pad`; the background `Stack`, the `Sized` wrappers and the
`WidgetPtr` per child make no difference. **Not** the `mov`-vs-
`reposition` fault f5b8893 fixed -- it survives that commit. The
workaround costs the group the 4dp inset its Compose counterpart holds
its cards off the edge by, so this is worth fixing.
[cards]), bar]` instead of the single `Span` it used. Bisected at the
time: removing the inner `Span` fixed it, and so did removing the
children's own `Pad`; the background `Stack`, the `Sized` wrappers and
the `WidgetPtr` per child made no difference. **Not** the `mov`-vs-
`reposition` fault f5b8893 fixed -- it survived that commit.
**Not reproducible on 2026-09-08.** Both spans are nested again and the
group has its 4dp inset back; that same headless render puts every
card's content in its own box, and `iris`'s
`a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is`
(`layout_tests.rs`, the same shape inside a `List`, which is the
context the real one is in) pins it at layer 1. Something between
09-06 and 09-08 fixed it -- most likely the nested-mask pass or the
`mov` work after f5b8893. Left ticked with the original symptom
recorded rather than deleted, in case it comes back.
- [x] **`scrollable_on(Axis::X)` on a non-editable `Text` draws nothing.**
The panel is drawn and the text inside it is not. A markdown fence does
the same to a `TextEdit` and is fine, so it is the widget kind rather
@@ -819,6 +832,32 @@ has the fuller account.
nothing on screen says it was cut. Whichever end is cut has to be a
choice when this lands: a path is identified by its tail, a command by
its head.
**Diagnosed 2026-09-08, and it is not trivial.** parley has no
ellipsis of its own (checked: nothing in the vendored crates), so iris
would build it, and the shape that looks easy is the one that breaks
something. The easy half really is easy: shape at
`max_advance = width - ellipsis_advance` with wrapping on, take line
0's `text_range()`, and re-shape `text[..end].trim_end() + "…"` with
wrapping off -- parley's own line breaker finds the cut, so nothing
here counts glyph advances by hand. The hard half is that
`TextBuffer` has exactly one string and everything addresses it by
byte offset: the inline spans that carry a fence's colours and a
link's range, `TextEditCtx::byte_at` (which turns a tap into a byte to
match a link against), `Selection`'s `select`/`selected_text`, and
`RowBlocks::apply_delta`. Truncating the buffer moves every one of
those. So the real work is giving `TextBuffer` a **displayed** string
distinct from its source, with one mapping from display byte to source
byte that all of those go through -- worth doing, and not a
by-the-way. Doing it only for text that is neither editable nor
selectable would avoid all of that and is exactly the kind of
exemption that comes back later.
It also wants an API change while it is open: `TextAttrs::wrap: bool`
cannot say three states. Something like `Overflow::{Wrap, Clip,
Ellipsis(End)}` replaces it, with `End::{Head, Tail}` making
UI_RULES's "choose which end to truncate" a thing a caller must
answer rather than a default nobody reads.
- [x] **A chevron the platform cannot fail to have.** **Done
2026-09-08**, twice. First as `iris::widget::mark(dir, dp, colour)`,
which rasterised an antialiased triangle into the ordinary texture path
@@ -841,9 +880,26 @@ has the fuller account.
- [ ] **A tool card's text is not selectable.** `Selection` is keyed
`(RowKey, block index)` and a card has no markdown blocks, so nothing in
a card registers. Compose's `SelectionContainer` covers tool output,
which is the text people most want to copy. Needs a key for "the nth
text of this row" that a card can mint without colliding with a
message's blocks.
which is the text people most want to copy.
**Diagnosed 2026-09-08: mechanical, but more than a sitting.** There
is no key collision to design around, which was the open question:
a `TranscriptRow::Tools` has *only* cards and no markdown blocks at
all, so a card is free to number its own texts from 0 in reading
order. What it costs is the registration lifecycle rather than the
key. Each card's `TextEdit`s have to `Selection::register` as they are
built and `unregister` when they are not -- and a card is rebuilt from
several directions (`redraw_card` when a result arrives,
`Shared::set_content` when the group is toggled or a call joins the
run, and the per-card `WidgetPtr` swap), each of which frees widgets
the map would otherwise still point at. That is the exact shape of the
crash `Selection::clear`'s doc records from
docs/REVIEW-2026-09-06.md: a handle in that map outliving the widget
panics on the *next* long press, somewhere else entirely. So the work
is a per-card base index with a stride (and a `debug_assert` that a
card stays inside it), one register/unregister path that every rebuild
route goes through, and a test per route that a rebuilt card leaves no
stale handle behind.
## Build (for the port)