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
137 // Register in global shortcuts registry for automatic webpage blocking
138 RegisterGlobalShortcut(accel)
139
140 return nil
141}
142
143//export goOnKeyPress
144func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolean {
145 // Normalize and dispatch
146 uid := uintptr(id)
147 kv := uint(keyval)
148 st := uint(state)
149
150 if dispatchAccelerator(uid, kv, st) {
151 return C.gboolean(1)
152 }
153 return C.gboolean(0)
154}
155
156// getCLevelBridgeShortcut returns the shortcut string for shortcuts that need C-level bridge handling.
157// These shortcuts are blocked at the webpage level and have their callbacks triggered directly from C code.
158// Returns empty string if the shortcut doesn't need C-level handling.
159func getCLevelBridgeShortcut(ctrl, alt, shift bool, keyName string) string {
160 // Alt+Arrow shortcuts - for workspace pane navigation
161 if alt && !ctrl && !shift {
162 switch keyName {
163 case "ArrowLeft":
164 return "alt+arrowleft"
165 case "ArrowRight":
166 return "alt+arrowright"
167 case "ArrowUp":
168 return "alt+arrowup"
169 case "ArrowDown":
170 return "alt+arrowdown"
171 }
172 }
173
174 // Ctrl+W shortcut - for closing current pane via workspace manager
175 if ctrl && !alt && !shift && keyName == "w" {
176 return "ctrl+w"
177 }
178
179 // Add other C-level bridge shortcuts here as needed
180 // Note: Most shortcuts (Ctrl+L, Ctrl+F, zoom) should NOT be here
181 // They work fine through normal GTK4 handling without C-level intervention
182
183 return ""
184}
185
186func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
187 ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
188 alt := (state & uint(C.GDK_ALT_MASK)) != 0
189 shift := (state & uint(C.GDK_SHIFT_MASK)) != 0
190 // Map keyval to string names
191 var keyName string
192 logMinusEvent := false
193 switch keyval {
194 case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
195 // Normalize minus; log raw detection for diagnostics
196 logMinusEvent = true
197 keyName = "-"
198 case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add):
199 keyName = "="
200 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
201 keyName = "0"
202 case uint(C.GDK_KEY_Left):
203 keyName = "ArrowLeft"
204 case uint(C.GDK_KEY_Right):
205 keyName = "ArrowRight"
206 case uint(C.GDK_KEY_Up):
207 keyName = "ArrowUp"
208 case uint(C.GDK_KEY_Down):
209 keyName = "ArrowDown"
210 case uint(C.GDK_KEY_KP_Left):
211 keyName = "ArrowLeft"
212 case uint(C.GDK_KEY_KP_Right):
213 keyName = "ArrowRight"
214 case uint(C.GDK_KEY_KP_Up):
215 keyName = "ArrowUp"
216 case uint(C.GDK_KEY_KP_Down):
217 keyName = "ArrowDown"
218 case uint(C.GDK_KEY_F12):
219 keyName = "F12"
220 case uint(C.GDK_KEY_F5):
221 keyName = "F5"
222 case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C):
223 keyName = "c"
224 case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R):
225 keyName = "r"
226 case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
227 keyName = "l"
228 case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F):
229 keyName = "f"
230 case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P):
231 keyName = "p"
232 case uint(C.GDK_KEY_w), uint(C.GDK_KEY_W):
233 keyName = "w"
234 default:
235 // Filter out modifier keys themselves (like Ctrl, Alt, Shift)
236 // These keyvals shouldn't be logged as misses
237 switch keyval {
238 case 0xffe1, 0xffe2: // GDK_KEY_Shift_L, GDK_KEY_Shift_R
239 return false
240 case 0xffe3, 0xffe4: // GDK_KEY_Control_L, GDK_KEY_Control_R
241 return false
242 case 0xffe9, 0xffea: // GDK_KEY_Alt_L, GDK_KEY_Alt_R
243 return false
244 case 0xffeb, 0xffec: // GDK_KEY_Super_L, GDK_KEY_Super_R
245 return false
246 }
247
248 // If Control held, log unknown keyval for diagnostics (but not modifier keys)
249 if ctrl {
250 log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
251 }
252 return false
253 }
254
255 // Check for shortcuts that need C-level handling and trigger if found
256 shortcut := getCLevelBridgeShortcut(ctrl, alt, shift, keyName)
257 if shortcut != "" {
258 // For all C-level bridge shortcuts (Alt+Arrow and Ctrl+W), use global window shortcut handler
259 // The window shortcut handler now properly handles both popup and regular panes
260 shortcutsRegistryMu.RLock()
261 handle, exists := globalShortcutHandles[shortcut]
262 shortcutsRegistryMu.RUnlock()
263
264 if exists && handle != 0 {
265 invokeWindowShortcutCallback(handle)
266 return true
267 }
268 }
269
270 now := time.Now()
271 rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName)
272 if shouldDebounceShortcut(uid, rawKey, now) {
273 return false
274 }
275
276 if logMinusEvent {
277 log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval)
278 }
279
280 // Candidate accelerator strings in order
281 handled := false
282 candidates := []string{keyName}
283 if ctrl && shift {
284 // Ctrl+Shift combinations
285 candidates = append([]string{"cmdorctrl+shift+" + keyName, "ctrl+shift+" + keyName}, candidates...)
286 } else if ctrl {
287 // Common style: modifier + '+' + keyName (e.g., cmdorctrl+=)
288 candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
289 // Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-)
290 candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...)
291 }
292 if alt {
293 candidates = append([]string{"alt+" + keyName}, candidates...)
294 }
295
296 // Compute a single normalized shortcut string to forward to GUI (single source of truth)
297 normalized := ""
298 if ctrl && shift {
299 normalized = "cmdorctrl+shift+" + keyName
300 } else if ctrl {
301 normalized = "cmdorctrl+" + keyName
302 } else if alt {
303 normalized = "alt+" + keyName
304 } else if keyName != "" {
305 // For future modes like Vim, we may want to forward non-modified keys
306 // For now, only forward if a modifier is present to avoid noise
307 normalized = ""
308 }
309
310 // Forward normalized shortcut to GUI KeyboardService via DOM event if present
311 if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) {
312 regMu.RLock()
313 vw := viewByID[uid]
314 regMu.RUnlock()
315 if vw != nil {
316 _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", normalized))
317 }
318 }
319
320 regMu.RLock()
321 reg := viewShortcuts[uid]
322 regMu.RUnlock()
323 if reg == nil {
324 return handled
325 }
326
327 for _, name := range candidates {
328 if cb, ok := reg[name]; ok {
329 if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
330 return handled
331 }
332 log.Printf("[accelerator] %s", name)
333 cb()
334 handled = true
335 break
336 }
337 }
338
339 return handled
340}
341
342func nextViewID() uintptr {
343 return uintptr(atomic.AddUint64(&viewIDCounter, 1))
344}
345
346func registerView(id uintptr, w *WebView) {
347 regMu.Lock()
348 viewByID[id] = w
349 regMu.Unlock()
350}
351
352func unregisterView(id uintptr) {
353 regMu.Lock()
354 delete(viewByID, id)
355 delete(viewShortcuts, id)
356 regMu.Unlock()
357}
358
359// AttachKeyboardControllers attaches GTK4 key and mouse controllers to the view widget.
360func AttachKeyboardControllers(w *WebView) {
361 if w == nil || w.native == nil || w.native.view == nil {
362 return
363 }
364 C.attach_key_controller(w.native.view, C.ulong(w.id))
365 if w.native.win != nil {
366 C.attach_key_controller(w.native.win, C.ulong(w.id))
367 }
368 C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
369 // Legacy controller captures raw button presses (8/9) for back/forward reliably
370 C.attach_mouse_legacy(w.native.view, C.ulong(w.id))
371 log.Printf("[input] GTK4 key/mouse controllers attached")
372}
373
374//export goOnScroll
375func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) {
376 uid := uintptr(id)
377 regMu.RLock()
378 vw := viewByID[uid]
379 regMu.RUnlock()
380 if vw == nil {
381 return
382 }
383 // Ctrl+scroll zooms
384 if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 {
385 if float64(dy) < 0 {
386 // scroll up -> zoom in
387 nz := vw.zoom
388 if nz <= 0 {
389 nz = 1.0
390 }
391 nz *= 1.1
392 if nz < 0.25 {
393 nz = 0.25
394 }
395 if nz > 5.0 {
396 nz = 5.0
397 }
398 _ = vw.SetZoom(nz)
399 } else if float64(dy) > 0 {
400 // scroll down -> zoom out
401 nz := vw.zoom
402 if nz <= 0 {
403 nz = 1.0
404 }
405 nz /= 1.1
406 if nz < 0.25 {
407 nz = 0.25
408 }
409 if nz > 5.0 {
410 nz = 5.0
411 }
412 _ = vw.SetZoom(nz)
413 }
414 }
415}
416
417//export goOnThemeChanged
418func goOnThemeChanged(id C.ulong, preferDark C.int) {
419 uid := uintptr(id)
420 regMu.RLock()
421 vw := viewByID[uid]
422 regMu.RUnlock()
423 if vw == nil {
424 return
425 }
426 // Inject runtime theme update into the page
427 dval := "false"
428 if int(preferDark) != 0 {
429 dval = "true"
430 }
431 js := fmt.Sprintf(`(() => { try { const d=%s; const cs=d?'dark':'light'; console.log('[dumber] theme change:'+cs);
432let m=document.querySelector('meta[name="color-scheme"]');
433if(!m){ m=document.createElement('meta'); m.name='color-scheme'; document.head.appendChild(m); }
434m.setAttribute('content', d ? 'dark light' : 'light dark');
435let s=document.getElementById('__dumber_theme_style');
436if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
437s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
438// Call the unified theme setter for Tailwind dark mode
439if (window.__dumber_setTheme) {
440 window.__dumber_setTheme(cs);
441} else {
442 // Retry after a short delay in case bridge hasn't loaded yet
443 setTimeout(() => {
444 if (window.__dumber_setTheme) {
445 window.__dumber_setTheme(cs);
446 } else {
447 // Fallback: directly apply dark class
448 console.warn('[dumber] Theme setter not available, using fallback');
449 if (d) {
450 document.documentElement.classList.add('dark');
451 } else {
452 document.documentElement.classList.remove('dark');
453 }
454 }
455 }, 100);
456}
457} catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
458 _ = vw.InjectScript(js)
459}
460
461//export goOnUcmMessage
462func goOnUcmMessage(id C.ulong, json *C.char) {
463 uid := uintptr(id)
464 regMu.RLock()
465 vw := viewByID[uid]
466 regMu.RUnlock()
467 if vw == nil || json == nil {
468 return
469 }
470 goPayload := C.GoString(json)
471
472 // Handle rendering backend detection messages specially
473 if handleRenderingBackendMessage(goPayload) {
474 return
475 }
476
477 // Handle keyboard blocking messages
478 if handleKeyboardBlockingMessage(vw, goPayload) {
479 return
480 }
481
482 vw.dispatchScriptMessage(goPayload)
483}
484
485//export goOnTitleChanged
486func goOnTitleChanged(id C.ulong, ctitle *C.char) {
487 uid := uintptr(id)
488 regMu.RLock()
489 vw := viewByID[uid]
490 regMu.RUnlock()
491 if vw == nil || ctitle == nil {
492 return
493 }
494 title := C.GoString(ctitle)
495 vw.dispatchTitleChanged(title)
496}
497
498//export goOnURIChanged
499func goOnURIChanged(id C.ulong, curi *C.char) {
500 uid := uintptr(id)
501 regMu.RLock()
502 vw := viewByID[uid]
503 regMu.RUnlock()
504 if vw == nil || curi == nil {
505 return
506 }
507 uri := C.GoString(curi)
508 vw.dispatchURIChanged(uri)
509}
510
511//export goOnFaviconURIChanged
512func goOnFaviconURIChanged(id C.ulong, pageURI *C.char, faviconURI *C.char) {
513 uid := uintptr(id)
514 pageURLStr := C.GoString(pageURI)
515 faviconURLStr := C.GoString(faviconURI)
516
517 log.Printf("[favicon] WebKit favicon URI changed for WebView ID %d - favicon: %s", uid, faviconURLStr)
518
519 regMu.RLock()
520 vw := viewByID[uid]
521 regMu.RUnlock()
522 if vw == nil {
523 log.Printf("[favicon] WebView not found for ID %d", uid)
524 return
525 }
526
527 vw.dispatchFaviconURIChanged(pageURLStr, faviconURLStr)
528}
529
530//export goHandleNewWindowPolicy
531func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int, cframeName *C.char, isUserGesture C.gboolean, modifiers C.guint, mouseButton C.guint) C.gboolean {
532 uid := uintptr(id)
533 regMu.RLock()
534 vw := viewByID[uid]
535 regMu.RUnlock()
536
537 if vw == nil {
538 log.Printf("[webkit] Policy decision: no WebView found for ID %d", uid)
539 return 0 // FALSE
540 }
541
542 uri := ""
543 if curi != nil {
544 uri = C.GoString(curi)
545 }
546
547 frameName := ""
548 if cframeName != nil {
549 frameName = C.GoString(cframeName)
550 }
551
552 // Navigation type constants from WebKit headers
553 navTypeNames := map[int]string{
554 0: "LINK_CLICKED",
555 1: "FORM_SUBMITTED",
556 2: "BACK_FORWARD",
557 3: "RELOAD",
558 4: "FORM_RESUBMITTED",
559 5: "OTHER",
560 }
561
562 navTypeName := navTypeNames[int(navType)]
563 if navTypeName == "" {
564 navTypeName = fmt.Sprintf("UNKNOWN(%d)", int(navType))
565 }
566
567 // Convert parameters for analysis
568 userGesture := isUserGesture != 0
569 mods := uint(modifiers)
570 button := uint(mouseButton)
571
572 // Check for modifier keys that indicate new tab intent
573 const (
574 GDK_CONTROL_MASK = 0x04
575 GDK_META_MASK = 0x10000000 // Command key on macOS
576 )
577 hasCtrlOrCmd := (mods&GDK_CONTROL_MASK != 0) || (mods&GDK_META_MASK != 0)
578
579 log.Printf("[webkit] Policy decision for new window: URI=%s, NavType=%s, FrameName=%s, UserGesture=%v, Modifiers=0x%x, Button=%d, CtrlOrCmd=%v",
580 uri, navTypeName, frameName, userGesture, mods, button, hasCtrlOrCmd)
581
582 // Enhanced detection logic for _blank vs popup using reliable heuristics
583 isRegularPane := false
584
585 // Criteria for treating as regular pane (new panel):
586 // 1. Explicit _blank frame name (target="_blank" links)
587 // 2. User gesture with Ctrl/Cmd+click (open in new panel intent)
588 // 3. User-initiated OTHER navigation with no frame name (window.open("url", "_blank"))
589 if frameName == "_blank" {
590 isRegularPane = true
591 log.Printf("[webkit] _blank frame detected - will handle as new panel")
592 } else if userGesture && hasCtrlOrCmd && (navType == 0 || navType == 5) {
593 // WEBKIT_NAVIGATION_TYPE_LINK_CLICKED = 0, WEBKIT_NAVIGATION_TYPE_OTHER = 5
594 isRegularPane = true
595 log.Printf("[webkit] Ctrl/Cmd+click detected - will handle as new panel")
596 } else if userGesture && navType == 5 && frameName == "" {
597 // window.open("url", "_blank") typically: user gesture + OTHER + no frame name
598 isRegularPane = true
599 log.Printf("[webkit] User-initiated window.open() detected - will handle as new panel")
600 }
601
602 // Handle _blank targets by creating new panels directly (prevent WindowFeatures crash)
603 if isRegularPane {
604 log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
605
606 // Create new panel through workspace manager using existing popup handler
607 if vw.popupHandler != nil {
608 // Call popup handler to create a new panel
609 // The workspace manager will handle this as a regular panel based on URI markers
610 vw.popupHandler(uri + "#__dumber_frame_blank")
611 }
612
613 return 1 // TRUE - we handled it by creating a new panel
614 }
615
616 // For actual popups, let the create signal handle it
617 if vw.popupHandler != nil {
618 log.Printf("[webkit] Letting popup go through create signal for WindowFeatures handling: %s", uri)
619 return 0 // FALSE - let WebKit call create signal for popup handling
620 } else {
621 log.Printf("[webkit] No popup handler registered - allowing native popup for: %s", uri)
622 return 0 // FALSE - no handler, allow default popup
623 }
624}
625
626//export goHandleLoadChanged
627func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) {
628 uid := uintptr(id)
629 regMu.RLock()
630 vw := viewByID[uid]
631 regMu.RUnlock()
632
633 if vw == nil {
634 return
635 }
636
637 // OAuth auto-close is handled via WebKit's "close" signal when providers call window.close()
638 // No need to detect OAuth callback URLs - let the providers handle it
639 _ = curi // Suppress unused parameter warning
640}
641
642//export goHandleWebViewClose
643func goHandleWebViewClose(id C.ulong) {
644 uid := uintptr(id)
645 regMu.RLock()
646 vw := viewByID[uid]
647 regMu.RUnlock()
648
649 if vw == nil {
650 log.Printf("[webkit] WebView close signal: no WebView found for ID %d", uid)
651 return
652 }
653
654 log.Printf("[webkit] WebView close signal received for view_id: %d", uid)
655
656 if vw.closeHandler != nil {
657 log.Printf("[webkit] Calling close handler for view_id: %d", uid)
658 vw.closeHandler()
659 } else {
660 log.Printf("[webkit] No close handler registered for view_id: %d", uid)
661 }
662}
663
664//export goCreateNewPanel
665func goCreateNewPanel(id C.ulong, curi *C.char) {
666 uid := uintptr(id)
667 regMu.RLock()
668 vw := viewByID[uid]
669 regMu.RUnlock()
670
671 if vw == nil {
672 log.Printf("[webkit] Create panel: no WebView found for ID %d", uid)
673 return
674 }
675
676 uri := ""
677 if curi != nil {
678 uri = C.GoString(curi)
679 }
680
681 log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
682
683 // Create new panel through popup handler if available
684 if vw.popupHandler != nil {
685 vw.popupHandler(uri)
686 log.Printf("[webkit] New panel creation handled by popup handler")
687 } else {
688 log.Printf("[webkit] No popup handler available for panel creation")
689 }
690}
691
692//export goHandlePopupGeometry
693func goHandlePopupGeometry(id C.ulong, x, y, width, height C.int) {
694 // Store geometry for workspace manager to use
695 uid := uintptr(id)
696 regMu.Lock()
697 if vw := viewByID[uid]; vw != nil {
698 // For now, we'll store this in a simple way
699 // The workspace manager will handle the geometry
700 log.Printf("[webkit] Popup geometry: %dx%d at (%d,%d)",
701 int(width), int(height), int(x), int(y))
702 }
703 regMu.Unlock()
704}
705
706//export goOnButtonPress
707func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) {
708 uid := uintptr(id)
709 btn := uint(button)
710 log.Printf("[mouse] button=%d state=0x%x", btn, uint(state))
711 regMu.RLock()
712 vw := viewByID[uid]
713 regMu.RUnlock()
714 if vw == nil {
715 return
716 }
717 switch btn {
718 case 8: // Back button on many mice
719 _ = vw.GoBack()
720 case 9: // Forward button on many mice
721 _ = vw.GoForward()
722 }
723}
724
725//export goQuitMainLoop
726func goQuitMainLoop() {
727 log.Printf("[webkit] Window close requested - quitting main loop")
728 QuitMainLoop()
729}
730
731// handleRenderingBackendMessage processes rendering backend detection messages
732func handleRenderingBackendMessage(payload string) bool {
733 // Parse JSON to check if this is a rendering backend detection message
734 if !strings.Contains(payload, "rendering_backend_detection") {
735 return false
736 }
737
738 // Simple JSON parsing for the specific structure we expect
739 // {"type":"rendering_backend_detection","data":{...}}
740 var msg struct {
741 Type string `json:"type"`
742 Data struct {
743 WebGLAvailable bool `json:"webgl_available"`
744 Renderer string `json:"renderer"`
745 Vendor string `json:"vendor"`
746 Version string `json:"version"`
747 ShadingLanguageVersion string `json:"shading_language_version"`
748 MaxTextureSize int `json:"max_texture_size"`
749 HardwareAccelerated bool `json:"hardware_accelerated"`
750 } `json:"data"`
751 }
752
753 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
754 log.Printf("[webkit] Failed to parse rendering backend message: %v", err)
755 return false
756 }
757
758 if msg.Type != "rendering_backend_detection" {
759 return false
760 }
761
762 // Log the detected rendering backend information
763 backendType := "SOFTWARE"
764 if msg.Data.HardwareAccelerated {
765 backendType = "HARDWARE"
766 }
767
768 log.Printf("[webkit] Detected rendering backend: %s", backendType)
769 log.Printf("[webkit] WebGL available: %v", msg.Data.WebGLAvailable)
770 log.Printf("[webkit] Renderer: %s", msg.Data.Renderer)
771 log.Printf("[webkit] Vendor: %s", msg.Data.Vendor)
772 log.Printf("[webkit] Version: %s", msg.Data.Version)
773 log.Printf("[webkit] Max texture size: %d", msg.Data.MaxTextureSize)
774
775 return true
776}
777
778// handleKeyboardBlockingMessage handles keyboard blocking control messages from omnibox
779func handleKeyboardBlockingMessage(vw *WebView, payload string) bool {
780 if vw == nil || vw.IsDestroyed() {
781 return false
782 }
783
784 var msg struct {
785 Type string `json:"type"`
786 Action string `json:"action"`
787 }
788
789 if err := json.Unmarshal([]byte(payload), &msg); err != nil {
790 return false
791 }
792
793 if msg.Type != "keyboard_blocking" {
794 return false
795 }
796
797 switch msg.Action {
798 case "enable":
799 log.Printf("[webkit] Enabling page keyboard blocking for omnibox")
800 if err := vw.EnablePageKeyboardBlocking(); err != nil {
801 log.Printf("[webkit] Failed to enable keyboard blocking: %v", err)
802 }
803 case "disable":
804 log.Printf("[webkit] Disabling page keyboard blocking")
805 if err := vw.DisablePageKeyboardBlocking(); err != nil {
806 log.Printf("[webkit] Failed to disable keyboard blocking: %v", err)
807 }
808 default:
809 log.Printf("[webkit] Unknown keyboard blocking action: %s", msg.Action)
810 }
811
812 return true
813}