package cef import ( "context" "strings" "sync" "sync/atomic" "time" purecef "github.com/bnema/purego-cef/cef" "github.com/bnema/dumber/internal/application/dto" "github.com/bnema/dumber/internal/application/port" downloadutil "github.com/bnema/dumber/internal/domain/download" "github.com/bnema/dumber/internal/logging" ) // Console message markers used by injected JavaScript for log filtering. const ( consoleMarkerVideoDiag = "[VIDEO-DIAG]" consoleMarkerAutoCopy = "[AUTO-COPY]" consoleMarkerScaleProbe = "[SCALE-PROBE]" ) // handlerSet implements all CEF handler interfaces and dispatches events to the // owning WebView. A single struct is used so that the Client's Get*Handler // methods can return the same receiver, avoiding extra allocations. type handlerSet struct { wv *WebView fileDialogPresenter fileDialogPresenter renderHandlerOnce sync.Once renderHandler purecef.RenderHandler permissions permissionTracker } // Compile-time interface checks. var ( _ purecef.Client = (*handlerSet)(nil) _ purecef.DisplayHandler = (*handlerSet)(nil) _ purecef.LoadHandler = (*handlerSet)(nil) _ purecef.LifeSpanHandler = (*handlerSet)(nil) _ purecef.RequestHandler = (*handlerSet)(nil) _ purecef.AudioHandler = (*handlerSet)(nil) _ purecef.ContextMenuHandler = (*handlerSet)(nil) _ purecef.DialogHandler = (*handlerSet)(nil) _ purecef.DownloadHandler = (*handlerSet)(nil) _ purecef.FindHandler = (*handlerSet)(nil) _ purecef.PermissionHandler = (*handlerSet)(nil) ) // =========================================================================== // Client // =========================================================================== func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { // Always return the audio handler — without it, CEF has no audio output // path and refuses to start media decoding, breaking all video playback. // The handler accepts audio but discards packets (no-op) until a real // audio backend (PipeWire/PulseAudio) is wired up. return h } func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil } func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler { return h } func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return h } func (h *handlerSet) GetDisplayHandler() purecef.DisplayHandler { return h } func (h *handlerSet) GetDownloadHandler() purecef.DownloadHandler { if h.downloadHandler() != nil { return h } return nil } func (h *handlerSet) GetDragHandler() purecef.DragHandler { return nil } func (h *handlerSet) GetFindHandler() purecef.FindHandler { return h } func (h *handlerSet) GetFocusHandler() purecef.FocusHandler { return nil } func (h *handlerSet) GetFrameHandler() purecef.FrameHandler { return nil } func (h *handlerSet) GetPermissionHandler() purecef.PermissionHandler { return h } func (h *handlerSet) GetJsdialogHandler() purecef.JsdialogHandler { return h } func (h *handlerSet) GetKeyboardHandler() purecef.KeyboardHandler { return nil } func (h *handlerSet) GetLifeSpanHandler() purecef.LifeSpanHandler { return h } func (h *handlerSet) GetLoadHandler() purecef.LoadHandler { return h } func (h *handlerSet) GetPrintHandler() purecef.PrintHandler { return nil } func (h *handlerSet) GetRenderHandler() purecef.RenderHandler { if h == nil { return nil } h.renderHandlerOnce.Do(func() { h.renderHandler = newDumberRenderHandler(h.wv) }) return h.renderHandler } func (h *handlerSet) GetRequestHandler() purecef.RequestHandler { return h } func (h *handlerSet) OnProcessMessageReceived( browser purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, message purecef.ProcessMessage, ) int32 { if h == nil || h.wv == nil || h.wv.engine == nil { return 0 } action, payload, ok := decodeRendererBridgeProcessMessage(message) if !ok { return 0 } log := logging.FromContext(h.wv.ctx) log.Debug(). Str("action", action). Int("payload_len", len(payload)). Msg("cef: renderer bridge message received") switch action { case rendererBridgeActionExplicitTextCopy: req, err := decodeRendererBridgeExplicitTextCopyPayload([]byte(payload)) if err != nil { log.Debug().Str("action", action).Msg("cef: invalid explicit copy payload") return 1 } h.wv.engine.handleExplicitClipboardBridgeText(h.wv.id, req.Action, req.Text) case rendererBridgeActionEditableFocusChanged: h.wv.setEditableFocus(payload == "1" || strings.EqualFold(payload, "true")) case rendererBridgeActionFocusSync: h.wv.engine.handleEditableFocusBridge(browser) case rendererBridgeActionPopupOpen: req, err := decodeRendererBridgePopupOpenPayload([]byte(payload)) if err != nil { log.Debug().Str("action", action).Msg("cef: invalid popup_open payload") return 1 } h.wv.handleSyntheticPopupOpen(req.URL, req.FrameName, req.ProxyID, req.UserGesture, req.NoJavaScriptAccess) case rendererBridgeActionPopupNavigate: req, err := decodeRendererBridgePopupNavigatePayload([]byte(payload)) if err != nil { log.Debug().Str("action", action).Msg("cef: invalid popup_navigate payload") return 1 } h.wv.handleSyntheticPopupNavigate(req.ProxyID, req.URL) case rendererBridgeActionPopupClose: req, err := decodeRendererBridgePopupClosePayload([]byte(payload)) if err != nil { log.Debug().Str("action", action).Msg("cef: invalid popup_close payload") return 1 } h.wv.handleSyntheticPopupClose(req.ProxyID) case rendererBridgeActionReady: log.Debug(). Str("frame_url", logging.TruncateURL(payload, logging.PermissionLogURLMaxLen)). Msg("cef: renderer bridge ready") default: log.Debug().Str("action", action).Msg("cef: unknown renderer bridge action") } return 1 } // =========================================================================== // RenderHandler methods live in render_handler_adapter.go. handlerSet keeps // Dumber's non-render CEF handlers and returns a delegating render handler from // GetRenderHandler. // =========================================================================== // =========================================================================== // DisplayHandler (13 methods) // =========================================================================== // OnAddressChange updates the cached URI when the main frame navigates. func (h *handlerSet) OnAddressChange(_ purecef.Browser, frame purecef.Frame, url string) { if frame != nil && frame.IsMain() { if h.wv != nil && h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Debug(). Str("url", logging.TruncateURL(url, logging.PermissionLogURLMaxLen)). Msg("cef: OnAddressChange") } h.wv.updateURI(url) } } // OnTitleChange updates the cached title. func (h *handlerSet) OnTitleChange(_ purecef.Browser, title string) { h.wv.updateTitle(title) } func (h *handlerSet) OnFaviconUrlchange(browser purecef.Browser, iconURLs purecef.StringList) { if h == nil || h.wv == nil { return } h.wv.mu.RLock() pageURL, committedURL, cb, currentBrowser, documentSeq := h.wv.uri, h.wv.committedURL, h.wv.callbacks, h.wv.browser, h.wv.documentSeq h.wv.mu.RUnlock() if cb == nil || cb.OnFaviconURLChanged == nil || browser == nil || currentBrowser == nil || browser.GetIdentifier() != currentBrowser.GetIdentifier() { return } if committedURL == "" { committedURL = pageURL } candidates := resolveFaviconCandidates(committedURL, decodeCEFStringList(iconURLs)) if len(candidates) == 0 { return } h.wv.mu.Lock() if h.wv.documentSeq != documentSeq || h.wv.uri != pageURL { h.wv.mu.Unlock() return } h.wv.faviconEngineDocumentSeq = documentSeq h.wv.mu.Unlock() h.wv.runOnGTK(func() { if !h.faviconCallbackCurrent(cb, browser, documentSeq, pageURL) { return } cb.OnFaviconURLChanged(pageURL, candidates) }) } // faviconCallbackCurrent reports whether a queued favicon callback still // belongs to the current committed document, callback owner, and browser. The // GTK hop outlives the state it was queued for, so callbacks revalidate before // handing candidates to the UI. func (h *handlerSet) faviconCallbackCurrent(cb *port.WebViewCallbacks, browser purecef.Browser, documentSeq uint64, pageURL string) bool { if h == nil || h.wv == nil || cb == nil { return false } h.wv.mu.RLock() currentBrowser := h.wv.browser current := h.wv.callbacks == cb && h.wv.documentSeq == documentSeq && h.wv.uri == pageURL h.wv.mu.RUnlock() if !current { return false } if browser == nil { // No captured browser identity to compare; the document checks above // still bound the callback. return true } return currentBrowser != nil && currentBrowser.GetIdentifier() == browser.GetIdentifier() } // OnFullscreenModeChange toggles the fullscreen atomic and fires callbacks. func (h *handlerSet) OnFullscreenModeChange(_ purecef.Browser, fullscreen int32) { entering := fullscreen != 0 h.wv.fullscreen.Store(entering) h.wv.mu.RLock() cb := h.wv.callbacks h.wv.mu.RUnlock() if cb == nil { return } if entering && cb.OnEnterFullscreen != nil { h.wv.runOnGTK(func() { cb.OnEnterFullscreen() }) } if !entering && cb.OnLeaveFullscreen != nil { h.wv.runOnGTK(func() { cb.OnLeaveFullscreen() }) } } func (h *handlerSet) OnTooltip(_ purecef.Browser, _ uintptr) int32 { return 0 } // OnStatusMessage fires the OnLinkHover callback with the status text // and caches the hover URI for middle-click interception. func (h *handlerSet) OnStatusMessage(_ purecef.Browser, value string) { h.wv.updateHoverURI(value) h.wv.mu.RLock() cb := h.wv.callbacks h.wv.mu.RUnlock() if cb != nil && cb.OnLinkHover != nil { h.wv.runOnGTK(func() { cb.OnLinkHover(value) }) } } func (h *handlerSet) OnConsoleMessage( _ purecef.Browser, level purecef.LogSeverity, message, source string, line int32, ) int32 { if h.wv != nil && h.wv.ctx != nil && (strings.Contains(message, consoleMarkerVideoDiag) || strings.Contains(message, consoleMarkerAutoCopy) || strings.Contains(message, consoleMarkerScaleProbe)) { log := logging.FromContext(h.wv.ctx).With(). Str("component", "cef-console"). Str("source", source). Int32("line", line). Logger() switch level { case purecef.LogSeverityLogseverityError, purecef.LogSeverityLogseverityFatal: log.Error().Str("message", message).Msg("cef: console message") case purecef.LogSeverityLogseverityWarning: log.Warn().Str("message", message).Msg("cef: console message") default: log.Info().Str("message", message).Msg("cef: console message") } } return 0 } func (h *handlerSet) OnAutoResize(_ purecef.Browser, _ *purecef.Size) int32 { return 0 } // OnLoadingProgressChange updates the cached progress value. func (h *handlerSet) OnLoadingProgressChange(_ purecef.Browser, progress float64) { if h.wv != nil && h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Trace(). Float64("progress", progress). Msg("cef: OnLoadingProgressChange") } h.wv.updateProgress(progress) } func (h *handlerSet) OnCursorChange( _ purecef.Browser, _ uintptr, cursorType purecef.CursorType, _ *purecef.CursorInfo, ) int32 { if h == nil || h.wv == nil { return 0 } name := cefCursorToGDKName(cursorType) h.wv.runOnGTK(func() { if h.wv.viewBridge != nil { h.wv.viewBridge.SetCursorFromName(name) } }) return 1 // handled } func (h *handlerSet) OnMediaAccessChange(_ purecef.Browser, _, _ int32) {} func (h *handlerSet) OnContentsBoundsChange(_ purecef.Browser, _ *purecef.Rect) int32 { return 0 } func (h *handlerSet) GetRootWindowScreenRect(_ purecef.Browser, _ *purecef.Rect) int32 { return 0 } // =========================================================================== // LoadHandler (4 methods) // =========================================================================== type faviconSourceVisitorRetention struct { id uint64 visitor purecef.StringVisitor } type faviconSourceVisitor struct { visit func(string) } func (v faviconSourceVisitor) Visit(source string) { if v.visit != nil { v.visit(source) } } // OnLoadingStateChange updates the loading/navigation state cache and fires callbacks. func (h *handlerSet) OnLoadingStateChange(browser purecef.Browser, isloading, cangoback, cangoforward int32) { loading := isloading != 0 h.wv.updateLoadState(loading, cangoback != 0, cangoforward != 0) if h.wv != nil && h.wv.ctx != nil { h.wv.mu.RLock() uri := h.wv.uri progress := h.wv.progress pendingURI := h.wv.pendingURI h.wv.mu.RUnlock() logging.FromContext(h.wv.ctx).Debug(). Bool("loading", loading). Bool("can_go_back", cangoback != 0). Bool("can_go_forward", cangoforward != 0). Float64("progress", progress). Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)). Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)). Msg("cef: OnLoadingStateChange") } if !loading && h.wv.viewBridge != nil { h.wv.runOnGTK(func() { if h.wv.viewBridge != nil && h.wv.viewBridge.HasFocus() { h.wv.mu.RLock() host := h.wv.host h.wv.mu.RUnlock() if host != nil { syncWindowlessBrowserFocus(host) } } }) } h.wv.mu.RLock() cb := h.wv.callbacks pageURL := h.wv.uri committedURL := h.wv.committedURL h.wv.mu.RUnlock() if committedURL == "" { committedURL = pageURL } if !loading && cb != nil && cb.OnFaviconURLChanged != nil { h.emitDiscoveredFaviconCandidates(browser, pageURL, committedURL, cb) } if cb != nil && cb.OnLoadChanged != nil { if loading { h.wv.runOnGTK(func() { cb.OnLoadChanged(port.LoadStarted) }) } else { h.wv.runOnGTK(func() { cb.OnLoadChanged(port.LoadFinished) }) } } } var faviconSourceVisitorRetentionID atomic.Uint64 func (wv *WebView) retainFaviconSourceVisitor(visitor purecef.StringVisitor) func() { if wv == nil || visitor == nil { return func() {} } id := faviconSourceVisitorRetentionID.Add(1) wv.faviconSourceVisitorsMu.Lock() wv.faviconSourceVisitors = append(wv.faviconSourceVisitors, faviconSourceVisitorRetention{id: id, visitor: visitor}) wv.faviconSourceVisitorsMu.Unlock() return func() { wv.faviconSourceVisitorsMu.Lock() defer wv.faviconSourceVisitorsMu.Unlock() for i, candidate := range wv.faviconSourceVisitors { if candidate.id != id { continue } last := len(wv.faviconSourceVisitors) - 1 copy(wv.faviconSourceVisitors[i:], wv.faviconSourceVisitors[i+1:]) wv.faviconSourceVisitors[last] = faviconSourceVisitorRetention{} wv.faviconSourceVisitors = wv.faviconSourceVisitors[:last] return } } } func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pageURL, committedURL string, cb *port.WebViewCallbacks) { if h == nil || h.wv == nil || cb == nil || cb.OnFaviconURLChanged == nil { return } h.wv.mu.Lock() documentSeq := h.wv.documentSeq if h.wv.faviconEngineDocumentSeq == documentSeq || h.wv.faviconSourceCompletedDocumentSeq == documentSeq || h.wv.faviconSourcePendingToken != 0 || documentSeq == 0 { h.wv.mu.Unlock() return } h.wv.faviconSourceRequestSerial++ token := h.wv.faviconSourceRequestSerial h.wv.faviconSourcePendingToken = token h.wv.mu.Unlock() finish := func() { h.wv.mu.Lock() if h.wv.documentSeq == documentSeq && h.wv.faviconSourcePendingToken == token { h.wv.faviconSourcePendingToken = 0 h.wv.faviconSourceCompletedDocumentSeq = documentSeq } h.wv.mu.Unlock() } fallback := fallbackFaviconCandidates(committedURL) emit := func(candidates []string) { if len(candidates) == 0 { return } h.wv.runOnGTK(func() { if !h.faviconCallbackCurrent(cb, browser, documentSeq, pageURL) { return } cb.OnFaviconURLChanged(pageURL, candidates) }) } if browser == nil { finish() emit(fallback) return } frame := browser.GetMainFrame() if frame == nil { finish() emit(fallback) return } var releaseOnce sync.Once var release func() visitor := cefNewStringVisitor(faviconSourceVisitor{visit: func(source string) { releaseOnce.Do(func() { if release != nil { release() } }) finish() if !h.faviconCallbackCurrent(cb, browser, documentSeq, pageURL) { return } h.wv.mu.RLock() engineWon := h.wv.faviconEngineDocumentSeq == documentSeq h.wv.mu.RUnlock() if engineWon { return } candidates := DiscoverFaviconCandidates(committedURL, source) if len(candidates) == 0 { candidates = fallback } emit(candidates) }}) release = h.wv.retainFaviconSourceVisitor(visitor) time.AfterFunc(5*time.Second, func() { releaseOnce.Do(func() { if release != nil { release() } }) finish() }) frame.GetSource(visitor) } // OnLoadStart fires LoadCommitted for main frame navigations. // In CEF this callback runs after navigation commit, so it is the closest // equivalent to WebKit's LoadCommitted event. func (h *handlerSet) OnLoadStart(_ purecef.Browser, frame purecef.Frame, _ purecef.TransitionType) { if frame == nil || !frame.IsMain() { return } // Post-commit fallback for navigation paths that bypass OnBeforeBrowse // (same-page, error, and helper-driven commits). Main frame only. if h.wv != nil { // A new document replaces the page that asked for any open JS dialog. h.wv.cancelJSDialogs() h.wv.invalidateScrollMotion() h.wv.mu.Lock() h.wv.documentSeq++ h.wv.faviconSourcePendingToken = 0 h.wv.faviconSourceCompletedDocumentSeq = 0 h.wv.mu.Unlock() } bridgeNonce := h.wv.ensureBridgeNonce() if openerBridgeScript := h.wv.popupOpenerBridgeScript(bridgeNonce); openerBridgeScript != "" { if h.wv != nil && h.wv.ctx != nil { parentURI, active, blocked := h.wv.popupOpenerBridgeState() logging.FromContext(h.wv.ctx).Debug(). Uint64("webview_id", uint64(h.wv.id)). Str("url", logging.TruncateURL(frame.GetURL(), logging.PermissionLogURLMaxLen)). Str("parent_uri", logging.TruncateURL(parentURI, logging.PermissionLogURLMaxLen)). Bool("bridge_active", active). Bool("bridge_blocked", blocked). Msg("cef: injecting popup opener bridge") } frame.ExecuteJavaScript(openerBridgeScript, frame.GetURL(), 0) } if h.wv != nil && h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Debug(). Str("url", logging.TruncateURL(frame.GetURL(), logging.PermissionLogURLMaxLen)). Msg("cef: OnLoadStart") } h.wv.setEditableFocus(false) h.wv.updateURI(frame.GetURL()) h.wv.mu.RLock() cb := h.wv.callbacks h.wv.mu.RUnlock() if cb != nil && cb.OnLoadChanged != nil { h.wv.runOnGTK(func() { cb.OnLoadChanged(port.LoadCommitted) }) } } // OnLoadEnd resets crash count and injects content scripts for the main frame. func (h *handlerSet) OnLoadEnd(browser purecef.Browser, frame purecef.Frame, httpStatusCode int32) { log := logging.FromContext(h.wv.ctx) log.Debug(). Bool("frame_nil", frame == nil). Bool("is_main", frame != nil && frame.IsMain()). Int32("http_status", httpStatusCode). Msg("cef: OnLoadEnd") if frame == nil || !frame.IsMain() { return } // Successful load — reset the consecutive crash counter. h.wv.crashCount.Store(0) // If a queued startup navigation is still pending after about:blank finished, // replay it now that the initial main-frame load completed. frameURL := frame.GetURL() pendingURI, pendingIntentID := h.wv.pendingNavigationSnapshot() if pendingURI != "" && !pendingURIEquivalent(frameURL, pendingURI) { if strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") { rearmed := h.wv.rearmPendingNavigationAfterBlankLoadEnd(pendingIntentID) log.Info(). Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)). Bool("rearmed", rearmed). Msg("cef: replaying pending navigation after about:blank load end") h.wv.schedulePendingNavigationReplayForIntent(0, pendingIntentID) } } // Inject scripts and styles after page load. // Must run on GTK thread — OnLoadEnd fires on the CEF IO thread, // and JavaScript injection requires the main thread. if cefScaleProbeEnabled() && shouldRunCEFScaleProbe(frameURL, httpStatusCode) { // Do not retain/use CEF callback-scoped frame wrappers across the GTK idle hop. // Re-read the current main frame through WebView.RunJavaScript instead. h.wv.runOnGTK(func() { snapshot := cefScaleProbeSnapshot(h.wv) event := logging.FromContext(h.wv.ctx).Debug() for key, value := range snapshot.logFields() { event = event.Interface(key, value) } event.Msg("cef: injecting scale probe") h.wv.RunJavaScript(context.Background(), cefScaleProbeScript) }) } if h.wv.engine != nil && h.wv.engine.contentInj != nil { // Capture navigation identity for the GTK hop from the callback's // browser: a process swap or a newer navigation can stale this // event before it runs. event := h.wv.captureInjectionEvent(browser, frameURL) h.wv.runOnGTK(func() { h.wv.engine.contentInj.onLoadEndForEvent(h.wv, event) }) } // NOTE: We intentionally do NOT dispatch LoadFinished or ProgressChanged(1.0) // here. OnLoadEnd is a per-frame event, and during cross-site navigations // (process swap), CEF fires OnLoadEnd for the OLD main frame before the new // page finishes loading. Dispatching LoadFinished here caused the progress // bar to hide prematurely while the new page was still loading. // // OnLoadingStateChange(isLoading=false) is the correct browser-level signal // for load completion — CEF guarantees it fires AFTER all OnLoadEnd calls. // CEF's OnLoadingProgressChange also provides progress=1.0 at completion. } const cefErrAborted purecef.Errorcode = -3 // OnLoadError handles failed main-frame navigations that never reach // OnLoadStart. CEF still sends OnLoadingStateChange(false) after load errors, // so this callback only synthesizes the missing committed event needed to reveal // the pane; the normal browser-level loading-state callback remains responsible // for the terminal LoadFinished state. func (h *handlerSet) OnLoadError(_ purecef.Browser, frame purecef.Frame, errorCode purecef.Errorcode, errorText, failedURL string) { if h == nil || h.wv == nil || frame == nil || !frame.IsMain() { return } if errorCode == cefErrAborted { return } if failedURL == "" { failedURL = frame.GetURL() } h.wv.mu.RLock() currentURI := h.wv.uri cb := h.wv.callbacks h.wv.mu.RUnlock() if h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Debug(). Int32("error_code", errorCode). Str("error_text", errorText). Str("failed_url", logging.TruncateURL(failedURL, logging.PermissionLogURLMaxLen)). Msg("cef: OnLoadError") } if failedURL != "" { h.wv.updateURI(failedURL) } if cb == nil || cb.OnLoadChanged == nil { return } if failedURL != "" && !pendingURIEquivalent(currentURI, failedURL) { h.wv.runOnGTK(func() { cb.OnLoadChanged(port.LoadCommitted) }) } } func mapCEFPopupFeatures(in *purecef.PopupFeatures) dto.PopupFeatures { if in == nil { return dto.PopupFeatures{State: dto.PopupFeaturesNone} } state := dto.PopupFeaturesNone if in.Xset != 0 || in.Yset != 0 || in.Widthset != 0 || in.Heightset != 0 || in.Ispopup != 0 { state = dto.PopupFeaturesSpecified } return dto.PopupFeatures{ State: state, X: int(in.X), Y: int(in.Y), Width: int(in.Width), Height: int(in.Height), XSet: in.Xset != 0, YSet: in.Yset != 0, WidthSet: in.Widthset != 0, HeightSet: in.Heightset != 0, IsPopup: in.Ispopup != 0, IsPopupSet: true, } } func mapCEFWindowDisposition(disposition purecef.WindowOpenDisposition) dto.WindowDisposition { switch disposition { case purecef.WindowOpenDispositionWodCurrentTab: return dto.WindowDispositionCurrentTab case purecef.WindowOpenDispositionWodNewPopup: return dto.WindowDispositionNewPopup case purecef.WindowOpenDispositionWodNewWindow: return dto.WindowDispositionNewWindow case purecef.WindowOpenDispositionWodNewForegroundTab, purecef.WindowOpenDispositionWodNewBackgroundTab, purecef.WindowOpenDispositionWodSingletonTab, purecef.WindowOpenDispositionWodSwitchToTab: return dto.WindowDispositionNewTab default: return "" } } // =========================================================================== // LifeSpanHandler (6 methods) // =========================================================================== // OnBeforePopup intercepts popup requests (target="_blank", window.open). // When the coordinator returns a related CEF popup shell we hand that shell's // client back to CEF and allow native popup creation so opener semantics are // preserved. Otherwise we keep blocking and let the coordinator's fallback // pane handle the navigation. func (h *handlerSet) OnBeforePopup( browser purecef.Browser, frame purecef.Frame, popupID int32, targetURL, targetFrameName string, targetDisposition purecef.WindowOpenDisposition, userGesture int32, popupFeatures *purecef.PopupFeatures, windowInfo *purecef.WindowInfo, clientSlot *purecef.RawClientWriteSlot, settings *purecef.BrowserSettings, _ *purecef.DictionaryValue, noJavaScriptAccess *bool, ) bool { if targetURL == "" { return true } h.wv.mu.RLock() cb := h.wv.callbacks h.wv.mu.RUnlock() if cb == nil || cb.OnCreate == nil { return true } requestNoJavaScriptAccess := false if noJavaScriptAccess != nil { requestNoJavaScriptAccess = *noJavaScriptAccess } var popup port.WebView sourceBrowserID := int32(0) if browser != nil { sourceBrowserID = browser.GetIdentifier() } sourceFrameID := "" sourceFrameURL := "" if frame != nil { sourceFrameID = frame.GetIdentifier() sourceFrameURL = frame.GetURL() } dispatchResult := h.wv.runOnGTKSyncLabel("cef.on_before_popup", func() { popup = cb.OnCreate(port.PopupRequest{ Engine: dto.BrowserEngineCEF, PopupFeatures: mapCEFPopupFeatures(popupFeatures), TargetURI: targetURL, FrameName: targetFrameName, IsUserGesture: userGesture != 0, NoJavaScriptAccess: requestNoJavaScriptAccess, ParentViewID: h.wv.id, SourceBrowserID: sourceBrowserID, SourceFrameID: sourceFrameID, SourceFrameURL: sourceFrameURL, TargetDisposition: mapCEFWindowDisposition(targetDisposition), }) }) if !dispatchResult.Completed() { logging.FromContext(h.currentContext()).Warn(). Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). Str("target_frame", targetFrameName). Dur("elapsed", dispatchResult.Elapsed). Str("dispatch_status", string(dispatchResult.Status)). Msg("cef: blocked popup after GTK dispatch did not complete") return true } cefPopup, ok := popup.(*WebView) if !ok || cefPopup == nil { return true } decision := dto.HostDecision{} if carrier, ok := popup.(port.BrowsingContextHostDecisionCapable); ok { if got, hasDecision := carrier.BrowsingContextHostDecision(); hasDecision { decision = got } } return h.handlePopupHostDecision( cefPopup, decision, requestNoJavaScriptAccess, popupID, targetURL, targetDisposition, windowInfo, clientSlot, settings, ) } func (h *handlerSet) handlePopupHostDecision( cefPopup *WebView, decision dto.HostDecision, requestNoJavaScriptAccess bool, popupID int32, targetURL string, targetDisposition purecef.WindowOpenDisposition, windowInfo *purecef.WindowInfo, clientSlot *purecef.RawClientWriteSlot, settings *purecef.BrowserSettings, ) bool { cefPopup.setPopupNoJavaScriptAccess(requestNoJavaScriptAccess) switch decision.Kind { case dto.HostDecisionCreateNativePopup: if cefPopup.prepareNativePopup(popupID, targetURL, windowInfo, clientSlot, settings) { logging.FromContext(h.currentContext()).Debug(). Int32("popup_id", popupID). Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). Msg("cef: native popup armed") return false } fallbackEligible := !decision.RequiresNativeOpener && decision.ReasonCode != dto.HostDecisionReasonAuthNativePopup if fallbackEligible { // The Phase 3 abort callback may synchronously detach and adopt this // shell. Arm direct creation first so the adopted shell can create a // browser and retain the synthetic opener bridge when allowed. cefPopup.preparePopupShellDirectBrowserCreation() } abortInvoked := cefPopup.abortNativePopupHost() if !abortInvoked { cefPopup.Destroy() } logging.FromContext(h.currentContext()).Warn(). Str("engine", string(dto.BrowserEngineCEF)). Str("source_host", string(decision.SourceHost)). Str("decision", string(decision.Kind)). Str("target_disposition", string(mapCEFWindowDisposition(targetDisposition))). Str("reason_code", string(dto.BrowsingContextFailureNativeArm)). Int32("popup_id", popupID). Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). Bool("fallback_eligible", fallbackEligible). Bool("host_abort_invoked", abortInvoked). Msg("cef: native popup arming failed") return true case dto.HostDecisionCreateBrowserWindow, dto.HostDecisionCreatePane, dto.HostDecisionReuseNamedPane: // CEF's native popup is blocked. A related shell adopted by a Dumber // host must create its browser directly; noopener controls whether the // synthetic opener/postMessage bridge is installed. directCreationPrepared := cefPopup.preparePopupShellDirectBrowserCreation() if !directCreationPrepared && cefPopup.isNativePopupCandidate() { // Clear only a still-unprepared candidate; an already-started fallback // owns its opener state and must remain intact. cefPopup.discardNativePopupCandidate() } logging.FromContext(h.currentContext()).Debug(). Int32("popup_id", popupID). Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). Str("decision", string(decision.Kind)). Bool("direct_creation_prepared", directCreationPrepared). Msg("cef: blocking native popup for related shell browser creation") return true case dto.HostDecisionAwaitPopupFeatures, dto.HostDecisionDeny: cefPopup.Destroy() logging.FromContext(h.currentContext()).Warn(). Int32("popup_id", popupID). Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). Str("decision", string(decision.Kind)). Msg("cef: denied popup host decision") return true default: if !cefPopup.preparePopupShellDirectBrowserCreation() { cefPopup.discardNativePopupCandidate() } logging.FromContext(h.currentContext()).Debug(). Int32("popup_id", popupID). Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). Msg("cef: missing host decision, blocking native popup and using popup shell browser creation") return true } } func (h *handlerSet) OnBeforePopupAborted(_ purecef.Browser, popupID int32) { if popup := h.wv.takePendingNativePopup(popupID); popup != nil { popup.handleNativePopupAborted() } } func (h *handlerSet) OnBeforeDevToolsPopup( _ purecef.Browser, _ *purecef.WindowInfo, _ *purecef.RawClientWriteSlot, _ *purecef.BrowserSettings, _ *purecef.DictionaryValue, _ *bool, ) { } // OnAfterCreated stores the browser and host references and enables input. func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { log := logging.FromContext(h.wv.ctx) if browser == nil { log.Warn().Msg("cef: OnAfterCreated called with nil browser") if h.wv.engine != nil { // No browser will ever complete this creation: resolve the // pending count so quiescence cannot strand on it. h.wv.engine.activity.NoteCreationResolved() } return } browserID := browser.GetIdentifier() log.Debug().Int32("browser_id", browserID).Msg("cef: OnAfterCreated") host := browser.GetHost() if host == nil { log.Warn().Msg("cef: OnAfterCreated returned nil host") if h.wv.engine != nil { // No view will ever attach to this browser: resolve the // pending count so quiescence cannot strand on it. h.wv.engine.activity.NoteCreationResolved() } return } state := h.attachAfterCreatedBrowser(browser, host, browserID) if state.closeDuplicate { log.Warn(). Int32("browser_id", browserID). Int32("existing_browser_id", state.duplicateBrowserID). Msg("cef: duplicate popup browser attached after shell already had a browser; closing duplicate") if h.wv.engine != nil { h.wv.engine.activity.NoteCreationResolved() } host.CloseBrowser(1) return } h.finishAfterCreated(browser, host, state) } func (h *handlerSet) attachAfterCreatedBrowser( browser purecef.Browser, host purecef.BrowserHost, browserID int32, ) afterCreatedState { state := afterCreatedState{} h.wv.mu.Lock() if existing := h.wv.browser; existing != nil { existingID := existing.GetIdentifier() if existingID != 0 && existingID != browserID { state.closeDuplicate = true state.duplicateBrowserID = existingID h.wv.mu.Unlock() return state } } h.wv.browser = browser h.wv.host = host h.wv.clearAppliedZoomCompensation() h.wv.pendingCreate = nil bridge := h.wv.viewBridge h.wv.inputAttached = bridge == nil state.inputAttached = h.wv.inputAttached if h.wv.findCtrl != nil { h.wv.findCtrl.setHost(host) } state.nativePopupParent = h.wv.nativePopupParent state.nativePopupID = h.wv.nativePopupID h.wv.nativePopupParent = nil h.wv.nativePopupID = 0 h.wv.nativePopupFallbackStarted = false state.hasPendingNavigation = strings.TrimSpace(h.wv.pendingURI) != "" h.wv.mu.Unlock() if bridge != nil { wv := h.wv log := logging.FromContext(wv.ctx) log.Debug(). Uint64("webview_id", uint64(wv.id)). Int32("browser_id", browserID). Bool("destroyed", wv.destroyed.Load()). Bool("bridge_present", true). Msg("cef: scheduling input bridge attachment") wv.runOnGTK(func() { currentBridge := wv.viewBridge log.Debug(). Uint64("webview_id", uint64(wv.id)). Int32("browser_id", browserID). Bool("destroyed", wv.destroyed.Load()). Bool("bridge_present", currentBridge != nil). Bool("bridge_stale", currentBridge != bridge). Msg("cef: starting input bridge attachment") if wv.destroyed.Load() || currentBridge == nil || currentBridge != bridge || bridge.IsDestroyed() { wv.handleInputAttachFailure(ErrAdapterDestroyed, host) return } if err := bridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil { wv.handleInputAttachFailure(err, host) return } bridgeFocused := bridge.HasFocus() if bridgeFocused { syncWindowlessBrowserFocus(host) } else { host.Invalidate(purecef.PaintElementTypePetView) } wv.markInputAttached() log.Debug(). Uint64("webview_id", uint64(wv.id)). Int32("browser_id", browserID). Bool("destroyed", wv.destroyed.Load()). Bool("bridge_present", wv.viewBridge != nil). Bool("bridge_focused", bridgeFocused). Msg("cef: input bridge attachment succeeded") }) } // CEF OSR starts hidden. SyncViewport reads GTK's mapped, ancestor-aware // effective state on the main context and reports that truthful initial state. if h.wv != nil { if h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Info(). Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()). Msg("cef: browser initial/configured windowless frame rate") } h.wv.SyncViewport(h.wv.ctx, "after-created") } return state } func (wv *WebView) handleInputAttachFailure(err error, host purecef.BrowserHost) { if wv == nil || err == nil { return } if wv.ctx != nil { browserID := int32(0) if host != nil { if browser := host.GetBrowser(); browser != nil { browserID = browser.GetIdentifier() } } logging.FromContext(wv.ctx).Warn().Err(err). Uint64("webview_id", uint64(wv.id)). Int32("browser_id", browserID). Bool("destroyed", wv.destroyed.Load()). Bool("bridge_present", wv.viewBridge != nil). Msg("cef: input bridge attachment failed") } if host != nil && !wv.destroyed.Load() { host.CloseBrowser(1) return } wv.runCloseCallbacks() } func (h *handlerSet) finishAfterCreated( browser purecef.Browser, host purecef.BrowserHost, state afterCreatedState, ) { if h.wv.engine != nil { h.wv.engine.recordBrowserAfterCreated(browser) h.wv.engine.registerWebView(h.wv) h.wv.engine.bindBrowserWebView(browser, h.wv) } h.wv.stopNativePopupFallbackTimer() if state.inputAttached { // Request an initial OSR frame even before the first real navigation // commits. This restores the about:blank warm-up paint that the stable // startup path relied on and prevents the rendering bridge from staying idle. host.Invalidate(purecef.PaintElementTypePetView) } if state.hasPendingNavigation { h.wv.schedulePendingNavigationReplay() } if state.nativePopupParent != nil && state.nativePopupID != 0 { state.nativePopupParent.clearPendingNativePopup(state.nativePopupID, h.wv) } h.wv.scheduleStartAdaptiveFrameRatePolling() h.wv.scheduleStartBeginFrameLoop() h.wv.applyAccessibilityIfReady() h.wv.fireReadyToShow() } type afterCreatedState struct { inputAttached bool hasPendingNavigation bool nativePopupParent *WebView nativePopupID int32 duplicateBrowserID int32 closeDuplicate bool } // DoClose returns false to allow the default close behavior. func (h *handlerSet) DoClose(_ purecef.Browser) bool { return false } // OnBeforeClose fires the OnClose callback. func (h *handlerSet) OnBeforeClose(browser purecef.Browser) { h.denyPendingPermissions() if h.wv.engine != nil { browserID := int32(0) if browser != nil { browserID = browser.GetIdentifier() } h.wv.engine.unregisterWebView(h.wv, browserID) } h.wv.mu.Lock() h.wv.browser = nil h.wv.host = nil h.wv.clearAppliedZoomCompensation() h.wv.inputAttached = false if h.wv.findCtrl != nil { h.wv.findCtrl.setHost(nil) } h.wv.mu.Unlock() if h.wv.destroyed.Load() { h.wv.destroyViewBridgeOnGTKAsync() } else if h.wv.viewBridge != nil { wv := h.wv wv.runOnGTK(func() { if wv.viewBridge == nil { return } if err := wv.viewBridge.SetInputHost(nil); err != nil && wv.ctx != nil { logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to clear cef2gtk input host") } }) } h.wv.scheduleStopAdaptiveFrameRatePolling() h.wv.scheduleStopBeginFrameLoop() h.wv.mu.RLock() cb := h.wv.callbacks h.wv.mu.RUnlock() if cb != nil && cb.OnClose != nil { h.wv.runOnGTK(func() { cb.OnClose() }) } h.wv.runCloseCallbacks() } // =========================================================================== // RequestHandler (11 methods) // =========================================================================== func (h *handlerSet) OnBeforeBrowse(browser purecef.Browser, frame purecef.Frame, request purecef.Request, _, _ int32) bool { if frame == nil || !frame.IsMain() || request == nil { return false } // Pre-commit invalidation for every main-frame navigation. The allow / // cancel decision below is unchanged; subframe navigations never reach // this point and do not cancel the main page's gesture. if h.wv != nil { h.wv.invalidateScrollMotion() } handler := h.downloadHandler() if handler == nil || !strings.EqualFold(request.GetMethod(), "GET") { return false } url := request.GetURL() if !downloadutil.ShouldForceDownloadForURI(url) || browser == nil { return false } host := browser.GetHost() if host == nil { return false } logging.FromContext(h.currentContext()).Debug(). Str("url", logging.TruncateURL(url, maxSchemeTruncatedURLLength)). Msg("cef: forcing download for navigation") host.StartDownload(url) return true } func (h *handlerSet) CanDownload(browser purecef.Browser, url, requestMethod string) bool { handler := h.downloadHandler() if handler == nil { return false } return handler.canDownload(browser, url, requestMethod) } func (h *handlerSet) OnBeforeDownload( browser purecef.Browser, downloadItem purecef.DownloadItem, suggestedName string, callback purecef.BeforeDownloadCallback, ) bool { handler := h.downloadHandler() if handler == nil { return false } return handler.onBeforeDownload(h.currentContext(), browser, downloadItem, suggestedName, callback) } func (h *handlerSet) OnDownloadUpdated( browser purecef.Browser, downloadItem purecef.DownloadItem, callback purecef.DownloadItemCallback, ) { handler := h.downloadHandler() if handler == nil { return } handler.onDownloadUpdated(h.currentContext(), browser, downloadItem, callback) } func (h *handlerSet) downloadHandler() *downloadHandler { if h == nil || h.wv == nil || h.wv.engine == nil { return nil } return h.wv.engine.currentDownloadHandler() } func (h *handlerSet) currentContext() context.Context { if h == nil || h.wv == nil || h.wv.ctx == nil { return context.Background() } return h.wv.ctx } func (h *handlerSet) OnOpenUrlfromTab( _ purecef.Browser, _ purecef.Frame, _ string, _ purecef.WindowOpenDisposition, _ int32, ) int32 { return 0 } func (h *handlerSet) GetResourceRequestHandler( _ purecef.Browser, _ purecef.Frame, _ purecef.Request, _, _ int32, _ string, _ *int32, ) purecef.ResourceRequestHandler { return nil } func (h *handlerSet) GetAuthCredentials( _ purecef.Browser, _ string, _ int32, _ string, _ int32, _, _ string, _ purecef.AuthCallback, ) int32 { return 0 } func (h *handlerSet) OnCertificateError( _ purecef.Browser, _ purecef.Errorcode, _ string, _ purecef.Sslinfo, _ purecef.Callback, ) int32 { return 0 } func (h *handlerSet) OnSelectClientCertificate( _ purecef.Browser, _ int32, _ string, _ int32, _ []purecef.X509Certificate, _ purecef.SelectClientCertificateCallback, ) int32 { return 0 } func (h *handlerSet) OnRenderViewReady(_ purecef.Browser) {} func (h *handlerSet) OnRenderProcessUnresponsive(_ purecef.Browser, _ purecef.UnresponsiveProcessCallback) int32 { return 0 } func (h *handlerSet) OnRenderProcessResponsive(_ purecef.Browser) {} // maxConsecutiveCrashes caps how many times OnRenderProcessTerminated will // forward the event before suppressing further notifications. This prevents // infinite crash → redirect → crash loops. const maxConsecutiveCrashes = 3 // OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason. func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) { // CEF never resets dialog state on a renderer crash; don't leave the UI // (and the dead page's callback) hanging. h.wv.cancelJSDialogs() if h.wv.crashCount.Add(1) > maxConsecutiveCrashes { return // suppress to break the loop } var reason port.WebProcessTerminationReason var label string switch status { case purecef.TerminationStatusTsAbnormalTermination: reason = port.WebProcessTerminationCrashed label = "abnormal termination" case purecef.TerminationStatusTsProcessWasKilled: reason = port.WebProcessTerminationByAPI label = "killed" case purecef.TerminationStatusTsProcessCrashed: reason = port.WebProcessTerminationCrashed label = "crashed" case purecef.TerminationStatusTsProcessOom: reason = port.WebProcessTerminationExceededMemory label = "out of memory" default: reason = port.WebProcessTerminationUnknown label = "unknown" } h.wv.mu.RLock() cb := h.wv.callbacks uri := h.wv.uri h.wv.mu.RUnlock() if cb != nil && cb.OnWebProcessTerminated != nil { h.wv.runOnGTK(func() { cb.OnWebProcessTerminated(reason, label, uri) }) } } func (h *handlerSet) OnDocumentAvailableInMainFrame(_ purecef.Browser) {} // =========================================================================== // AudioHandler (5 methods) // =========================================================================== func (h *handlerSet) GetAudioParameters(_ purecef.Browser, _ *purecef.AudioParameters) int32 { return 1 // proceed with defaults } // OnAudioStreamStarted handles the start of an audio stream. // The third callback argument is the channel count (not frames-per-buffer); // frames-per-buffer comes from params.FramesPerBuffer. func (h *handlerSet) OnAudioStreamStarted(_ purecef.Browser, params *purecef.AudioParameters, channels int32) { // Validate params and factory first if params == nil || h.wv.audioOutputFactory == nil { return } // Build format from CEF parameters format := h.buildAudioStreamFormat(params, channels) if h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Info(). Int("sample_rate", format.SampleRate). Int("channels", format.ChannelCount). Int("frames_per_buffer", format.FramesPerBuffer). Int32("channel_layout", params.ChannelLayout). Msg("cef: OnAudioStreamStarted") } // Starting a stream invalidates both a previous publication and any older // creation already in flight. Close detached streams outside the mutex. generation, previous, accepted := h.wv.beginAudioStreamStart() if !accepted { return } h.wv.closeDetachedAudioStream(previous) h.wv.audioPacketCount.Store(0) h.wv.audioWriteCount.Store(0) ctx := h.wv.ctx if ctx == nil { ctx = context.Background() } stream, err := h.wv.audioOutputFactory.NewStream(ctx, format) if err != nil { if h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Warn().Err(err). Int("sample_rate", format.SampleRate).Int("channels", format.ChannelCount). Msg("cef: failed to create audio output stream") } return } replaced, published := h.wv.publishAudioStream(generation, stream) if !published { // Stop, Destroy, or a newer Start won the race. This callback owns the // unpublishable stream and closes it exactly once. h.wv.closeDetachedAudioStream(&audioStreamHandle{stream: stream, generation: generation}) return } h.wv.closeDetachedAudioStream(replaced) if h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Info(). Int("sample_rate", format.SampleRate).Int("channels", format.ChannelCount). Int("frames_per_buffer", format.FramesPerBuffer). Msg("cef: audio output stream created successfully") } } // buildAudioStreamFormat converts CEF AudioParameters to port.AudioStreamFormat. // channels is the authoritative channel count from the callback; params.FramesPerBuffer // supplies the buffer size. func (h *handlerSet) buildAudioStreamFormat(params *purecef.AudioParameters, channels int32) port.AudioStreamFormat { return port.AudioStreamFormat{ SampleRate: int(params.SampleRate), ChannelCount: int(channels), FramesPerBuffer: int(params.FramesPerBuffer), } } // OnAudioStreamPacket is a real-time callback: it atomically snapshots an // immutable handle and performs only allocation-free atomic diagnostics. It // never takes a lifecycle mutex or emits synchronous logs. func (h *handlerSet) OnAudioStreamPacket(_ purecef.Browser, data [][]float32, frames int32, _ int64) { if len(data) == 0 || frames <= 0 { return } handle := h.wv.audioStream.Load() if handle == nil || handle.stream == nil { return } h.wv.audioPacketCount.Add(1) if handle.stream.Write(data) == nil { h.wv.audioWriteCount.Add(1) } } // OnAudioStreamStopped handles stream stop. func (h *handlerSet) OnAudioStreamStopped(_ purecef.Browser) { if h.wv.ctx != nil { logging.FromContext(h.wv.ctx).Info(). Uint64("packets_received", h.wv.audioPacketCount.Load()). Uint64("writes_succeeded", h.wv.audioWriteCount.Load()). Msg("cef: OnAudioStreamStopped") } h.wv.setAudioPlaying(false) h.wv.closeAudioStream() } // OnAudioStreamError handles stream errors. func (h *handlerSet) OnAudioStreamError(_ purecef.Browser, message string) { if h.wv.ctx != nil { event := logging.FromContext(h.wv.ctx).Warn() logMsg := "cef: audio stream error" if h.wv.destroyed.Load() && message == "Socket closed unexpectedly" { event = logging.FromContext(h.wv.ctx).Debug() logMsg = "cef: audio stream closed during shutdown" } event. Str("error", message). Uint64("packets_received", h.wv.audioPacketCount.Load()). Uint64("writes_succeeded", h.wv.audioWriteCount.Load()). Msg(logMsg) } h.wv.setAudioPlaying(false) h.wv.closeAudioStream() } // =========================================================================== // FindHandler (1 method) // =========================================================================== // OnFindResult dispatches CEF find results to the WebView's FindController. func (h *handlerSet) OnFindResult( _ purecef.Browser, identifier, count int32, _ *purecef.Rect, activematchordinal, finalupdate int32, ) { if h.wv.findCtrl != nil { h.wv.findCtrl.handleFindResult(identifier, count, activematchordinal, finalupdate) } } // cefCursorNames maps CEF cursor types to GDK/CSS cursor names. var cefCursorNames = map[purecef.CursorType]string{ purecef.CursorTypeCtPointer: "default", purecef.CursorTypeCtCross: "crosshair", purecef.CursorTypeCtHand: "pointer", purecef.CursorTypeCtIbeam: "text", purecef.CursorTypeCtWait: "wait", purecef.CursorTypeCtHelp: "help", purecef.CursorTypeCtEastresize: "e-resize", purecef.CursorTypeCtNorthresize: "n-resize", purecef.CursorTypeCtNortheastresize: "ne-resize", purecef.CursorTypeCtNorthwestresize: "nw-resize", purecef.CursorTypeCtSouthresize: "s-resize", purecef.CursorTypeCtSoutheastresize: "se-resize", purecef.CursorTypeCtSouthwestresize: "sw-resize", purecef.CursorTypeCtWestresize: "w-resize", purecef.CursorTypeCtNorthsouthresize: "ns-resize", purecef.CursorTypeCtEastwestresize: "ew-resize", purecef.CursorTypeCtNortheastsouthwestresize: "nesw-resize", purecef.CursorTypeCtNorthwestsoutheastresize: "nwse-resize", purecef.CursorTypeCtColumnresize: "col-resize", purecef.CursorTypeCtRowresize: "row-resize", purecef.CursorTypeCtMove: "move", purecef.CursorTypeCtProgress: "progress", purecef.CursorTypeCtNodrop: "no-drop", purecef.CursorTypeCtNotallowed: "not-allowed", purecef.CursorTypeCtGrab: "grab", purecef.CursorTypeCtGrabbing: "grabbing", purecef.CursorTypeCtZoomin: "zoom-in", purecef.CursorTypeCtZoomout: "zoom-out", purecef.CursorTypeCtNone: "none", } // cefCursorToGDKName maps a CEF cursor type to a GDK/CSS cursor name. func cefCursorToGDKName(ct purecef.CursorType) string { if name, ok := cefCursorNames[ct]; ok { return name } return "default" }