Skip to content

Surface the context menu Chromium already builds (OSR) - #34

Merged
wenkaifan0720 merged 3 commits into
mainfrom
feat/osr-context-menu
Aug 7, 2026
Merged

Surface the context menu Chromium already builds (OSR)#34
wenkaifan0720 merged 3 commits into
mainfrom
feat/osr-context-menu

Conversation

@wenkaifan0720

Copy link
Copy Markdown
Collaborator

Right-click did nothing in an OSR tile. Not because the menu was missing — Chromium builds a complete, correctly-stateful one for every right-click (back/forward/reload, cut/copy/paste with the right items greyed out, view-source, copy-link-address, the whole spellcheck block with live dictionary suggestions) — but because no CefContextMenuHandler was implemented, so CEF constructed it and threw it away.

Approach

We can't let CEF display it: the default path is a native menu parented to a window, and a windowless browser has none. So RunContextMenu takes over display only:

  1. Serialise the model Chromium built — labels, command ids, enabled/checked, separators, one level of submenu — plus hit context (coords, link url, media source, selection, misspelled word).
  2. The host draws it as ordinary Flutter UI over the texture.
  3. The chosen command id goes back and Chromium executes it.

That split matters: every command's behaviour and its enabled/checked state stays authoritative instead of being reimplemented. Paste greys out with an empty clipboard because Chromium said so, not because we guessed.

The invariant

Answer exactly once. CEF requires the menu callback be continued or cancelled; a dropped answer wedges the page's menu handling so later right-clicks are silently ignored. Every path answers, with 0 = dismiss:

  • item chosen → that id
  • handler returns null → 0
  • no handler at all → 0
  • handler throws → reported via FlutterError, then 0
  • malformed JSON → reported, then 0

OnContextMenuDismissed drops pending entries, because CEF has already invalidated the callback by then and answering later would be a use-after-free.

Protocol

6 → 7. kOpContextMenu (0x20) up, kOpContextMenuCommand (0x3e) down. Both sides bumped in the same commit — a mismatch is a hard processGone(protocolMismatch), so they can never land separately.

Verification

  • flutter analyze — clean (1 pre-existing unrelated info).
  • flutter test178 pass (173 before + 5 new covering each answer-exactly-once path).
  • Native compiles clean; the example app builds and runs with the new host, so the v7 handshake is confirmed live (host process alive under the example — a mismatch would have killed it).
  • Example app wires a real menu so the seam is demonstrable rather than theoretical.

Not yet verified: an actual interactive right-click producing the menu. That needs a real click in a frontmost window — an agent shell can't do it reliably (OSR reports false freezes when not frontmost). The example app is built and running for exactly that check.

Note for consumers

main.mm changed, so the content hash moved: make publish-cef-host is required for the new hash before any consumer can make pin-cef to this commit.

🤖 Generated with Claude Code

wenkaifan0720 and others added 3 commits August 6, 2026 19:00
Right-click did nothing in an OSR tile. Not because the menu was missing —
Chromium builds a complete, correctly-stateful one for every right-click
(back/forward/reload, cut/copy/paste with the right items greyed out,
view-source, copy-link-address, the whole spellcheck block with live dictionary
suggestions) — but because no CefContextMenuHandler was implemented, so CEF
constructed it and threw it away.

We cannot let CEF display it: the default path is a native menu parented to a
window, and a windowless browser has none. So RunContextMenu takes over display
instead. Serialise the model Chromium built (labels, command ids, enabled /
checked, separators, one level of submenu) plus the hit context (coords, link
url, media source, selection, misspelled word) and hand it to the host to draw;
send the chosen command id back so CHROMIUM executes it. Every command's
behaviour and enabled state stays authoritative rather than reimplemented.

The invariant is answer-exactly-once: CEF requires the menu callback be
continued or cancelled, and a dropped answer wedges the page's menu handling so
later right-clicks are silently ignored. Every path therefore answers —
chosen, dismissed, no handler, throwing handler, malformed json — with 0
meaning dismiss. OnContextMenuDismissed drops pending entries because CEF has
already invalidated the callback by then. 5 tests cover those paths.

Protocol 6 -> 7 (kOpContextMenu 0x20 up, kOpContextMenuCommand 0x3e down); both
sides bumped together, since a mismatch is a hard processGone(protocolMismatch).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Demonstrates the seam end to end: Chromium's model arrives, a plain Material
menu draws it at the click point, the chosen id goes back. Deliberately not
styled — it shows the wiring, not a design. Chromium's enabled state is honored
so Paste greys out with an empty clipboard without the host deriving anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three follow-ups from driving the context menu in a real app.

DevTools inspect point. showDevTools now takes an optional page-DIP point and
passes it to ShowDevTools(inspect_element_at), so a host can open DevTools
already inspecting the right-clicked element — what "Inspect" means in a
browser. Empty payload still means "just open", so the existing call is
unchanged. DevTools is a REAL window even though the page is windowless, which
is why this works at all where view-source does not.

view-source is judged by what it wraps. OnBeforeBrowse refused `view-source:`
outright (not in the allowlist, not `about:`), so Chromium's own View Page
Source silently did nothing — no error, no log. Viewing the source of a page you
were already allowed to LOAD grants no new reach: it renders bytes as text and
executes nothing. So `view-source:X` is allowed iff X's own scheme is allowed.
Verified: `view-source:https://…` renders, `view-source:file:///etc/hosts` is
still refused. Chromium rejects nested `view-source:view-source:` upstream, so
one unwrap is the whole story.

(The context-menu ROW for view-source is still dropped host-side: CEF's default
handler for it wants a new tab, and a windowless browser has no tab strip. The
scheme working is what lets an agent — or a future "open source in a new tile" —
reach it.)

Opcode collision. kOpContextMenu was 0x20, which kOpNavigate already uses. The
two travel in opposite directions so dispatch happened to work, but it was a
trap for the next reader; moved to 0x40.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wenkaifan0720
wenkaifan0720 merged commit 7ca6eea into main Aug 7, 2026
2 checks passed
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