Conversation
`set_viewport` updated the process-global metrics that seed a *future* JS context but never touched one that already existed, so a loaded page kept the size it started at forever: `innerWidth`, `innerHeight` and `devicePixelRatio` stayed frozen, width-based media queries never re-evaluated, and no `resize` event was ever dispatched. Any page doing its own responsive work in JS was stuck at its load-time layout. `__viewportChanged` is added to the browser environment — it refreshes the globals, re-evaluates live `MediaQueryList`s and dispatches `resize` — with `Session::notify_viewport_changed` to drive it, mirroring the existing `notify_color_scheme_changed` path. Layout is re-run and the rect table re-pushed *before* dispatch. A resize handler exists in order to measure, and `getBoundingClientRect` reads the pushed rect table rather than the globals, so dispatching first let handlers observe the pre-resize geometry and cache the wrong values. The cache is dropped again afterwards so DOM mutations made by handlers are laid out on the next render. Co-authored-by: Cursor <cursoragent@cursor.com>
The early return added for "no change" skipped `js::set_device_metrics` as well as the JS re-entry it was meant to avoid. Those metrics are process-global and start at their own 1200x780 default rather than at the engine's 800x600, so an embedder whose first call names the size the engine already holds never synced them at all — and every document it loaded reported 1200x780 as `window.innerWidth`/`innerHeight`. That is exactly what the WebDriver server does: a session with no requested size is 800x600, so `set_viewport(800, 600, 1.0)` on a fresh engine hit the guard. Pages under the whole test suite saw a 1200x780 window. Publish before the guard, since it is a cheap atomic store that has to reflect the current viewport regardless, and keep the guard for the layout churn and `resize` dispatch it was added for. The test drives it through WebDriver, where the bug was reachable, and covers both the unchanged and the resized path so the fix can't degenerate into dropping the guard. It needs its own integration binary because the metrics are process-global: sharing a process with a test that sets a different viewport would make it race. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Pushed a follow-up: the "no change" early return I added was also skipping Those metrics are process-global and start at their own The WebDriver server does exactly that — a session with no requested size is 800×600, so It cost one real test in The publish now happens before the guard — it's a cheap atomic store that has to reflect |
What & why
set_viewportupdated the process-global metrics that seed a future JS context, but never toucheda context that already existed. A loaded page therefore kept the size it started at forever:
window.innerWidth/innerHeight/devicePixelRatiostayed frozen at load-time valuesmatchMedia(...).matcheswent staleresizeevent was ever dispatchedAnything doing its own responsive work in JS was pinned to its load-time layout. I hit this building
a GPUI frontend against the engine: resizing the window resized the framebuffer, but the page inside
it kept laying out for the old width.
__viewportChangedis added to the browser environment — it refreshes the globals, re-evaluateslive
MediaQueryLists and dispatchesresize— withSession::notify_viewport_changedto drive it.This deliberately mirrors the existing
notify_color_scheme_changedpath rather than inventing asecond mechanism.
Ordering: re-layout happens before dispatch
The part worth reviewing closely. A
resizehandler exists in order to measure, andgetBoundingClientRectreads the pushed rect table, not the globals set above. Dispatching firsttherefore let handlers observe pre-resize geometry and cache the wrong values — a stale layout is
arguably worse than no event, because the page acts on it confidently.
So
set_viewportnow drops the layout cache, re-runsensure_layout, and re-pushes the rect tablebefore dispatching. The cache is dropped once more afterwards, so DOM mutations made by handlers
are laid out on the next render (the same reasoning as
set_color_scheme).The regression test pins this ordering specifically. It asserts on a percentage-width div measured
inside the handler, because the root's own
clientWidthfalls back toinnerWidthand so wouldpass even if layout had never re-run. Verified in both directions — with the re-layout removed the
handler sees
500(the pre-resize width) instead of320.Noted but deliberately not fixed here
clientWidth/clientHeightare never populated for non-root elements:set_layout_rectsfeedsgetBoundingClientRectbut not the__elemMetricspadding-box fields, so a non-root element reads0. That is a pre-existing gap unrelated to resize, and folding it in would have muddled thischange — happy to open a separate issue or PR for it if useful.
🤖 How this was built
Checklist
cargo test --workspacepasses — addsresize_relayouts_before_handlers_observe_geometry, which also covers that an unchangedviewport does not re-enter JS at all
cargo fmt --all+cargo clippyclean for this change (clippy reports some pre-existingwarnings in
wurlandnet, untouched here)a fixed viewport; covered by the unit test above instead
Made with Cursor