Skip to content

fix(html): read a zoomed view's rects in the space its pointer events use - #744

Open
andiwand wants to merge 1 commit into
mainfrom
fix/viewport-rect-space-and-held-position
Open

fix(html): read a zoomed view's rects in the space its pointer events use#744
andiwand wants to merge 1 commit into
mainfrom
fix/viewport-rect-space-and-held-position

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Two defects in the emitted zoom script, both against 6.10.0 and neither waiting on #726's mode (4). Stacked on top of this: the opt-in live fit itself.

The coordinate split

getBoundingClientRect carries an applied body{zoom} on chromium (since 128) and not on webkit, while elementFromPoint takes viewport coordinates on both. anchor() and restore() mixed the two spaces directly, so on webkit a body{zoom} — written by viewport_width, or applied by odr.setZoom() — put the anchor's intoY in one space and its target point in the other.

What that costs, measured:

setZoom(0.8) from 0.5, webkit
focus at y = 400 off by 301px of document fixed: 1.3px
no focus (y = 1) off by 1.3px 0.6px

The unfocused anchor was already right, which is why nothing had shown up: restore() is re-asserted over 30 frames, and that iteration converges on the same answer for a point at y = 1 whatever space it computes in. A pinch's focus is where it comes apart.

rectFactor() measures how much of the zoom the engine reports, through a 100px probe, and caches the answer — it is a property of the engine, and only decidable while a zoom is applied, which is also the only time it matters. The correction is 1 on chromium, so nothing changes there.

odr.getViewportRect(element) puts the same conversion in reach of a host. That is the half core cannot fix from inside: OpenDocument.ios#180's edit test reads a rect and hands its centre to elementFromPoint, which picks nothing while a fit is applied.

The dropped reading position

resized() assigned width before delegating, so remember()'s root.clientWidth !== width guard passed and held = anchor() overwrote the pre-resize anchor with one read after the browser had already relaid out and moved the scroll. Every view whose zoom does not follow the viewport — a stated viewport_width, a pinned initial_zoom, a sheet — lost the reader's place on every rotation.

The settling loop is now settle(), which apply() calls after changing the zoom and resized() calls without changing it. A prose document scrolled to paragraph 143, viewport 900 → 400: p143 → p100 before, p143 → p143 after.

This is a behaviour change beyond the missing variable, and the changelog says so: a sheet resized in a desktop browser now holds its place too. Chromium's own scroll anchoring was already doing approximately this; webkit has none.

Verification

test/browser/viewport/ — the harness this was measured with, added because viewport.js had no way to be tested and this changes what it does. serve lifts the script out of frontend.cpp so it cannot drift from what ships; tests.html runs it in an iframe and reports.

Both engines are covered from one browser: page.html?webkit=1 divides the zoom back out of chromium's rects, which is exactly what webkit returns. Positions are read as (scrollY + y) / zoom, never through the script's own helpers, and overflow-anchor: none keeps chromium's scroll anchoring from covering for the script. It is run by hand — there is no headless driver here — and the README says what each choice is for.

Against this branch all seven checks pass; against main's script the pinch is 301px out, the resize drifts 43 paragraphs, and getViewportRect does not exist.

Not covered

Neither fix has been run on a real device — no ios simulator or android emulator in this loop. The webkit half is a simulation of the behaviour OpenDocument.ios#180 measured, not a reading from webkit itself.

… use

`getBoundingClientRect` carries an applied `body{zoom}` on chromium since
128 and not on webkit, while `elementFromPoint` takes viewport
coordinates on both. `anchor()` and `restore()` mixed the two, so a
pinch's focus moved by the zoom's worth of its distance from the top of
the screen on ios - measured at 301px of document for a focus 400px down
at 0.5 -> 0.8. The unfocused anchor at `y = 1` was already right: the
settling loop converges there whatever space it computes in, which is why
nothing had shown up.

`rectFactor()` detects how much of the zoom the engine reports, so the
correction is `1` where the rects already carry it. `getViewportRect` puts
the same conversion in reach of a host hit-testing its way back to an
element.

Separately, a view whose zoom does not follow the viewport dropped the
reading position on a resize: `resized()` assigned `width` before
delegating, so `remember()` took the assignment branch and overwrote the
held anchor with one read after the browser had relaid out and scrolled.
It now settles on the anchor it held, like the fitted branch does.

Refs #726

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ7jMy9qsh5CofYxiAncUG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant