1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: gtk4 webkitgtk-6.0
7#include <gtk/gtk.h>
8#include <gdk/gdk.h>
9#include <webkit/webkit.h>
10
11// Declarations for Go callbacks
12extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
13extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);
14extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state);
15
16// Key controller callback -> forward to Go
17static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
18 (void)controller; (void)keycode;
19 return goOnKeyPress((unsigned long)user_data, keyval, state);
20}
21
22// Mouse click gesture -> forward side buttons to Go
23static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) {
24 (void)n_press; (void)x; (void)y;
25 guint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
26 // Pass 0 for modifier state (not provided via gesture API)
27 goOnButtonPress((unsigned long)user_data, button, 0);
28}
29
30// Attach controllers to a widget (GTK4)
31static void attach_key_controller(GtkWidget* widget, unsigned long id) {
32 if (!widget) return;
33 GtkEventController* keyc = gtk_event_controller_key_new();
34 // Capture phase so we see keys even if child consumes them (e.g., WebKit)
35 gtk_event_controller_set_propagation_phase(keyc, GTK_PHASE_CAPTURE);
36 g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);
37 gtk_widget_add_controller(widget, keyc);
38}
39
40static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) {
41 if (!widget) return;
42 GtkGesture* click = gtk_gesture_click_new();
43 g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);
44 gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
45}
46
47// Legacy controller to capture side buttons (8/9) reliably in GTK4
48static gboolean on_legacy_event(GtkEventController* controller, GdkEvent* event, gpointer user_data) {
49 (void)controller;
50 if (!event) return FALSE;
51 GdkEventType type = gdk_event_get_event_type(event);
52 if (type == GDK_BUTTON_PRESS) {
53 guint button = gdk_button_event_get_button(event);
54 GdkModifierType state = gdk_event_get_modifier_state(event);
55 if (button == 8 || button == 9) {
56 goOnButtonPress((unsigned long)user_data, button, state);
57 return TRUE; // consume side buttons
58 }
59 return FALSE; // let WebKit handle normal clicks
60 } else if (type == GDK_SCROLL) {
61 double dx = 0.0, dy = 0.0;
62 gdk_scroll_event_get_deltas(event, &dx, &dy);
63 GdkModifierType state = gdk_event_get_modifier_state(event);
64 goOnScroll((unsigned long)user_data, dx, dy, state);
65 return FALSE; // don't steal regular scrolling
66 }
67 return FALSE;
68}
69
70static void attach_mouse_legacy(GtkWidget* widget, unsigned long id) {
71 if (!widget) return;
72 GtkEventController* legacy = gtk_event_controller_legacy_new();
73 gtk_event_controller_set_propagation_phase(legacy, GTK_PHASE_CAPTURE);
74 g_signal_connect_data(legacy, "event", G_CALLBACK(on_legacy_event), (gpointer)id, NULL, 0);
75 gtk_widget_add_controller(widget, legacy);
76}
77*/
78import "C"
79
80import (
81 "encoding/json"
82 "fmt"
83 "log"
84 "strings"
85 "sync"
86 "sync/atomic"
87 "time"
88)
89
90// Registry of accelerators per view id.
91type shortcutRegistry map[string]func()
92
93var (
94 regMu sync.RWMutex
95 viewShortcuts = make(map[uintptr]shortcutRegistry)
96 viewIDCounter uint64
97 viewByID = make(map[uintptr]*WebView)
98 lastKeyTime = make(map[uintptr]map[string]time.Time)
99)
100
101const shortcutDebounceWindow = 120 * time.Millisecond
102
103// shouldDebounceShortcut returns true when the given key should be skipped
104// because it was delivered within the debounce window for the same view.
105func shouldDebounceShortcut(uid uintptr, key string, now time.Time) bool {
106 regMu.Lock()
107 defer regMu.Unlock()
108
109 if _, ok := lastKeyTime[uid]; !ok {
110 lastKeyTime[uid] = make(map[string]time.Time)
111 }
112 last := lastKeyTime[uid][key]
113 if now.Sub(last) < shortcutDebounceWindow {
114 return true
115 }
116 lastKeyTime[uid][key] = now
117 return false
118}
119
120// Locale hint function removed; no layout-specific remaps.
121
122// RegisterKeyboardShortcut registers a callback under an accelerator string.
123func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error {
124 if w == nil || w.destroyed {
125 return ErrNotImplemented
126 }
127 if accel == "" || callback == nil {
128 return fmt.Errorf("invalid shortcut registration")
129 }
130 regMu.Lock()
131 if _, ok := viewShortcuts[w.id]; !ok {
132 viewShortcuts[w.id] = make(shortcutRegistry)
133 }
134 viewShortcuts[w.id][accel] = callback
135 regMu.Unlock()
136 return nil
137}
138
139//export goOnKeyPress
140func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolean {
141 // Normalize and dispatch
142 uid := uintptr(id)
143 kv := uint(keyval)
144 st := uint(state)
145 if dispatchAccelerator(uid, kv, st) {
146 return C.gboolean(1)
147 }
148 return C.gboolean(0)
149}
150
151func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
152 ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
153 alt := (state & uint(C.GDK_ALT_MASK)) != 0
154 shift := (state & uint(C.GDK_SHIFT_MASK)) != 0
155 // Map keyval to string names
156 var keyName string
157 logMinusEvent := false
158 switch keyval {
159 case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
160 // Normalize minus; log raw detection for diagnostics
161 logMinusEvent = true
162 keyName = "-"
163 case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add):
164 keyName = "="
165 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
166 keyName = "0"
167 case uint(C.GDK_KEY_Left):
168 keyName = "ArrowLeft"
169 case uint(C.GDK_KEY_Right):
170 keyName = "ArrowRight"
171 case uint(C.GDK_KEY_Up):
172 keyName = "ArrowUp"
173 case uint(C.GDK_KEY_Down):
174 keyName = "ArrowDown"
175 case uint(C.GDK_KEY_KP_Left):
176 keyName = "ArrowLeft"
177 case uint(C.GDK_KEY_KP_Right):
178 keyName = "ArrowRight"
179 case uint(C.GDK_KEY_KP_Up):
180 keyName = "ArrowUp"
181 case uint(C.GDK_KEY_KP_Down):
182 keyName = "ArrowDown"
183 case uint(C.GDK_KEY_F12):
184 keyName = "F12"
185 case uint(C.GDK_KEY_F5):
186 keyName = "F5"
187 case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C):
188 keyName = "c"
189 case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R):
190 keyName = "r"
191 case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
192 keyName = "l"
193 case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F):
194 keyName = "f"
195 case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P):
196 keyName = "p"
197 default:
198 // If Control held, log unknown keyval for diagnostics
199 if ctrl {
200 log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
201 }
202 return false
203 }
204
205 now := time.Now()
206 rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName)
207 if shouldDebounceShortcut(uid, rawKey, now) {
208 return false
209 }
210
211 if logMinusEvent {
212 log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval)
213 }
214
215 // Candidate accelerator strings in order
216 handled := false
217 candidates := []string{keyName}
218 if ctrl && shift {
219 // Ctrl+Shift combinations
220 candidates = append([]string{"cmdorctrl+shift+" + keyName, "ctrl+shift+" + keyName}, candidates...)
221 } else if ctrl {
222 // Common style: modifier + '+' + keyName (e.g., cmdorctrl+=)
223 candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
224 // Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-)
225 candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...)
226 }
227 if alt {
228 candidates = append([]string{"alt+" + keyName}, candidates...)
229 }
230
231 // Compute a single normalized shortcut string to forward to GUI (single source of truth)
232 normalized := ""
233 if ctrl && shift {
234 normalized = "cmdorctrl+shift+" + keyName
235 } else if ctrl {
236 normalized = "cmdorctrl+" + keyName
237 } else if alt {
238 normalized = "alt+" + keyName
239 } else if keyName != "" {
240 // For future modes like Vim, we may want to forward non-modified keys
241 // For now, only forward if a modifier is present to avoid noise
242 normalized = ""
243 }
244
245 // Forward normalized shortcut to GUI KeyboardService via DOM event if present
246 if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) {
247 regMu.RLock()
248 vw := viewByID[uid]
249 regMu.RUnlock()
250 if vw != nil {
251 _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", normalized))
252 }
253 }
254
255 regMu.RLock()
256 reg := viewShortcuts[uid]
257 regMu.RUnlock()
258 if reg == nil {
259 return handled
260 }
261 for _, name := range candidates {
262 if cb, ok := reg[name]; ok {
263 if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
264 return handled
265 }
266 log.Printf("[accelerator] %s", name)
267 cb()
268 handled = true
269 break
270 }
271 }
272
273 return handled
274}
275
276func nextViewID() uintptr {
277 return uintptr(atomic.AddUint64(&viewIDCounter, 1))
278}
279
280func registerView(id uintptr, w *WebView) {
281 regMu.Lock()
282 viewByID[id] = w
283 regMu.Unlock()
284}
285
286func unregisterView(id uintptr) {
287 regMu.Lock()
288 delete(viewByID, id)
289 delete(viewShortcuts, id)
290 regMu.Unlock()
291}
292
293// AttachKeyboardControllers attaches GTK4 key and mouse controllers to the view widget.
294func AttachKeyboardControllers(w *WebView) {
295 if w == nil || w.native == nil || w.native.view == nil {
296 return
297 }
298 C.attach_key_controller(w.native.view, C.ulong(w.id))
299 if w.native.win != nil {
300 C.attach_key_controller(w.native.win, C.ulong(w.id))
301 }
302 C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
303 // Legacy controller captures raw button presses (8/9) for back/forward reliably
304 C.attach_mouse_legacy(w.native.view, C.ulong(w.id))
305 log.Printf("[input] GTK4 key/mouse controllers attached")
306}
307
308//export goOnScroll
309func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) {
310 uid := uintptr(id)
311 regMu.RLock()
312 vw := viewByID[uid]
313 regMu.RUnlock()
314 if vw == nil {
315 return
316 }
317 // Ctrl+scroll zooms
318 if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 {
319 if float64(dy) < 0 {
320 // scroll up -> zoom in
321 nz := vw.zoom
322 if nz <= 0 {
323 nz = 1.0
324 }
325 nz *= 1.1
326 if nz < 0.25 {
327 nz = 0.25
328 }
329 if nz > 5.0 {
330 nz = 5.0
331 }
332 _ = vw.SetZoom(nz)
333 } else if float64(dy) > 0 {
334 // scroll down -> zoom out
335 nz := vw.zoom
336 if nz <= 0 {
337 nz = 1.0
338 }
339 nz /= 1.1
340 if nz < 0.25 {
341 nz = 0.25
342 }
343 if nz > 5.0 {
344 nz = 5.0
345 }
346 _ = vw.SetZoom(nz)
347 }
348 }
349}
350
351//export goOnThemeChanged
352func goOnThemeChanged(id C.ulong, preferDark C.int) {
353 uid := uintptr(id)
354 regMu.RLock()
355 vw := viewByID[uid]
356 regMu.RUnlock()
357 if vw == nil {
358 return
359 }
360 // Inject runtime theme update into the page
361 dval := "false"
362 if int(preferDark) != 0 {
363 dval = "true"
364 }
365 js := fmt.Sprintf(`(() => { try { const d=%s; const cs=d?'dark':'light'; console.log('[dumber] theme change:'+cs);
366let m=document.querySelector('meta[name="color-scheme"]');
367if(!m){ m=document.createElement('meta'); m.name='color-scheme'; document.head.appendChild(m); }
368m.setAttribute('content', d ? 'dark light' : 'light dark');
369let s=document.getElementById('__dumber_theme_style');
370if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
371s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
372// Call the unified theme setter for Tailwind dark mode
373if (window.__dumber_setTheme) {
374 window.__dumber_setTheme(cs);
375} else {
376 // Retry after a short delay in case bridge hasn't loaded yet
377 setTimeout(() => {
378 if (window.__dumber_setTheme) {
379 window.__dumber_setTheme(cs);
380 } else {
381 // Fallback: directly apply dark class
382 console.warn('[dumber] Theme setter not available, using fallback');
383 if (d) {
384 document.documentElement.classList.add('dark');
385 } else {
386 document.documentElement.classList.remove('dark');
387 }
388 }
389 }, 100);
390}
391} catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
392 _ = vw.InjectScript(js)
393}
394
395//export goOnUcmMessage
396func goOnUcmMessage(id C.ulong, json *C.char) {
397 uid := uintptr(id)
398 regMu.RLock()
399 vw := viewByID[uid]
400 regMu.RUnlock()
401 if vw == nil || json == nil {
402 return
403 }
404 goPayload := C.GoString(json)
405
406 // Handle rendering backend detection messages specially
407 if handleRenderingBackendMessage(goPayload) {
408 return
409 }
410
411 vw.dispatchScriptMessage(goPayload)
412}
413
414//export goOnTitleChanged
415func goOnTitleChanged(id C.ulong, ctitle *C.char) {
416 uid := uintptr(id)
417 regMu.RLock()
418 vw := viewByID[uid]
419 regMu.RUnlock()
420 if vw == nil || ctitle == nil {
421 return
422 }
423 title := C.GoString(ctitle)
424 vw.dispatchTitleChanged(title)
425}
426
427//export goOnURIChanged
428func goOnURIChanged(id C.ulong, curi *C.char) {
429 uid := uintptr(id)
430 regMu.RLock()
431 vw := viewByID[uid]
432 regMu.RUnlock()
433 if vw == nil || curi == nil {
434 return
435 }
436 uri := C.GoString(curi)
437 vw.dispatchURIChanged(uri)
438}
439
440//export goHandleNewWindowPolicy
441func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int, cframeName *C.char, isUserGesture C.gboolean, modifiers C.guint, mouseButton C.guint) C.gboolean {
442 uid := uintptr(id)
443 regMu.RLock()
444 vw := viewByID[uid]
445 regMu.RUnlock()
446
447 if vw == nil {
448 log.Printf("[webkit] Policy decision: no WebView found for ID %d", uid)
449 return 0 // FALSE
450 }
451
452 uri := ""
453 if curi != nil {
454 uri = C.GoString(curi)
455 }
456
457 frameName := ""
458 if cframeName != nil {
459 frameName = C.GoString(cframeName)
460 }
461
462 // Navigation type constants from WebKit headers
463 navTypeNames := map[int]string{
464 0: "LINK_CLICKED",
465 1: "FORM_SUBMITTED",
466 2: "BACK_FORWARD",
467 3: "RELOAD",
468 4: "FORM_RESUBMITTED",
469 5: "OTHER",
470 }
471
472 navTypeName := navTypeNames[int(navType)]
473 if navTypeName == "" {
474 navTypeName = fmt.Sprintf("UNKNOWN(%d)", int(navType))
475 }
476
477 // Convert parameters for analysis
478 userGesture := isUserGesture != 0
479 mods := uint(modifiers)
480 button := uint(mouseButton)
481
482 // Check for modifier keys that indicate new tab intent
483 const (
484 GDK_CONTROL_MASK = 0x04
485 GDK_META_MASK = 0x10000000 // Command key on macOS
486 )
487 hasCtrlOrCmd := (mods&GDK_CONTROL_MASK != 0) || (mods&GDK_META_MASK != 0)
488
489 log.Printf("[webkit] Policy decision for new window: URI=%s, NavType=%s, FrameName=%s, UserGesture=%v, Modifiers=0x%x, Button=%d, CtrlOrCmd=%v",
490 uri, navTypeName, frameName, userGesture, mods, button, hasCtrlOrCmd)
491
492 // Enhanced detection logic for _blank vs popup using reliable heuristics
493 isRegularPane := false
494
495 // Criteria for treating as regular pane (new panel):
496 // 1. Explicit _blank frame name (target="_blank" links)
497 // 2. User gesture with Ctrl/Cmd+click (open in new panel intent)
498 // 3. User-initiated OTHER navigation with no frame name (window.open("url", "_blank"))
499 if frameName == "_blank" {
500 isRegularPane = true
501 log.Printf("[webkit] _blank frame detected - will handle as new panel")
502 } else if userGesture && hasCtrlOrCmd && (navType == 0 || navType == 5) {
503 // WEBKIT_NAVIGATION_TYPE_LINK_CLICKED = 0, WEBKIT_NAVIGATION_TYPE_OTHER = 5
504 isRegularPane = true
505 log.Printf("[webkit] Ctrl/Cmd+click detected - will handle as new panel")
506 } else if userGesture && navType == 5 && frameName == "" {
507 // window.open("url", "_blank") typically: user gesture + OTHER + no frame name
508 isRegularPane = true
509 log.Printf("[webkit] User-initiated window.open() detected - will handle as new panel")
510 }
511
512 // Handle _blank targets by creating new panels directly (prevent WindowFeatures crash)
513 if isRegularPane {
514 log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
515
516 // Create new panel through workspace manager using existing popup handler
517 if vw.popupHandler != nil {
518 // Call popup handler to create a new panel
519 // The workspace manager will handle this as a regular panel based on URI markers
520 vw.popupHandler(uri + "#__dumber_frame_blank")
521 }
522
523 return 1 // TRUE - we handled it by creating a new panel
524 }
525
526 // For actual popups, let the create signal handle it
527 if vw.popupHandler != nil {
528 log.Printf("[webkit] Letting popup go through create signal for WindowFeatures handling: %s", uri)
529 return 0 // FALSE - let WebKit call create signal for popup handling
530 } else {
531 log.Printf("[webkit] No popup handler registered - allowing native popup for: %s", uri)
532 return 0 // FALSE - no handler, allow default popup
533 }
534}
535
536//export goHandleLoadChanged
537func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) {
538 uid := uintptr(id)
539 regMu.RLock()
540 vw := viewByID[uid]
541 regMu.RUnlock()
542
543 if vw == nil {
544 return
545 }
546
547 // OAuth auto-close is handled via WebKit's "close" signal when providers call window.close()
548 // No need to detect OAuth callback URLs - let the providers handle it
549 _ = curi // Suppress unused parameter warning
550}
551
552//export goHandleWebViewClose
553func goHandleWebViewClose(id C.ulong) {
554 uid := uintptr(id)
555 regMu.RLock()
556 vw := viewByID[uid]
557 regMu.RUnlock()
558
559 if vw == nil {
560 log.Printf("[webkit] WebView close signal: no WebView found for ID %d", uid)
561 return
562 }
563
564 log.Printf("[webkit] WebView close signal received for view_id: %d", uid)
565
566 if vw.closeHandler != nil {
567 log.Printf("[webkit] Calling close handler for view_id: %d", uid)
568 vw.closeHandler()
569 } else {
570 log.Printf("[webkit] No close handler registered for view_id: %d", uid)
571 }
572}
573
574//export goCreateNewPanel
575func goCreateNewPanel(id C.ulong, curi *C.char) {
576 uid := uintptr(id)
577 regMu.RLock()
578 vw := viewByID[uid]
579 regMu.RUnlock()
580
581 if vw == nil {
582 log.Printf("[webkit] Create panel: no WebView found for ID %d", uid)
583 return
584 }
585
586 uri := ""
587 if curi != nil {
588 uri = C.GoString(curi)
589 }
590
591 log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
592
593 // Create new panel through popup handler if available
594 if vw.popupHandler != nil {
595 vw.popupHandler(uri)
596 log.Printf("[webkit] New panel creation handled by popup handler")
597 } else {
598 log.Printf("[webkit] No popup handler available for panel creation")
599 }
600}
601
602//export goHandlePopupGeometry
603func goHandlePopupGeometry(id C.ulong, x, y, width, height C.int) {
604 // Store geometry for workspace manager to use
605 uid := uintptr(id)
606 regMu.Lock()
607 if vw := viewByID[uid]; vw != nil {
608 // For now, we'll store this in a simple way
609 // The workspace manager will handle the geometry
610 log.Printf("[webkit] Popup geometry: %dx%d at (%d,%d)",
611 int(width), int(height), int(x), int(y))
612 }
613 regMu.Unlock()
614}
615
616//export goOnButtonPress
617func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) {
618 uid := uintptr(id)
619 btn := uint(button)
620 log.Printf("[mouse] button=%d state=0x%x", btn, uint(state))
621 regMu.RLock()
622 vw := viewByID[uid]
623 regMu.RUnlock()
624 if vw == nil {
625 return
626 }
627 switch btn {
628 case 8: // Back button on many mice
629 _ = vw.GoBack()
630 case 9: // Forward button on many mice
631 _ = vw.GoForward()
632 }
633}
634
635//export goQuitMainLoop
636func goQuitMainLoop() {
637 log.Printf("[webkit] Window close requested - quitting main loop")
638 QuitMainLoop()
639}
640
641// handleRenderingBackendMessage processes rendering backend detection messages
642func handleRenderingBackendMessage(payload string) bool {
643 // Parse JSON to check if this is a rendering backend detection message
644 if !strings.Contains(payload, "rendering_backend_detection") {
645 return false
646 }
647
648 // Simple JSON parsing for the specific structure we expect
649 // {"type":"rendering_backend_detection","data":{...}}
650 var msg struct {
651 Type string `json:"type"`
652 Data struct {
653 WebGLAvailable bool `json:"webgl_available"`
654 Renderer string `json:"renderer"`
655 Vendor string `json:"vendor"`
656 Version string `json:"version"`
657 ShadingLanguageVersion string `json:"shading_language_version"`
658 MaxTextureSize int `json:"max_texture_size"`
659 HardwareAccelerated bool `json:"hardware_accelerated"`
660 } `json:"data"`
661 }
662
663 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
664 log.Printf("[webkit] Failed to parse rendering backend message: %v", err)
665 return false
666 }
667
668 if msg.Type != "rendering_backend_detection" {
669 return false
670 }
671
672 // Log the detected rendering backend information
673 backendType := "SOFTWARE"
674 if msg.Data.HardwareAccelerated {
675 backendType = "HARDWARE"
676 }
677
678 log.Printf("[webkit] Detected rendering backend: %s", backendType)
679 log.Printf("[webkit] WebGL available: %v", msg.Data.WebGLAvailable)
680 log.Printf("[webkit] Renderer: %s", msg.Data.Renderer)
681 log.Printf("[webkit] Vendor: %s", msg.Data.Vendor)
682 log.Printf("[webkit] Version: %s", msg.Data.Version)
683 log.Printf("[webkit] Max texture size: %d", msg.Data.MaxTextureSize)
684
685 return true
686}