From 476609e1d314204dfc2781b27bcc97e3cca4c923 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Tue, 8 Sep 2026 13:27:46 -0400 Subject: [PATCH] docs/RUST.md: Iris's 2026-09-08 phone report -- tap-jump, nested scroll, folded cards, and the bench's 60Hz gesture Co-Authored-By: Claude Opus 5 --- docs/RUST.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/docs/RUST.md b/docs/RUST.md index 3bc63c0..51d2cfa 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -7804,3 +7804,139 @@ bigger design question in shared tooling. - uniffi: [repo](https://github.com/mozilla/uniffi-rs), [KMP bindings fork](https://github.com/UbiqueInnovation/uniffi-kotlin-multiplatform-bindings) - GPUI mobile: [gpui-mobile](https://github.com/itsbalamurali/gpui-mobile) - The earlier Dioxus spike's findings on `wgpu`/Vulkan in this emulator: `~/repos/tdep-survey/app-dioxus/README.md` + +## Iris's phone report, 2026-09-08 (three defects, from the app on her phone) + +Sent with a screenshot of an expanded tool group and three `iris +diagnostics` dumps taken while scrolling by hand (no benchmark had run). +Her words, then what the diagnosis is and who is fixing it. The phone was +on Vulkan throughout (`adapter: Mali-G715 (Vulkan)`), with `wgpu errors: +none` and frame times that are *not* the complaint: `p50=2.8ms p90=5.4ms +p99=55.7ms worst=99.2ms janky%=1.55` on the worst of the three dumps and +`janky%=0.21` on the best. So all three are behaviour, not throughput. + +### 1. "Tapping sometimes seems to make the scrolling jump, particularly +### when tapping on things that have events like horizontal scrolling" + +**Hypothesis to test first: `List::note_tap`'s re-anchor fires on taps +that change no height, and resolves against stale `extents`.** `list.rs`'s +module doc ("Hold the edge nearest the tap") is explicit that the *next* +`List::draw` re-anchors to whichever row contained the last tap, choosing +the nearer edge, using `extents` cached *as of the previous frame*. That is +right for the case it was built for -- a row about to change height -- and +it is a free position change for every tap that changes nothing, which is +most of them: `tool.rs`'s `note_tap` runs on every card tap and every group +toggle, and a tap landing on a code fence or a card in the middle of a +settle or a fling resolves against extents that no longer describe where +those rows are. Re-anchoring to a stale box *is* the jump. + +Pass conditions: + +- A tap that changes no row's height moves nothing on screen, from a + settled list and from one still settling, in a layer-1 + (`transcript-fixture`) test driven from a `.touch` recording -- the + layer that owns the clock, so "still settling" is expressible. +- A tap on a row that *does* change height still holds the edge nearest + the tap; the existing test for that keeps passing. +- The fix is in `List`, not in each caller: `note_tap`'s siblings are + every widget that taps (cards, the collapse bar, links, the composer), + and a rule stated at one caller is the defect class CODE_RULES names. + +**Iris, same day, once she had looked harder -- this is the real shape of +it, and it supersedes the "check this too" paragraph below:** *"it seems +like mixing horizontal and vertical scrolling is broken. It should only +trigger horizontal if you drag left or right, and vertical should fall +through if you drag up or down. It also keeps snapping back to some +position when horizontally scrolling for some reason."* + +So there are two faults in the nested case, and the first is the one to fix +properly: + +- **A horizontal `Scroll` claims a gesture it should have let through.** + The rule she states is the one to implement, and it is the rule + `DragArbiter` already knows how to express: a press is `Undecided` until + it has moved `DRAG_SLOP`, and then it belongs to whichever axis it + actually moved along -- the child if that is the child's axis, the + enclosing list otherwise. Today the child's arbiter is built with + `DragArbiter::on(Axis::X)`, whose `Panning` branch takes the drag on its + own axis and answers `Undecided` for everything else, and *nothing hands + the rejected gesture to the list* -- the outer list's arbiter never got a + `press_start` for it. This is one mechanism for both directions, not a + special case for fences: an inner scrollable defers to its parent for + any axis it does not own. +- **Horizontal scrolling snaps back to a position.** First suspect is + `Scroll::update_amt` clamping against `content_len - container_len`, + which LAYOUT.md says is a frame behind -- and the fence's content is + inside a `masked()` (the `scrollable_on(Axis::X)`-draws-nothing + workaround above), so the length being clamped against may be the + *clipped* box rather than the text's real width. If so, `amt` is pulled + back to 0 on the next frame, every frame, which reads exactly as a snap + back. Measure it before fixing it: log `amt`, `content_len` and + `container_len` for one horizontal drag. + +Also check, because it is the other half of what she named: what a +horizontal `Scroll` (`row.rs`'s `BlockFrame::Verbatim`, `scrollable_on +(Axis::X)`) does to the vertical gesture that passes through it. Both +arbiters see the same press; `Scroll::drag` passes `PressState::default()`, +so it cannot know the list under it was flinging, and the list's own +arbiter may never have seen the `press_start` (`DragArbiter::update`'s doc +records that recovery path for `Selection`). Say what the arbitration +actually is rather than adding a second recovery. + +### 2. "Opening the folded cards seems pretty buggy right now" -- and the +### sanity check she asked for + +Her framing is the important part: *"I don't understand how this is even +possible if the code is sane, please sanity check it and make sure it's +using the framework correctly. The framework should have good built in +change detection just by editing widgets in the context normally."* + +She is right to suspect the framework rather than the screen, because +`tool.rs` is **already written around four known, unfixed iris defects**, +all listed under docs/IRIS_TODO.md's "Found by P1b (2026-09-06)" and all +worked around there rather than fixed: + +- a `Span` of `Pad`ded children inside another `Span` places those children + a slot out of step (so the group is one flat `Span` and has lost the 4dp + inset Compose gives it); +- `scrollable_on(Axis::X)` on a non-editable `Text` draws nothing (so a + card's raw block is `masked()` and a long command is **clipped**, which + is what her screenshot's output box shows -- lines cut at the right edge + with nothing saying so); +- no overflow ellipsis at all, so a clipped summary looks like a short one; +- no drawn chevron, so the mark is a font glyph -- and the bundled fonts + were **removed on 2026-09-07** in favour of the platform collection, so + the empty box before "Edit" in her screenshot is almost certainly that + codepoint missing from the phone's own faces. UI_RULES: "don't rely on + characters the platform might not have." This one is a regression the + font change introduced and is the cheapest of the four to fix. + +So the work is not "make `tool.rs` behave"; it is **fix the framework +defects and delete the workarounds** (the standing rule: solve the setup, +don't live with a workaround every caller has to remember). The sanity +check she asked for is a second deliverable: whether `tool.rs`'s +rebuild-the-whole-row-on-toggle (`build_content` + `Shared::set_content`) +is the framework's intended change detection or a way around it, and if +iris's change detection is meant to handle "edit the widgets in the context +normally", why this screen does not. + +### 3. "The benchmark scrolling seems like it's at 60fps or something, it's +### way less smooth than me scrolling manually" + +**Confirmed by reading, before any measurement: the bench drives the +gesture at 60Hz by construction on a 120Hz phone.** +`iris/android-app/src/bench_client.rs`'s `ANIM_STEP_MS = 16` is a +`tokio::time::sleep` between synthetic move samples, and the fling phase +"ticks the fling forward in ~60Hz steps" the same way. A finger delivers +samples at the panel's rate; the bench delivers 62 a second. So the bench's +scroll really is coarser than hers, the difference she sees is the rig +rather than the renderer, and every frame number the bench has produced was +measured against a gesture no hand would make. + +Fix: drive one step per **real frame**, from the frame callback the +renderer already runs on, and derive each step's delta from the elapsed +time it reports rather than from a constant. Pass condition: the bench's +sample rate matches the display's refresh (say so in the report, next to +the adapter line, so a number is never read without it), and the report +states the rate it drove at so an old report cannot be compared to a new +one by accident.