iris: the composer scrolls on a finger -- a dp cap worth zero, a stale mask slot, a hit box moved twice

Wrapping the composer's field in .scrollable().masked() needed three
layout defects fixed first, each with a headless regression test that was
confirmed to fail without its fix:

- MaxSize/Sized reported a caller's declared dp length unresolved, and
  Span places a child from the abs/rel of what it reported, so dp(168)
  was worth zero: the bar got a slot of nothing the moment its content
  passed six lines and the Scroll inside measured its container at -63px
  (container=-63 content=415.8 amt=478.8 on the emulator). Len::fold_dp,
  used on the way out, plus a debug_assert in draw_inner that a reported
  Size carries no dp -- the rule is about every widget, not those two.
- Masked allocated a fresh mask slot per draw, and draw_inner's
  unchanged-region fast path does not revisit descendants, so they kept
  clipping against a box the bar had moved away from: four live mask
  entries, none of them current, and the field drew nothing.
  ActiveData::own_mask, allocated once and rewritten in place.
- mov updates active.region and accumulates the same delta on the move
  slot, and resolved_region added both, so a panned widget's own hit box
  sat at twice the pan -- the composer's field was untappable after a
  drag. ActiveData::move_applied.

Scroll itself measured the right number by a misleading route; it is
written against painter.px_size() now and still reports its content's
size, since reporting the container makes the answer a function of
itself.

Verified on this checkout's emulator: swipe 540 1200 -> 540 1460 moved
the field's Message box 31,1041..1048,1509 -> 31,1131..1048,1651 with its
height unchanged at 468px.

run-bench.sh polled logcat for a prefix copy_report also logs at startup,
so it printed a report that had never been run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-06 17:17:42 -04:00
1 parent d73db97629
commit 167862ca1b
14 files changed
+567 -39

No files matched your search

+44
View File
@@ -8,6 +8,50 @@ capability that moved. Small and trivial changes do not go here.
An entry gives the date, what changed, why, and a short before/after where
it helps judge the change without the session that made it. Newest first.
## 2026-09-06: a reported `Size` may not carry `dp`; `Len::fold_dp`
**New: `Len::fold_dp(density) -> Len`** -- the same fold `apply_rest` does
(`dp` becomes physical pixels), but staying a `Len` so `rest` survives.
**New rule, and it is a rule about every widget, not about the two that
broke it**: a `Len` a widget *reports* from `draw` must not carry an
unresolved `dp`. `dp` is an input unit -- a number the widget author wrote
-- and the containers that consume a reported length read `abs`, `rel` and
`rest` straight off it (`Span`'s placement arithmetic, `Pad`'s addition),
so a reported `dp` is silently worth **zero**. `MaxSize` and `Sized` both
returned the caller's declared `Len` as written; a `.max_height(dp(168))`
therefore gave its child a slot of nothing the moment the cap actually
applied, which is what made the composer's bar collapse. Both put their
declared lengths through `fold_dp` now, and
`UiRenderState::draw_inner` `debug_assert!`s the invariant after every
`Widget::draw`, so a widget that gets this wrong says so at the mistake
rather than laying out at zero somewhere else.
Nothing changes for a caller: `.max_height(dp(48))` is written the same
way. It is only widget *authors* who now have a rule to follow, and a
debug build that enforces it.
## 2026-09-06: `Painter::set_mask` reuses one slot; `ActiveData` gains two fields
**`Painter::set_mask(region)` allocates its widget's mask slot once and
rewrites it in place** on every later draw, instead of pushing a new one
each time. It has to: `draw_inner`'s unchanged-region fast path does not
revisit a descendant whose own region did not change, so those descendants
go on referencing whichever slot they were first drawn under. Pushing a
fresh slot per draw left the composer's field clipped to a box the bar had
long since moved away from -- four live mask entries, none of them the
`Masked`'s current region -- and it drew nothing at all. Same call, same
signature; only the lifetime changed.
**`ActiveData` gains `own_mask` and `move_applied`** (both public, since
`ActiveData` is). `own_mask` is the slot above, `MaskIdx::NONE` for a
widget that sets no mask. `move_applied` is how much of a widget's own
move-slot delta its `region` already accounts for: `mov` shifts both,
`Painter::reposition` shifts only the slot, and `resolved_region` -- and so
every hit test -- has to subtract it. Without that a widget that had been
panned had its *own* hit box at twice the pan while its descendants were
correct, which made the composer's field untappable after a finger drag.
## 2026-09-06: `Scroll` pans on a finger drag, and a vertical drag in a focused text field no longer selects
Three related public changes, all in aid of IRIS_TODO.md's "the composer