@@ -52,6 +52,7 @@ final class CefWebSession: NSObject, FlutterTexture {
5252 // cef_host -> us: a page called getUserMedia and the site has no remembered
5353 // decision, so the host must show a permission prompt. {u32 id}{u32 mask}{utf8 origin}
5454 private static let opMediaRequest : UInt8 = 0x1e
55+ private static let opContextMenu : UInt8 = 0x40
5556 // cef_host -> us: {u8 videoActive}{u8 audioActive}{u8 setting 0=ask 1=allow}
5657 private static let opMediaState : UInt8 = 0x1f
5758 private static let opNavigate : UInt8 = 0x20
@@ -79,6 +80,7 @@ final class CefWebSession: NSObject, FlutterTexture {
7980 // us -> cef_host: answer a permission prompt {u32 id}{u8 allow}{u8 remember};
8081 // remembered per-origin only when a human chose, exactly like a browser.
8182 private static let opMediaResponse : UInt8 = 0x3c
83+ private static let opContextMenuCommand : UInt8 = 0x3e
8284 // us -> cef_host: {u8 0=ask 1=allow 2=block} rewrite this site's remembered
8385 // camera/mic decision (the URL-bar "site settings" path). No reload.
8486 private static let opSetMediaSetting : UInt8 = 0x3d
@@ -106,6 +108,9 @@ final class CefWebSession: NSObject, FlutterTexture {
106108 var onImeBounds : ( ( Int , Int , Int , Int ) -> Void ) ? // caret rect x,y,w,h (DIP)
107109 var onCookies : ( ( Int , String ) -> Void ) ? // request id, json array
108110 var onMediaRequest : ( ( Int , Int , String ) -> Void ) ? // id, permission mask, origin
111+ /// Right-click in the page. `json` carries Chromium's own menu model + hit
112+ /// context; the Flutter side draws it and answers with `chooseContextMenu`.
113+ var onContextMenu : ( ( Int , String ) -> Void ) ? // id, json
109114 var onMediaState : ( ( Bool , Bool , Int ) -> Void ) ? // videoActive, audioActive, setting
110115 // Fired when the backing IOSurface is (re)allocated — at create and on every
111116 // resize() (which reallocs). Args are the live global surface id and the
@@ -483,6 +488,16 @@ final class CefWebSession: NSObject, FlutterTexture {
483488 sendFrame ( Self . opMediaResponse, p)
484489 }
485490
491+ /// Answer a context menu. `commandId` 0 means dismissed without choosing —
492+ /// which must still be sent: CEF requires the menu callback be answered
493+ /// exactly once, and skipping it wedges the page's menu handling.
494+ func chooseContextMenu( id: Int , commandId: Int ) {
495+ var p = [ UInt8] ( )
496+ appendU32 ( & p, UInt32 ( truncatingIfNeeded: id) )
497+ appendU32 ( & p, UInt32 ( truncatingIfNeeded: commandId) )
498+ sendFrame ( Self . opContextMenuCommand, p)
499+ }
500+
486501 /// Rewrite this site's remembered camera/mic decision (0 = ask again, 1 =
487502 /// allow, 2 = block). No reload — it applies next time the page asks.
488503 func setMediaSetting( _ value: Int ) {
@@ -555,7 +570,18 @@ final class CefWebSession: NSObject, FlutterTexture {
555570 sendFrame ( Self . opDeleteCookie, Array ( ( url + " \u{0} " + name) . utf8) )
556571 }
557572
558- func showDevTools( ) { sendFrame ( Self . opShowDevTools) }
573+ /// Open DevTools. With a point (page DIP coords) it opens INSPECTING the
574+ /// element there — the right-click "Inspect" path.
575+ func showDevTools( inspectAt: ( x: Int , y: Int ) ? = nil ) {
576+ guard let at = inspectAt else {
577+ sendFrame ( Self . opShowDevTools)
578+ return
579+ }
580+ var p = [ UInt8] ( )
581+ appendU32 ( & p, UInt32 ( truncatingIfNeeded: max ( 0 , at. x) ) )
582+ appendU32 ( & p, UInt32 ( truncatingIfNeeded: max ( 0 , at. y) ) )
583+ sendFrame ( Self . opShowDevTools, p)
584+ }
559585
560586 func imeSetComposition( _ text: String ) {
561587 sendFrame ( Self . opImeSetComp, Array ( text. utf8) )
@@ -824,6 +850,13 @@ final class CefWebSession: NSObject, FlutterTexture {
824850 : " "
825851 onMediaRequest ? ( readU32 ( payload, 0 ) , readU32 ( payload, 4 ) , origin)
826852 }
853+ case Self . opContextMenu:
854+ if payload. count >= 4 {
855+ let json = payload. count > 4
856+ ? ( String ( bytes: payload [ 4 ... ] , encoding: . utf8) ?? " {} " )
857+ : " {} "
858+ onContextMenu ? ( readU32 ( payload, 0 ) , json)
859+ }
827860 case Self . opMediaState:
828861 if payload. count >= 3 {
829862 onMediaState ? ( payload [ 0 ] != 0 , payload [ 1 ] != 0 , Int ( payload [ 2 ] ) )
0 commit comments