Docs: full coordinate pipeline reference (screen px -> file byte)

doc/README.md §Screen coordinates: add the complete 4-hop pipeline
(screen px -> app dp -> text-local dp -> window-relative glyph byte ->
absolute file byte), the tapLocalY sub-line-remainder rule, the
glyphBase window-base rule, and the selection/menu/IME coordinate
spaces (absolute file bytes vs window-relative TextField selection vs
app-dp drag events vs menu-local MenuItem offsets). Cross-reference
the architecture.md invariants.

doc/architecture.md §8: state that CheckGestures input-event positions
are app-local Dp (same space as element Regions / MenuRect) and that
logic converts them to text coordinates.
This commit is contained in:
Greg Pomerantz 2026-08-17 09:06:22 -04:00
parent 2a16c0017c
commit 7f2d3144eb
2 changed files with 50 additions and 4 deletions

View File

@ -76,10 +76,13 @@ directory: `/storage/emulated/0/Notes`.
## Screen coordinates
Three coordinate spaces are in play; mixing them up is the #1 cause of
tap-test errors. (Measured 2026-08-16 on `pad_avd`; re-derive with
`adb shell wm size`, `adb shell wm density`, `dumpsys window`, and a fresh
screenshot with the keyboard open if the device config ever changes.)
The app has several coordinate spaces (screen/display/app-local px, app dp,
text-local, window-relative glyph bytes, absolute file bytes); mixing them
up is the #1 cause of tap-test and cursor bugs. (Measured 2026-08-16 on
`pad_avd`; the 128 px offset and the 2.625 scale are device-config facts —
re-derive with `adb shell wm size`, `adb shell wm density`, `dumpsys
window`, and a fresh screenshot with the keyboard open if the device config
ever changes.)
| space | size | used by |
|--------------|-----------|------------------------------------------|
@ -94,6 +97,44 @@ Conversions:
- screen → app-local: **Y 128** (surface starts below the status bar; X unchanged)
- app-local px → pt: **÷ 2.625** (density 420)
Full pipeline, screen → file byte (each hop has exactly one place it
happens; the app is internally consistent, so errors only appear at the
hops, not inside a space):
1. **screen px → window px → app dp** — OS/Gio. Taps you inject land here;
this is the only space `adb`/screenshots touch.
2. **app dp → text-local dp** — the tap/drag handlers
(`localX = x EditorRegion.X`, `localY = tapLocalY(...)`).
**Y adds only the sub-line scroll remainder** (`ScrollOffset mod
lineHeight`), never the full scroll: the visible glyph layout is
window-relative, so adding the full scroll maps a tap to a line far
below the window (the Phase 7 tap-to-position bug).
3. **text-local dp → window-relative glyph byte**
`visualLine = y / lineHeight`, then glyph x-search within that line
group. `GlyphLayout.ByteOffsets` are relative to the **top of the
visible window** (`IMEWindowStartByte`), not the file start.
4. **window-relative byte → absolute file byte** — add/subtract the window
base (`IMEWindowStartByte`, the `glyphBase()` helper) at each cursor
boundary (tap, Home, End, vertical move, selection handles). Forgetting
this hop is what snapped the cursor to the window top on scrolled files.
Selection / menu / IME spaces (all app dp unless noted):
- `EditorState.SelectionStart/End`, `SelectionAnchor`, `CursorPosition`:
**absolute file bytes** (architecture.md §6.3).
- `ui.TextField.SelectionStart/End`: **window-relative** bytes into the
visible `Value` (1 = none) — rendering + IME use only.
- `SelectionDragEvent.X/Y`: finger position in **app dp** (renderer reports
the finger; logic converts, exactly like a tap — §6.3a).
- `MenuRect`: app dp; `MenuItem.X/Y`: **menu-local** (relative to the menu
origin — `Menu.Draw` adds the origin back when drawing; a tap is inside
the menu iff it is inside `MenuRect`, item index =
`(x MenuRect.X) / itemW`).
- IME `key.EditEvent.Range`: **window-relative rune indices** (§6.4).
Invariants: architecture.md §6.2 (window base), §6.3 (selection/caret),
§6.3a (touch selection), §6.4 (IME).
**The tap rule (covers ~95% of needs):** to tap something visible, read its
(x, y) off the PNG you are looking at, multiply both by **1.2**, and
`input tap x y`. Screenshot px *are* screen px — no 128 offset involved.

View File

@ -391,6 +391,11 @@ Only the visible byte range is shaped and drawn each frame:
4. `e.Frame(&ops)` flushes ops.
- After draw, `CheckGestures` turns raw pointer/gesture state into
`[]ui.InputEvent` (each carrying its own handler) for the logic channel.
Tap/double-tap/long-press/selection-drag event positions are **app-local
Dp** (window px ÷ `PxPerDp` — the same space as element `Region`s, and
`MenuRect`); the logic side converts them to text coordinates with the
stored `EditorRegion` + scroll (README.md §Screen coordinates, pipeline
hop 2).
- **Units:** `ui.Dp`/`ui.Px` convert via the current `PxPerDp` scale
(`ToDp`/`ToPx`). The window is 390×844 dp; the real pixel size arrives via
`ConfigEvent`.