1package cef
2
3import (
4 "context"
5 "strings"
6 "sync"
7 "sync/atomic"
8 "time"
9
10 purecef "github.com/bnema/purego-cef/cef"
11
12 "github.com/bnema/dumber/internal/application/dto"
13 "github.com/bnema/dumber/internal/application/port"
14 downloadutil "github.com/bnema/dumber/internal/domain/download"
15 "github.com/bnema/dumber/internal/logging"
16)
17
18// Console message markers used by injected JavaScript for log filtering.
19const (
20 consoleMarkerVideoDiag = "[VIDEO-DIAG]"
21 consoleMarkerAutoCopy = "[AUTO-COPY]"
22 consoleMarkerScaleProbe = "[SCALE-PROBE]"
23)
24
25// handlerSet implements all CEF handler interfaces and dispatches events to the
26// owning WebView. A single struct is used so that the Client's Get*Handler
27// methods can return the same receiver, avoiding extra allocations.
28type handlerSet struct {
29 wv *WebView
30 fileDialogPresenter fileDialogPresenter
31 renderHandlerOnce sync.Once
32 renderHandler purecef.RenderHandler
33}
34
35// Compile-time interface checks.
36var (
37 _ purecef.Client = (*handlerSet)(nil)
38 _ purecef.DisplayHandler = (*handlerSet)(nil)
39 _ purecef.LoadHandler = (*handlerSet)(nil)
40 _ purecef.LifeSpanHandler = (*handlerSet)(nil)
41 _ purecef.RequestHandler = (*handlerSet)(nil)
42 _ purecef.AudioHandler = (*handlerSet)(nil)
43 _ purecef.ContextMenuHandler = (*handlerSet)(nil)
44 _ purecef.DialogHandler = (*handlerSet)(nil)
45 _ purecef.DownloadHandler = (*handlerSet)(nil)
46 _ purecef.FindHandler = (*handlerSet)(nil)
47)
48
49// ===========================================================================
50// Client
51// ===========================================================================
52
53func (h *handlerSet) GetAudioHandler() purecef.AudioHandler {
54 // Always return the audio handler — without it, CEF has no audio output
55 // path and refuses to start media decoding, breaking all video playback.
56 // The handler accepts audio but discards packets (no-op) until a real
57 // audio backend (PipeWire/PulseAudio) is wired up.
58 return h
59}
60func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil }
61func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler { return h }
62func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return h }
63func (h *handlerSet) GetDisplayHandler() purecef.DisplayHandler { return h }
64func (h *handlerSet) GetDownloadHandler() purecef.DownloadHandler {
65 if h.downloadHandler() != nil {
66 return h
67 }
68 return nil
69}
70func (h *handlerSet) GetDragHandler() purecef.DragHandler { return nil }
71func (h *handlerSet) GetFindHandler() purecef.FindHandler { return h }
72func (h *handlerSet) GetFocusHandler() purecef.FocusHandler { return nil }
73func (h *handlerSet) GetFrameHandler() purecef.FrameHandler { return nil }
74func (h *handlerSet) GetPermissionHandler() purecef.PermissionHandler { return nil }
75func (h *handlerSet) GetJsdialogHandler() purecef.JsdialogHandler { return nil }
76func (h *handlerSet) GetKeyboardHandler() purecef.KeyboardHandler { return nil }
77func (h *handlerSet) GetLifeSpanHandler() purecef.LifeSpanHandler { return h }
78func (h *handlerSet) GetLoadHandler() purecef.LoadHandler { return h }
79func (h *handlerSet) GetPrintHandler() purecef.PrintHandler { return nil }
80func (h *handlerSet) GetRenderHandler() purecef.RenderHandler {
81 if h == nil {
82 return nil
83 }
84 h.renderHandlerOnce.Do(func() {
85 h.renderHandler = newDumberRenderHandler(h.wv)
86 })
87 return h.renderHandler
88}
89func (h *handlerSet) GetRequestHandler() purecef.RequestHandler { return h }
90
91func (h *handlerSet) OnProcessMessageReceived(
92 browser purecef.Browser,
93 _ purecef.Frame,
94 _ purecef.ProcessID,
95 message purecef.ProcessMessage,
96) int32 {
97 if h == nil || h.wv == nil || h.wv.engine == nil {
98 return 0
99 }
100 action, payload, ok := decodeRendererBridgeProcessMessage(message)
101 if !ok {
102 return 0
103 }
104
105 log := logging.FromContext(h.wv.ctx)
106 log.Debug().
107 Str("action", action).
108 Int("payload_len", len(payload)).
109 Msg("cef: renderer bridge message received")
110
111 switch action {
112 case rendererBridgeActionExplicitTextCopy:
113 req, err := decodeRendererBridgeExplicitTextCopyPayload([]byte(payload))
114 if err != nil {
115 log.Debug().Str("action", action).Msg("cef: invalid explicit copy payload")
116 return 1
117 }
118 h.wv.engine.handleExplicitClipboardBridgeText(h.wv.id, req.Action, req.Text)
119 case rendererBridgeActionEditableFocusChanged:
120 h.wv.setEditableFocus(payload == "1" || strings.EqualFold(payload, "true"))
121 case rendererBridgeActionFocusSync:
122 h.wv.engine.handleEditableFocusBridge(browser)
123 case rendererBridgeActionPopupOpen:
124 req, err := decodeRendererBridgePopupOpenPayload([]byte(payload))
125 if err != nil {
126 log.Debug().Str("action", action).Msg("cef: invalid popup_open payload")
127 return 1
128 }
129 h.wv.handleSyntheticPopupOpen(req.URL, req.FrameName, req.ProxyID, req.UserGesture, req.NoJavaScriptAccess)
130 case rendererBridgeActionPopupNavigate:
131 req, err := decodeRendererBridgePopupNavigatePayload([]byte(payload))
132 if err != nil {
133 log.Debug().Str("action", action).Msg("cef: invalid popup_navigate payload")
134 return 1
135 }
136 h.wv.handleSyntheticPopupNavigate(req.ProxyID, req.URL)
137 case rendererBridgeActionPopupClose:
138 req, err := decodeRendererBridgePopupClosePayload([]byte(payload))
139 if err != nil {
140 log.Debug().Str("action", action).Msg("cef: invalid popup_close payload")
141 return 1
142 }
143 h.wv.handleSyntheticPopupClose(req.ProxyID)
144 case rendererBridgeActionReady:
145 log.Debug().
146 Str("frame_url", logging.TruncateURL(payload, logging.PermissionLogURLMaxLen)).
147 Msg("cef: renderer bridge ready")
148 default:
149 log.Debug().Str("action", action).Msg("cef: unknown renderer bridge action")
150 }
151 return 1
152}
153
154// ===========================================================================
155// RenderHandler methods live in render_handler_adapter.go. handlerSet keeps
156// Dumber's non-render CEF handlers and returns a delegating render handler from
157// GetRenderHandler.
158// ===========================================================================
159
160// ===========================================================================
161// DisplayHandler (13 methods)
162// ===========================================================================
163
164// OnAddressChange updates the cached URI when the main frame navigates.
165func (h *handlerSet) OnAddressChange(_ purecef.Browser, frame purecef.Frame, url string) {
166 if frame != nil && frame.IsMain() {
167 if h.wv != nil && h.wv.ctx != nil {
168 logging.FromContext(h.wv.ctx).Debug().
169 Str("url", logging.TruncateURL(url, logging.PermissionLogURLMaxLen)).
170 Msg("cef: OnAddressChange")
171 }
172 h.wv.updateURI(url)
173 }
174}
175
176// OnTitleChange updates the cached title.
177func (h *handlerSet) OnTitleChange(_ purecef.Browser, title string) {
178 h.wv.updateTitle(title)
179}
180
181func (h *handlerSet) OnFaviconUrlchange(_ purecef.Browser, iconURLs purecef.StringList) {
182 if h == nil || h.wv == nil {
183 return
184 }
185 h.wv.mu.RLock()
186 pageURL := h.wv.uri
187 cb := h.wv.callbacks
188 h.wv.mu.RUnlock()
189 if cb == nil || cb.OnFaviconURLChanged == nil {
190 return
191 }
192 candidates := resolveFaviconCandidates(pageURL, decodeCEFStringList(iconURLs))
193 if len(candidates) == 0 {
194 return
195 }
196 h.wv.runOnGTK(func() {
197 cb.OnFaviconURLChanged(pageURL, candidates)
198 })
199}
200
201// OnFullscreenModeChange toggles the fullscreen atomic and fires callbacks.
202func (h *handlerSet) OnFullscreenModeChange(_ purecef.Browser, fullscreen int32) {
203 entering := fullscreen != 0
204 h.wv.fullscreen.Store(entering)
205
206 h.wv.mu.RLock()
207 cb := h.wv.callbacks
208 h.wv.mu.RUnlock()
209 if cb == nil {
210 return
211 }
212 if entering && cb.OnEnterFullscreen != nil {
213 h.wv.runOnGTK(func() {
214 cb.OnEnterFullscreen()
215 })
216 }
217 if !entering && cb.OnLeaveFullscreen != nil {
218 h.wv.runOnGTK(func() {
219 cb.OnLeaveFullscreen()
220 })
221 }
222}
223
224func (h *handlerSet) OnTooltip(_ purecef.Browser, _ uintptr) int32 { return 0 }
225
226// OnStatusMessage fires the OnLinkHover callback with the status text
227// and caches the hover URI for middle-click interception.
228func (h *handlerSet) OnStatusMessage(_ purecef.Browser, value string) {
229 h.wv.updateHoverURI(value)
230
231 h.wv.mu.RLock()
232 cb := h.wv.callbacks
233 h.wv.mu.RUnlock()
234 if cb != nil && cb.OnLinkHover != nil {
235 h.wv.runOnGTK(func() {
236 cb.OnLinkHover(value)
237 })
238 }
239}
240
241func (h *handlerSet) OnConsoleMessage(
242 _ purecef.Browser,
243 level purecef.LogSeverity,
244 message, source string,
245 line int32,
246) int32 {
247 if h.wv != nil && h.wv.ctx != nil &&
248 (strings.Contains(message, consoleMarkerVideoDiag) ||
249 strings.Contains(message, consoleMarkerAutoCopy) ||
250 strings.Contains(message, consoleMarkerScaleProbe)) {
251 log := logging.FromContext(h.wv.ctx).With().
252 Str("component", "cef-console").
253 Str("source", source).
254 Int32("line", line).
255 Logger()
256 switch level {
257 case purecef.LogSeverityLogseverityError, purecef.LogSeverityLogseverityFatal:
258 log.Error().Str("message", message).Msg("cef: console message")
259 case purecef.LogSeverityLogseverityWarning:
260 log.Warn().Str("message", message).Msg("cef: console message")
261 default:
262 log.Info().Str("message", message).Msg("cef: console message")
263 }
264 }
265 return 0
266}
267
268func (h *handlerSet) OnAutoResize(_ purecef.Browser, _ *purecef.Size) int32 { return 0 }
269
270// OnLoadingProgressChange updates the cached progress value.
271func (h *handlerSet) OnLoadingProgressChange(_ purecef.Browser, progress float64) {
272 if h.wv != nil && h.wv.ctx != nil {
273 logging.FromContext(h.wv.ctx).Trace().
274 Float64("progress", progress).
275 Msg("cef: OnLoadingProgressChange")
276 }
277 h.wv.updateProgress(progress)
278}
279
280func (h *handlerSet) OnCursorChange(
281 _ purecef.Browser,
282 _ uintptr,
283 cursorType purecef.CursorType,
284 _ *purecef.CursorInfo,
285) int32 {
286 if h == nil || h.wv == nil {
287 return 0
288 }
289 name := cefCursorToGDKName(cursorType)
290 h.wv.runOnGTK(func() {
291 if h.wv.viewBridge != nil {
292 h.wv.viewBridge.SetCursorFromName(name)
293 }
294 })
295 return 1 // handled
296}
297
298func (h *handlerSet) OnMediaAccessChange(_ purecef.Browser, _, _ int32) {}
299
300func (h *handlerSet) OnContentsBoundsChange(_ purecef.Browser, _ *purecef.Rect) int32 { return 0 }
301
302func (h *handlerSet) GetRootWindowScreenRect(_ purecef.Browser, _ *purecef.Rect) int32 { return 0 }
303
304// ===========================================================================
305// LoadHandler (4 methods)
306// ===========================================================================
307
308type faviconSourceVisitorRetention struct {
309 id uint64
310 visitor purecef.StringVisitor
311}
312
313type faviconSourceVisitor struct {
314 visit func(string)
315}
316
317func (v faviconSourceVisitor) Visit(source string) {
318 if v.visit != nil {
319 v.visit(source)
320 }
321}
322
323// OnLoadingStateChange updates the loading/navigation state cache and fires callbacks.
324func (h *handlerSet) OnLoadingStateChange(browser purecef.Browser, isloading, cangoback, cangoforward int32) {
325 loading := isloading != 0
326 h.wv.updateLoadState(loading, cangoback != 0, cangoforward != 0)
327 if h.wv != nil && h.wv.ctx != nil {
328 h.wv.mu.RLock()
329 uri := h.wv.uri
330 progress := h.wv.progress
331 pendingURI := h.wv.pendingURI
332 h.wv.mu.RUnlock()
333 logging.FromContext(h.wv.ctx).Debug().
334 Bool("loading", loading).
335 Bool("can_go_back", cangoback != 0).
336 Bool("can_go_forward", cangoforward != 0).
337 Float64("progress", progress).
338 Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)).
339 Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)).
340 Msg("cef: OnLoadingStateChange")
341 }
342
343 if !loading && h.wv.viewBridge != nil {
344 h.wv.runOnGTK(func() {
345 if h.wv.viewBridge != nil && h.wv.viewBridge.HasFocus() {
346 h.wv.mu.RLock()
347 host := h.wv.host
348 h.wv.mu.RUnlock()
349 if host != nil {
350 syncWindowlessBrowserFocus(host)
351 }
352 }
353 })
354 }
355
356 h.wv.mu.RLock()
357 cb := h.wv.callbacks
358 pageURL := h.wv.uri
359 h.wv.mu.RUnlock()
360 if !loading && cb != nil && cb.OnFaviconURLChanged != nil {
361 h.emitDiscoveredFaviconCandidates(browser, pageURL, cb)
362 }
363 if cb != nil && cb.OnLoadChanged != nil {
364 if loading {
365 h.wv.runOnGTK(func() {
366 cb.OnLoadChanged(port.LoadStarted)
367 })
368 } else {
369 h.wv.runOnGTK(func() {
370 cb.OnLoadChanged(port.LoadFinished)
371 })
372 }
373 }
374}
375
376var faviconSourceVisitorRetentionID atomic.Uint64
377
378func (wv *WebView) retainFaviconSourceVisitor(visitor purecef.StringVisitor) func() {
379 if wv == nil || visitor == nil {
380 return func() {}
381 }
382 id := faviconSourceVisitorRetentionID.Add(1)
383 wv.faviconSourceVisitorsMu.Lock()
384 wv.faviconSourceVisitors = append(wv.faviconSourceVisitors, faviconSourceVisitorRetention{id: id, visitor: visitor})
385 wv.faviconSourceVisitorsMu.Unlock()
386 return func() {
387 wv.faviconSourceVisitorsMu.Lock()
388 defer wv.faviconSourceVisitorsMu.Unlock()
389 for i, candidate := range wv.faviconSourceVisitors {
390 if candidate.id != id {
391 continue
392 }
393 last := len(wv.faviconSourceVisitors) - 1
394 copy(wv.faviconSourceVisitors[i:], wv.faviconSourceVisitors[i+1:])
395 wv.faviconSourceVisitors[last] = faviconSourceVisitorRetention{}
396 wv.faviconSourceVisitors = wv.faviconSourceVisitors[:last]
397 return
398 }
399 }
400}
401
402func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pageURL string, cb *port.WebViewCallbacks) {
403 if h == nil || h.wv == nil || cb == nil || cb.OnFaviconURLChanged == nil {
404 return
405 }
406 fallback := fallbackFaviconCandidates(pageURL)
407 emit := func(candidates []string) {
408 if len(candidates) == 0 {
409 return
410 }
411 h.wv.runOnGTK(func() {
412 cb.OnFaviconURLChanged(pageURL, candidates)
413 })
414 }
415 if browser == nil {
416 emit(fallback)
417 return
418 }
419 frame := browser.GetMainFrame()
420 if frame == nil {
421 emit(fallback)
422 return
423 }
424 var releaseOnce sync.Once
425 var release func()
426 visitor := cefNewStringVisitor(faviconSourceVisitor{visit: func(source string) {
427 releaseOnce.Do(func() {
428 if release != nil {
429 release()
430 }
431 })
432 candidates := DiscoverFaviconCandidates(pageURL, source)
433 if len(candidates) == 0 {
434 candidates = fallback
435 }
436 emit(candidates)
437 }})
438 release = h.wv.retainFaviconSourceVisitor(visitor)
439 time.AfterFunc(5*time.Second, func() {
440 releaseOnce.Do(func() {
441 if release != nil {
442 release()
443 }
444 })
445 })
446 frame.GetSource(visitor)
447}
448
449// OnLoadStart fires LoadCommitted for main frame navigations.
450// In CEF this callback runs after navigation commit, so it is the closest
451// equivalent to WebKit's LoadCommitted event.
452func (h *handlerSet) OnLoadStart(_ purecef.Browser, frame purecef.Frame, _ purecef.TransitionType) {
453 if frame == nil || !frame.IsMain() {
454 return
455 }
456 bridgeNonce := h.wv.ensureBridgeNonce()
457 if openerBridgeScript := h.wv.popupOpenerBridgeScript(bridgeNonce); openerBridgeScript != "" {
458 if h.wv != nil && h.wv.ctx != nil {
459 parentURI, active, blocked := h.wv.popupOpenerBridgeState()
460 logging.FromContext(h.wv.ctx).Debug().
461 Uint64("webview_id", uint64(h.wv.id)).
462 Str("url", logging.TruncateURL(frame.GetURL(), logging.PermissionLogURLMaxLen)).
463 Str("parent_uri", logging.TruncateURL(parentURI, logging.PermissionLogURLMaxLen)).
464 Bool("bridge_active", active).
465 Bool("bridge_blocked", blocked).
466 Msg("cef: injecting popup opener bridge")
467 }
468 frame.ExecuteJavaScript(openerBridgeScript, frame.GetURL(), 0)
469 }
470 if h.wv != nil && h.wv.ctx != nil {
471 logging.FromContext(h.wv.ctx).Debug().
472 Str("url", logging.TruncateURL(frame.GetURL(), logging.PermissionLogURLMaxLen)).
473 Msg("cef: OnLoadStart")
474 }
475 h.wv.updateURI(frame.GetURL())
476 h.wv.mu.RLock()
477 cb := h.wv.callbacks
478 h.wv.mu.RUnlock()
479 if cb != nil && cb.OnLoadChanged != nil {
480 h.wv.runOnGTK(func() {
481 cb.OnLoadChanged(port.LoadCommitted)
482 })
483 }
484}
485
486// OnLoadEnd resets crash count and injects content scripts for the main frame.
487func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatusCode int32) {
488 log := logging.FromContext(h.wv.ctx)
489 log.Debug().
490 Bool("frame_nil", frame == nil).
491 Bool("is_main", frame != nil && frame.IsMain()).
492 Int32("http_status", httpStatusCode).
493 Msg("cef: OnLoadEnd")
494 if frame == nil || !frame.IsMain() {
495 return
496 }
497 // Successful load — reset the consecutive crash counter.
498 h.wv.crashCount.Store(0)
499
500 // If a queued startup navigation is still pending after about:blank finished,
501 // replay it now that the initial main-frame load completed.
502 frameURL := frame.GetURL()
503 if pendingURI := h.wv.pendingNavigationURI(); pendingURI != "" && !pendingURIEquivalent(frameURL, pendingURI) {
504 if strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") {
505 log.Debug().
506 Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)).
507 Msg("cef: replaying pending navigation after about:blank load end")
508 h.wv.schedulePendingNavigationReplay(0)
509 }
510 }
511
512 // Inject scripts and styles after page load.
513 // Must run on GTK thread — OnLoadEnd fires on the CEF IO thread,
514 // and JavaScript injection requires the main thread.
515 if cefScaleProbeEnabled() && shouldRunCEFScaleProbe(frameURL, httpStatusCode) {
516 // Do not retain/use CEF callback-scoped frame wrappers across the GTK idle hop.
517 // Re-read the current main frame through WebView.RunJavaScript instead.
518 h.wv.runOnGTK(func() {
519 snapshot := cefScaleProbeSnapshot(h.wv)
520 event := logging.FromContext(h.wv.ctx).Debug()
521 for key, value := range snapshot.logFields() {
522 event = event.Interface(key, value)
523 }
524 event.Msg("cef: injecting scale probe")
525 h.wv.RunJavaScript(context.Background(), cefScaleProbeScript)
526 })
527 }
528 if h.wv.engine != nil && h.wv.engine.contentInj != nil {
529 h.wv.runOnGTK(func() {
530 h.wv.engine.contentInj.onLoadEnd(h.wv)
531 })
532 }
533
534 // NOTE: We intentionally do NOT dispatch LoadFinished or ProgressChanged(1.0)
535 // here. OnLoadEnd is a per-frame event, and during cross-site navigations
536 // (process swap), CEF fires OnLoadEnd for the OLD main frame before the new
537 // page finishes loading. Dispatching LoadFinished here caused the progress
538 // bar to hide prematurely while the new page was still loading.
539 //
540 // OnLoadingStateChange(isLoading=false) is the correct browser-level signal
541 // for load completion — CEF guarantees it fires AFTER all OnLoadEnd calls.
542 // CEF's OnLoadingProgressChange also provides progress=1.0 at completion.
543}
544
545const cefErrAborted purecef.Errorcode = -3
546
547// OnLoadError handles failed main-frame navigations that never reach
548// OnLoadStart. CEF still sends OnLoadingStateChange(false) after load errors,
549// so this callback only synthesizes the missing committed event needed to reveal
550// the pane; the normal browser-level loading-state callback remains responsible
551// for the terminal LoadFinished state.
552func (h *handlerSet) OnLoadError(_ purecef.Browser, frame purecef.Frame, errorCode purecef.Errorcode, errorText, failedURL string) {
553 if h == nil || h.wv == nil || frame == nil || !frame.IsMain() {
554 return
555 }
556 if errorCode == cefErrAborted {
557 return
558 }
559 if failedURL == "" {
560 failedURL = frame.GetURL()
561 }
562
563 h.wv.mu.RLock()
564 currentURI := h.wv.uri
565 cb := h.wv.callbacks
566 h.wv.mu.RUnlock()
567
568 if h.wv.ctx != nil {
569 logging.FromContext(h.wv.ctx).Debug().
570 Int32("error_code", errorCode).
571 Str("error_text", errorText).
572 Str("failed_url", logging.TruncateURL(failedURL, logging.PermissionLogURLMaxLen)).
573 Msg("cef: OnLoadError")
574 }
575
576 if failedURL != "" {
577 h.wv.updateURI(failedURL)
578 }
579
580 if cb == nil || cb.OnLoadChanged == nil {
581 return
582 }
583 if failedURL != "" && !pendingURIEquivalent(currentURI, failedURL) {
584 h.wv.runOnGTK(func() {
585 cb.OnLoadChanged(port.LoadCommitted)
586 })
587 }
588}
589
590func mapCEFWindowDisposition(disposition purecef.WindowOpenDisposition) dto.WindowDisposition {
591 switch disposition {
592 case purecef.WindowOpenDispositionWodCurrentTab:
593 return dto.WindowDispositionCurrentTab
594 case purecef.WindowOpenDispositionWodNewPopup:
595 return dto.WindowDispositionNewPopup
596 case purecef.WindowOpenDispositionWodNewWindow:
597 return dto.WindowDispositionNewWindow
598 case purecef.WindowOpenDispositionWodNewForegroundTab,
599 purecef.WindowOpenDispositionWodNewBackgroundTab,
600 purecef.WindowOpenDispositionWodSingletonTab,
601 purecef.WindowOpenDispositionWodSwitchToTab:
602 return dto.WindowDispositionNewTab
603 default:
604 return ""
605 }
606}
607
608// ===========================================================================
609// LifeSpanHandler (6 methods)
610// ===========================================================================
611
612// OnBeforePopup intercepts popup requests (target="_blank", window.open).
613// When the coordinator returns a related CEF popup shell we hand that shell's
614// client back to CEF and allow native popup creation so opener semantics are
615// preserved. Otherwise we keep blocking and let the coordinator's fallback
616// pane handle the navigation.
617func (h *handlerSet) OnBeforePopup(
618 browser purecef.Browser, frame purecef.Frame, popupID int32, targetURL, targetFrameName string,
619 targetDisposition purecef.WindowOpenDisposition, userGesture int32, _ *purecef.PopupFeatures,
620 windowInfo *purecef.WindowInfo, clientSlot *purecef.RawClientWriteSlot, settings *purecef.BrowserSettings,
621 _ *purecef.DictionaryValue, noJavaScriptAccess *bool,
622) bool {
623 if targetURL == "" {
624 return true
625 }
626
627 h.wv.mu.RLock()
628 cb := h.wv.callbacks
629 h.wv.mu.RUnlock()
630 if cb == nil || cb.OnCreate == nil {
631 return true
632 }
633
634 requestNoJavaScriptAccess := false
635 if noJavaScriptAccess != nil {
636 requestNoJavaScriptAccess = *noJavaScriptAccess
637 }
638 var popup port.WebView
639 sourceBrowserID := int32(0)
640 if browser != nil {
641 sourceBrowserID = browser.GetIdentifier()
642 }
643 sourceFrameID := ""
644 sourceFrameURL := ""
645 if frame != nil {
646 sourceFrameID = frame.GetIdentifier()
647 sourceFrameURL = frame.GetURL()
648 }
649
650 dispatchResult := h.wv.runOnGTKSyncLabel("cef.on_before_popup", func() {
651 popup = cb.OnCreate(port.PopupRequest{
652 TargetURI: targetURL,
653 FrameName: targetFrameName,
654 IsUserGesture: userGesture != 0,
655 NoJavaScriptAccess: requestNoJavaScriptAccess,
656 ParentViewID: h.wv.id,
657 SourceBrowserID: sourceBrowserID,
658 SourceFrameID: sourceFrameID,
659 SourceFrameURL: sourceFrameURL,
660 TargetDisposition: mapCEFWindowDisposition(targetDisposition),
661 })
662 })
663 if !dispatchResult.Completed() {
664 logging.FromContext(h.currentContext()).Warn().
665 Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)).
666 Str("target_frame", targetFrameName).
667 Dur("elapsed", dispatchResult.Elapsed).
668 Str("dispatch_status", string(dispatchResult.Status)).
669 Msg("cef: blocked popup after GTK dispatch did not complete")
670 return true
671 }
672
673 cefPopup, ok := popup.(*WebView)
674 if !ok || cefPopup == nil {
675 return true
676 }
677
678 decision := dto.HostDecision{}
679 if carrier, ok := popup.(port.BrowsingContextHostDecisionCapable); ok {
680 if got, hasDecision := carrier.BrowsingContextHostDecision(); hasDecision {
681 decision = got
682 }
683 }
684
685 cefPopup.setPopupNoJavaScriptAccess(requestNoJavaScriptAccess)
686 switch decision.Kind {
687 case dto.HostDecisionCreateNativeWin:
688 if cefPopup.prepareNativePopup(popupID, targetURL, windowInfo, clientSlot, settings) {
689 logging.FromContext(h.currentContext()).Debug().
690 Int32("popup_id", popupID).
691 Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)).
692 Msg("cef: native popup armed")
693 return false
694 }
695 if aborter, ok := popup.(port.NativePopupHostAbortCapable); ok {
696 aborter.AbortNativePopupHost()
697 }
698 cefPopup.discardNativePopupCandidate()
699 logging.FromContext(h.currentContext()).Warn().
700 Int32("popup_id", popupID).
701 Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)).
702 Msg("cef: native popup arming failed, denying without fallback")
703 return true
704 case dto.HostDecisionCreatePane, dto.HostDecisionReuseNamedPane:
705 logging.FromContext(h.currentContext()).Debug().
706 Int32("popup_id", popupID).
707 Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)).
708 Str("decision", string(decision.Kind)).
709 Msg("cef: pane-hosted browsing context, blocking native popup and using related shell browser creation")
710 return true
711 default:
712 if cefPopup.prepareNativePopup(popupID, targetURL, windowInfo, clientSlot, settings) {
713 return false
714 }
715
716 cefPopup.discardNativePopupCandidate()
717 logging.FromContext(h.currentContext()).Debug().
718 Int32("popup_id", popupID).
719 Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)).
720 Msg("cef: native popup bridge unavailable, blocking CEF popup and using popup shell browser creation")
721 return true
722 }
723}
724
725func (h *handlerSet) OnBeforePopupAborted(_ purecef.Browser, popupID int32) {
726 if popup := h.wv.takePendingNativePopup(popupID); popup != nil {
727 popup.handleNativePopupAborted()
728 }
729}
730
731func (h *handlerSet) OnBeforeDevToolsPopup(
732 _ purecef.Browser, _ *purecef.WindowInfo, _ *purecef.RawClientWriteSlot,
733 _ *purecef.BrowserSettings, _ *purecef.DictionaryValue, _ *bool,
734) {
735}
736
737// OnAfterCreated stores the browser and host references and enables input.
738func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
739 log := logging.FromContext(h.wv.ctx)
740 if browser == nil {
741 log.Warn().Msg("cef: OnAfterCreated called with nil browser")
742 return
743 }
744 browserID := browser.GetIdentifier()
745 log.Debug().Int32("browser_id", browserID).Msg("cef: OnAfterCreated")
746
747 host := browser.GetHost()
748 if host == nil {
749 log.Warn().Msg("cef: OnAfterCreated returned nil host")
750 return
751 }
752
753 state := h.attachAfterCreatedBrowser(browser, host, browserID)
754 if state.closeDuplicate {
755 log.Warn().
756 Int32("browser_id", browserID).
757 Int32("existing_browser_id", state.duplicateBrowserID).
758 Msg("cef: duplicate popup browser attached after shell already had a browser; closing duplicate")
759 host.CloseBrowser(1)
760 return
761 }
762
763 h.finishAfterCreated(browser, host, state)
764}
765
766func (h *handlerSet) attachAfterCreatedBrowser(
767 browser purecef.Browser,
768 host purecef.BrowserHost,
769 browserID int32,
770) afterCreatedState {
771 state := afterCreatedState{}
772 h.wv.mu.Lock()
773
774 if existing := h.wv.browser; existing != nil {
775 existingID := existing.GetIdentifier()
776 if existingID != 0 && existingID != browserID {
777 state.closeDuplicate = true
778 state.duplicateBrowserID = existingID
779 h.wv.mu.Unlock()
780 return state
781 }
782 }
783
784 h.wv.browser = browser
785 h.wv.host = host
786 h.wv.lastAppliedZoomScaleRatioBits.Store(0)
787 h.wv.pendingCreate = nil
788 bridge := h.wv.viewBridge
789 h.wv.inputAttached = bridge == nil
790 state.inputAttached = h.wv.inputAttached
791 if h.wv.findCtrl != nil {
792 h.wv.findCtrl.setHost(host)
793 }
794 state.nativePopupParent = h.wv.nativePopupParent
795 state.nativePopupID = h.wv.nativePopupID
796 h.wv.nativePopupParent = nil
797 h.wv.nativePopupID = 0
798 h.wv.nativePopupFallbackStarted = false
799 state.hasPendingNavigation = strings.TrimSpace(h.wv.pendingURI) != ""
800 h.wv.mu.Unlock()
801
802 if bridge != nil {
803 wv := h.wv
804 wv.runOnGTK(func() {
805 if wv.destroyed.Load() || wv.viewBridge == nil {
806 wv.handleInputAttachFailure(ErrAdapterDestroyed, host)
807 return
808 }
809 if err := wv.viewBridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil {
810 wv.handleInputAttachFailure(err, host)
811 return
812 }
813 if wv.viewBridge != nil && wv.viewBridge.HasFocus() {
814 syncWindowlessBrowserFocus(host)
815 } else {
816 host.Invalidate(purecef.PaintElementTypePetView)
817 }
818 wv.markInputAttached()
819 })
820 }
821
822 // Mark browser as visible — CEF OSR starts in hidden state and suppresses
823 // painting/caret updates until explicitly told the browser is shown.
824 host.WasHidden(0)
825 if h.wv != nil {
826 if h.wv.ctx != nil {
827 logging.FromContext(h.wv.ctx).Info().
828 Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()).
829 Msg("cef: browser initial/configured windowless frame rate")
830 }
831 h.wv.SyncViewport(h.wv.ctx, "after-created")
832 }
833 return state
834}
835
836func (wv *WebView) handleInputAttachFailure(err error, host purecef.BrowserHost) {
837 if wv == nil || err == nil {
838 return
839 }
840 if wv.ctx != nil {
841 logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to attach input to cef2gtk bridge")
842 }
843 if host != nil && !wv.destroyed.Load() {
844 host.CloseBrowser(1)
845 return
846 }
847 wv.runCloseCallbacks()
848}
849
850func (h *handlerSet) finishAfterCreated(
851 browser purecef.Browser,
852 host purecef.BrowserHost,
853 state afterCreatedState,
854) {
855 if h.wv.engine != nil {
856 h.wv.engine.recordBrowserAfterCreated(browser)
857 h.wv.engine.registerWebView(h.wv)
858 h.wv.engine.bindBrowserWebView(browser, h.wv)
859 }
860 h.wv.stopNativePopupFallbackTimer()
861 if state.inputAttached {
862 // Request an initial OSR frame even before the first real navigation
863 // commits. This restores the about:blank warm-up paint that the stable
864 // startup path relied on and prevents the rendering bridge from staying idle.
865 host.Invalidate(purecef.PaintElementTypePetView)
866 }
867 if state.hasPendingNavigation {
868 h.wv.schedulePendingNavigationReplay(0)
869 }
870 if state.nativePopupParent != nil && state.nativePopupID != 0 {
871 state.nativePopupParent.clearPendingNativePopup(state.nativePopupID, h.wv)
872 }
873 h.wv.scheduleStartAdaptiveFrameRatePolling()
874 h.wv.scheduleStartBeginFrameLoop()
875 h.wv.fireReadyToShow()
876}
877
878type afterCreatedState struct {
879 inputAttached bool
880 hasPendingNavigation bool
881 nativePopupParent *WebView
882 nativePopupID int32
883 duplicateBrowserID int32
884 closeDuplicate bool
885}
886
887// DoClose returns false to allow the default close behavior.
888func (h *handlerSet) DoClose(_ purecef.Browser) bool {
889 return false
890}
891
892// OnBeforeClose fires the OnClose callback.
893func (h *handlerSet) OnBeforeClose(browser purecef.Browser) {
894 if h.wv.engine != nil {
895 browserID := int32(0)
896 if browser != nil {
897 browserID = browser.GetIdentifier()
898 }
899 h.wv.engine.unregisterWebView(h.wv, browserID)
900 }
901 h.wv.mu.Lock()
902 h.wv.browser = nil
903 h.wv.host = nil
904 h.wv.lastAppliedZoomScaleRatioBits.Store(0)
905 h.wv.inputAttached = false
906 if h.wv.findCtrl != nil {
907 h.wv.findCtrl.setHost(nil)
908 }
909 h.wv.mu.Unlock()
910 if h.wv.destroyed.Load() {
911 h.wv.destroyViewBridgeOnGTKAsync()
912 } else if h.wv.viewBridge != nil {
913 wv := h.wv
914 wv.runOnGTK(func() {
915 if wv.viewBridge == nil {
916 return
917 }
918 if err := wv.viewBridge.SetInputHost(nil); err != nil && wv.ctx != nil {
919 logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: failed to clear cef2gtk input host")
920 }
921 })
922 }
923 h.wv.scheduleStopAdaptiveFrameRatePolling()
924 h.wv.scheduleStopBeginFrameLoop()
925
926 h.wv.mu.RLock()
927 cb := h.wv.callbacks
928 h.wv.mu.RUnlock()
929 if cb != nil && cb.OnClose != nil {
930 h.wv.runOnGTK(func() {
931 cb.OnClose()
932 })
933 }
934 h.wv.runCloseCallbacks()
935}
936
937// ===========================================================================
938// RequestHandler (11 methods)
939// ===========================================================================
940
941func (h *handlerSet) OnBeforeBrowse(browser purecef.Browser, frame purecef.Frame, request purecef.Request, _, _ int32) bool {
942 if frame == nil || !frame.IsMain() || request == nil {
943 return false
944 }
945
946 handler := h.downloadHandler()
947 if handler == nil || !strings.EqualFold(request.GetMethod(), "GET") {
948 return false
949 }
950
951 url := request.GetURL()
952 if !downloadutil.ShouldForceDownloadForURI(url) || browser == nil {
953 return false
954 }
955
956 host := browser.GetHost()
957 if host == nil {
958 return false
959 }
960
961 logging.FromContext(h.currentContext()).Debug().
962 Str("url", logging.TruncateURL(url, maxSchemeTruncatedURLLength)).
963 Msg("cef: forcing download for navigation")
964
965 host.StartDownload(url)
966 return true
967}
968
969func (h *handlerSet) CanDownload(browser purecef.Browser, url, requestMethod string) bool {
970 handler := h.downloadHandler()
971 if handler == nil {
972 return false
973 }
974 return handler.canDownload(browser, url, requestMethod)
975}
976
977func (h *handlerSet) OnBeforeDownload(
978 browser purecef.Browser,
979 downloadItem purecef.DownloadItem,
980 suggestedName string,
981 callback purecef.BeforeDownloadCallback,
982) bool {
983 handler := h.downloadHandler()
984 if handler == nil {
985 return false
986 }
987 return handler.onBeforeDownload(h.currentContext(), browser, downloadItem, suggestedName, callback)
988}
989
990func (h *handlerSet) OnDownloadUpdated(
991 _ purecef.Browser,
992 downloadItem purecef.DownloadItem,
993 callback purecef.DownloadItemCallback,
994) {
995 handler := h.downloadHandler()
996 if handler == nil {
997 return
998 }
999 handler.onDownloadUpdated(h.currentContext(), downloadItem, callback)
1000}
1001
1002func (h *handlerSet) downloadHandler() *downloadHandler {
1003 if h == nil || h.wv == nil || h.wv.engine == nil {
1004 return nil
1005 }
1006 return h.wv.engine.currentDownloadHandler()
1007}
1008
1009func (h *handlerSet) currentContext() context.Context {
1010 if h == nil || h.wv == nil || h.wv.ctx == nil {
1011 return context.Background()
1012 }
1013 return h.wv.ctx
1014}
1015
1016func (h *handlerSet) OnOpenUrlfromTab(
1017 _ purecef.Browser,
1018 _ purecef.Frame,
1019 _ string,
1020 _ purecef.WindowOpenDisposition,
1021 _ int32,
1022) int32 {
1023 return 0
1024}
1025
1026func (h *handlerSet) GetResourceRequestHandler(
1027 _ purecef.Browser, _ purecef.Frame, _ purecef.Request,
1028 _, _ int32, _ string, _ *int32,
1029) purecef.ResourceRequestHandler {
1030 return nil
1031}
1032
1033func (h *handlerSet) GetAuthCredentials(
1034 _ purecef.Browser, _ string, _ int32, _ string, _ int32,
1035 _, _ string, _ purecef.AuthCallback,
1036) int32 {
1037 return 0
1038}
1039
1040func (h *handlerSet) OnCertificateError(
1041 _ purecef.Browser,
1042 _ purecef.Errorcode,
1043 _ string,
1044 _ purecef.Sslinfo,
1045 _ purecef.Callback,
1046) int32 {
1047 return 0
1048}
1049
1050func (h *handlerSet) OnSelectClientCertificate(
1051 _ purecef.Browser, _ int32, _ string, _ int32,
1052 _ []purecef.X509Certificate, _ purecef.SelectClientCertificateCallback,
1053) int32 {
1054 return 0
1055}
1056
1057func (h *handlerSet) OnRenderViewReady(_ purecef.Browser) {}
1058
1059func (h *handlerSet) OnRenderProcessUnresponsive(_ purecef.Browser, _ purecef.UnresponsiveProcessCallback) int32 {
1060 return 0
1061}
1062
1063func (h *handlerSet) OnRenderProcessResponsive(_ purecef.Browser) {}
1064
1065// maxConsecutiveCrashes caps how many times OnRenderProcessTerminated will
1066// forward the event before suppressing further notifications. This prevents
1067// infinite crash → redirect → crash loops.
1068const maxConsecutiveCrashes = 3
1069
1070// OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason.
1071func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) {
1072 if h.wv.crashCount.Add(1) > maxConsecutiveCrashes {
1073 return // suppress to break the loop
1074 }
1075
1076 var reason port.WebProcessTerminationReason
1077 var label string
1078 switch status {
1079 case purecef.TerminationStatusTsAbnormalTermination:
1080 reason = port.WebProcessTerminationCrashed
1081 label = "abnormal termination"
1082 case purecef.TerminationStatusTsProcessWasKilled:
1083 reason = port.WebProcessTerminationByAPI
1084 label = "killed"
1085 case purecef.TerminationStatusTsProcessCrashed:
1086 reason = port.WebProcessTerminationCrashed
1087 label = "crashed"
1088 case purecef.TerminationStatusTsProcessOom:
1089 reason = port.WebProcessTerminationExceededMemory
1090 label = "out of memory"
1091 default:
1092 reason = port.WebProcessTerminationUnknown
1093 label = "unknown"
1094 }
1095
1096 h.wv.mu.RLock()
1097 cb := h.wv.callbacks
1098 uri := h.wv.uri
1099 h.wv.mu.RUnlock()
1100 if cb != nil && cb.OnWebProcessTerminated != nil {
1101 h.wv.runOnGTK(func() {
1102 cb.OnWebProcessTerminated(reason, label, uri)
1103 })
1104 }
1105}
1106
1107func (h *handlerSet) OnDocumentAvailableInMainFrame(_ purecef.Browser) {}
1108
1109// ===========================================================================
1110// AudioHandler (5 methods)
1111// ===========================================================================
1112
1113func (h *handlerSet) GetAudioParameters(_ purecef.Browser, _ *purecef.AudioParameters) int32 {
1114 return 1 // proceed with defaults
1115}
1116
1117// OnAudioStreamStarted handles the start of an audio stream.
1118// The third callback argument is the channel count (not frames-per-buffer);
1119// frames-per-buffer comes from params.FramesPerBuffer.
1120func (h *handlerSet) OnAudioStreamStarted(_ purecef.Browser, params *purecef.AudioParameters, channels int32) {
1121 // Validate params and factory first
1122 if params == nil || h.wv.audioOutputFactory == nil {
1123 return
1124 }
1125
1126 // Build format from CEF parameters
1127 format := h.buildAudioStreamFormat(params, channels)
1128
1129 if h.wv.ctx != nil {
1130 logging.FromContext(h.wv.ctx).Info().
1131 Int("sample_rate", format.SampleRate).
1132 Int("channels", format.ChannelCount).
1133 Int("frames_per_buffer", format.FramesPerBuffer).
1134 Int32("channel_layout", params.ChannelLayout).
1135 Msg("cef: OnAudioStreamStarted")
1136 }
1137
1138 // Reset packet counters for the new stream
1139 h.wv.audioPacketCount.Store(0)
1140 h.wv.audioWriteCount.Store(0)
1141
1142 // Close any existing stream first
1143 h.wv.closeAudioStream()
1144
1145 // Create new stream
1146 ctx := h.wv.ctx
1147 if ctx == nil {
1148 ctx = context.Background()
1149 }
1150
1151 stream, err := h.wv.audioOutputFactory.NewStream(ctx, format)
1152 if err != nil {
1153 // Log error but don't panic - audio is non-critical.
1154 // Do NOT set audioPlaying — the stream was never created.
1155 if h.wv.ctx != nil {
1156 logging.FromContext(h.wv.ctx).Warn().
1157 Err(err).
1158 Int("sample_rate", format.SampleRate).
1159 Int("channels", format.ChannelCount).
1160 Msg("cef: failed to create audio output stream")
1161 }
1162 return
1163 }
1164
1165 // Set playing state only after stream creation succeeds
1166 h.wv.setAudioPlaying(true)
1167
1168 if h.wv.ctx != nil {
1169 logging.FromContext(h.wv.ctx).Info().
1170 Int("sample_rate", format.SampleRate).
1171 Int("channels", format.ChannelCount).
1172 Int("frames_per_buffer", format.FramesPerBuffer).
1173 Msg("cef: audio output stream created successfully")
1174 }
1175
1176 // Store the new stream
1177 h.wv.audioStreamMu.Lock()
1178 h.wv.activeAudioStream = stream
1179 h.wv.audioStreamMu.Unlock()
1180}
1181
1182// buildAudioStreamFormat converts CEF AudioParameters to port.AudioStreamFormat.
1183// channels is the authoritative channel count from the callback; params.FramesPerBuffer
1184// supplies the buffer size.
1185func (h *handlerSet) buildAudioStreamFormat(params *purecef.AudioParameters, channels int32) port.AudioStreamFormat {
1186 return port.AudioStreamFormat{
1187 SampleRate: int(params.SampleRate),
1188 ChannelCount: int(channels),
1189 FramesPerBuffer: int(params.FramesPerBuffer),
1190 }
1191}
1192
1193// OnAudioStreamPacket receives audio packets and forwards them to the output stream.
1194// The mutex is held across both the stream snapshot and Write to prevent
1195// closeAudioStream from closing the stream mid-write.
1196func (h *handlerSet) OnAudioStreamPacket(_ purecef.Browser, data [][]float32, frames int32, pts int64) {
1197 if len(data) == 0 || frames <= 0 {
1198 return
1199 }
1200
1201 // Copy the data before acquiring the stream lock because CEF can reuse the
1202 // buffer as soon as this callback returns.
1203 copiedData := make([][]float32, len(data))
1204 for i, channel := range data {
1205 if len(channel) < int(frames) {
1206 continue
1207 }
1208 copiedData[i] = make([]float32, frames)
1209 copy(copiedData[i], channel[:frames])
1210 }
1211
1212 // Hold the lock across the stream read and Write so closeAudioStream
1213 // cannot close the stream between snapshot and write.
1214 h.wv.audioStreamMu.Lock()
1215 stream := h.wv.activeAudioStream
1216 if stream == nil {
1217 h.wv.audioStreamMu.Unlock()
1218 return
1219 }
1220
1221 pktNum := h.wv.audioPacketCount.Add(1)
1222 if pktNum == 1 && h.wv.ctx != nil {
1223 logging.FromContext(h.wv.ctx).Info().
1224 Int("channels", len(data)).
1225 Int32("frames", frames).
1226 Int64("pts", pts).
1227 Msg("cef: first audio packet received")
1228 }
1229
1230 err := stream.Write(copiedData)
1231 h.wv.audioStreamMu.Unlock()
1232
1233 if err != nil {
1234 if h.wv.ctx != nil {
1235 logging.FromContext(h.wv.ctx).Debug().
1236 Err(err).
1237 Uint64("packets_received", pktNum).
1238 Msg("cef: audio stream write failed")
1239 }
1240 return
1241 }
1242 h.wv.audioWriteCount.Add(1)
1243}
1244
1245// OnAudioStreamStopped handles stream stop.
1246func (h *handlerSet) OnAudioStreamStopped(_ purecef.Browser) {
1247 if h.wv.ctx != nil {
1248 logging.FromContext(h.wv.ctx).Info().
1249 Uint64("packets_received", h.wv.audioPacketCount.Load()).
1250 Uint64("writes_succeeded", h.wv.audioWriteCount.Load()).
1251 Msg("cef: OnAudioStreamStopped")
1252 }
1253 h.wv.setAudioPlaying(false)
1254 h.wv.closeAudioStream()
1255}
1256
1257// OnAudioStreamError handles stream errors.
1258func (h *handlerSet) OnAudioStreamError(_ purecef.Browser, message string) {
1259 if h.wv.ctx != nil {
1260 event := logging.FromContext(h.wv.ctx).Warn()
1261 logMsg := "cef: audio stream error"
1262 if h.wv.destroyed.Load() && message == "Socket closed unexpectedly" {
1263 event = logging.FromContext(h.wv.ctx).Debug()
1264 logMsg = "cef: audio stream closed during shutdown"
1265 }
1266 event.
1267 Str("error", message).
1268 Uint64("packets_received", h.wv.audioPacketCount.Load()).
1269 Uint64("writes_succeeded", h.wv.audioWriteCount.Load()).
1270 Msg(logMsg)
1271 }
1272 h.wv.setAudioPlaying(false)
1273 h.wv.closeAudioStream()
1274}
1275
1276// ===========================================================================
1277// FindHandler (1 method)
1278// ===========================================================================
1279
1280// OnFindResult dispatches CEF find results to the WebView's FindController.
1281func (h *handlerSet) OnFindResult(
1282 _ purecef.Browser,
1283 identifier, count int32,
1284 _ *purecef.Rect,
1285 activematchordinal, finalupdate int32,
1286) {
1287 if h.wv.findCtrl != nil {
1288 h.wv.findCtrl.handleFindResult(identifier, count, activematchordinal, finalupdate)
1289 }
1290}
1291
1292// cefCursorNames maps CEF cursor types to GDK/CSS cursor names.
1293var cefCursorNames = map[purecef.CursorType]string{
1294 purecef.CursorTypeCtPointer: "default",
1295 purecef.CursorTypeCtCross: "crosshair",
1296 purecef.CursorTypeCtHand: "pointer",
1297 purecef.CursorTypeCtIbeam: "text",
1298 purecef.CursorTypeCtWait: "wait",
1299 purecef.CursorTypeCtHelp: "help",
1300 purecef.CursorTypeCtEastresize: "e-resize",
1301 purecef.CursorTypeCtNorthresize: "n-resize",
1302 purecef.CursorTypeCtNortheastresize: "ne-resize",
1303 purecef.CursorTypeCtNorthwestresize: "nw-resize",
1304 purecef.CursorTypeCtSouthresize: "s-resize",
1305 purecef.CursorTypeCtSoutheastresize: "se-resize",
1306 purecef.CursorTypeCtSouthwestresize: "sw-resize",
1307 purecef.CursorTypeCtWestresize: "w-resize",
1308 purecef.CursorTypeCtNorthsouthresize: "ns-resize",
1309 purecef.CursorTypeCtEastwestresize: "ew-resize",
1310 purecef.CursorTypeCtNortheastsouthwestresize: "nesw-resize",
1311 purecef.CursorTypeCtNorthwestsoutheastresize: "nwse-resize",
1312 purecef.CursorTypeCtColumnresize: "col-resize",
1313 purecef.CursorTypeCtRowresize: "row-resize",
1314 purecef.CursorTypeCtMove: "move",
1315 purecef.CursorTypeCtProgress: "progress",
1316 purecef.CursorTypeCtNodrop: "no-drop",
1317 purecef.CursorTypeCtNotallowed: "not-allowed",
1318 purecef.CursorTypeCtGrab: "grab",
1319 purecef.CursorTypeCtGrabbing: "grabbing",
1320 purecef.CursorTypeCtZoomin: "zoom-in",
1321 purecef.CursorTypeCtZoomout: "zoom-out",
1322}
1323
1324// cefCursorToGDKName maps a CEF cursor type to a GDK/CSS cursor name.
1325func cefCursorToGDKName(ct purecef.CursorType) string {
1326 if name, ok := cefCursorNames[ct]; ok {
1327 return name
1328 }
1329 return "default"
1330}