The renderer kept a mirror of the pushed IME snippet (the 'IME model')
to translate commit positions, but it transiently desynced from the
buffer on fling/tap sequences (observed as a few-byte mapping drift on
both the x86_64 emulator and the ARM phone), corrupting text. The model
string also sat on the main goroutine next to the JNI render path,
where the app observed states that were impossible for Go memory
(string contents changing between reads microseconds apart), pointing
at corruption in the native bridge layer.
Restructure along the lines of the Android InputConnection contract
and Gio's own reference editor (widget/editor.go):
- Commits carry absolute file runes (the pushed snippet's coordinate
space) straight to the logic goroutine, which maps them to bytes
against the WHOLE buffer (runeToByteWhole, an 8 KiB-step scan).
Scrolling moves the window, not the buffer, so the mapping is exact
mid-fling by construction — no mirror to desync.
- Drift guard in HandleIMECommit: a small commit (range <= 2 runes)
is always anchored at the caret the IME was last told about; if the
IME reports it ending elsewhere, its snippet text is stale (a
dropped restartInput, as Gboard does during flings) and its
position is in the stale text's coordinates — snap the commit to
the cursor, the only position it cannot drift from.
- FlushIME simplifies to: push the snippet when the frame's
(context+window) text differs from the last push (gioui dedupes
against its own cache), force the selection re-push in the same
frame. After a commit the frame text equals what the IME already
holds locally, so the restart is naturally suppressed; a fling
re-anchors the IME once per text change.
- Remove the renderer model (adoptFrame/ModelTranslate/
ApplyIMEEdit/ApplyIMEKey/IMECaret), the IME freeze/settle
machinery (IMEFrozen, markIMEScrollActive, imeSettleChan), and the
window-relative imeRuneToByte.
Also fixed along the way (both found while chasing the corruption):
- real.ReadFileAt: loop over short reads. A single ReadAt on Android
FUSE can return a short read, silently truncating a chunk and
shifting every byte offset after it.
- logic: a late lazy-chunk result no longer clobbers a buffer that
SetContent has already fully loaded.
- e2e: large-file IME test (1.6 MB file, fling + commit).
- app icon (scripts/make_icon.py + cmd/pad/appicon.png) so gogio
builds the mipmap/adaptive icon set.
Verified: go vet + staticcheck, go test -race (all packages), and the
emulator scenario loop (open moby excerpt, fling to mid-file, tap,
type 'a', byte-compare the saved file) 75/75 clean.