Skip to content

fix(engine): propagate viewport changes to the live JS context - #133

Open
danperks wants to merge 1 commit into
lucid-softworks:mainfrom
danperks:fix/live-viewport-relayout
Open

fix(engine): propagate viewport changes to the live JS context#133
danperks wants to merge 1 commit into
lucid-softworks:mainfrom
danperks:fix/live-viewport-relayout

Conversation

@danperks

Copy link
Copy Markdown

What & why

set_viewport updated the process-global metrics that seed a future JS context, but never touched
a context that already existed. A loaded page therefore kept the size it started at forever:

  • window.innerWidth / innerHeight / devicePixelRatio stayed frozen at load-time values
  • width-based media queries never re-evaluated, so matchMedia(...).matches went stale
  • no resize event was ever dispatched

Anything 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.

__viewportChanged is added to the browser environment — it refreshes the globals, re-evaluates
live MediaQueryLists and dispatches resize — with Session::notify_viewport_changed to drive it.
This deliberately mirrors the existing notify_color_scheme_changed path rather than inventing a
second mechanism.

Ordering: re-layout happens before dispatch

The part worth reviewing closely. A resize handler exists in order to measure, and
getBoundingClientRect reads the pushed rect table, not the globals set above. Dispatching first
therefore 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_viewport now drops the layout cache, re-runs ensure_layout, and re-pushes the rect table
before 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 clientWidth falls back to innerWidth and so would
pass 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 of 320.

Noted but deliberately not fixed here

clientWidth/clientHeight are never populated for non-root elements: set_layout_rects feeds
getBoundingClientRect but not the __elemMetrics padding-box fields, so a non-root element reads
0. That is a pre-existing gap unrelated to resize, and folding it in would have muddled this
change — happy to open a separate issue or PR for it if useful.

🤖 How this was built

  • Authored primarily with an LLM (which: Claude Opus 5, driven via Cursor)

Checklist

  • PR title is a Conventional Commit
  • cargo test --workspace passes — adds
    resize_relayouts_before_handlers_observe_geometry, which also covers that an unchanged
    viewport does not re-enter JS at all
  • cargo fmt --all + cargo clippy clean for this change (clippy reports some pre-existing
    warnings in wurl and net, untouched here)
  • No conformance regressions — this path is not exercised by the WPT reftest runner, which uses
    a fixed viewport; covered by the unit test above instead

Made with Cursor

`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>
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