1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: webkitgtk-6.0 gtk4 javascriptcoregtk-6.0
7#include <stdlib.h>
8#include <gtk/gtk.h>
9#include <webkit/webkit.h>
10#include <glib-object.h>
11#include <gdk/gdk.h>
12#include <jsc/jsc.h>
13#include <glib.h>
14#include <gio/gio.h>
15
16static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
17// GTK4: gtk_main_quit removed; leave as no-op for now (Quit handled in Go)
18static void connect_destroy_quit(GtkWidget* w) { (void)w; }
19static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
20// WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code.
21
22// Forward declare helpers used below
23static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
24
25// Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
26static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm, WebKitMemoryPressureSettings* pressure_settings) {
27 WebKitUserContentManager* u = webkit_user_content_manager_new();
28 if (!u) return NULL;
29 // WebKitGTK 6: create NetworkSession with data/cache directories
30 WebKitNetworkSession* sess = webkit_network_session_new(
31 data_dir ? data_dir : "",
32 cache_dir ? cache_dir : "");
33 if (!sess) { g_object_unref(u); return NULL; }
34 // Apply memory pressure settings to the session when provided
35 if (pressure_settings) {
36 // WebKitGTK 6.0 API: setter expects only the settings pointer
37 webkit_network_session_set_memory_pressure_settings(pressure_settings);
38 }
39 // Configure cookie persistence and policy (no third-party by default)
40 WebKitCookieManager* cm = webkit_network_session_get_cookie_manager(sess);
41 if (cm) {
42 if (cookie_path && cookie_path[0]) {
43 webkit_cookie_manager_set_persistent_storage(cm, cookie_path, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
44 }
45 // 2 => no-third-party (default), 0 => always, 1 => never
46 maybe_set_cookie_policy(cm, 2);
47 }
48 // Persist credentials (HTTP auth, etc.)
49 webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
50 GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
51 "user-content-manager", u,
52 "network-session", sess,
53 NULL));
54 // WebView holds refs to provided objects; drop our temporary refs
55 g_object_unref(sess);
56 if (out_ucm) { *out_ucm = u; }
57 return w;
58}
59
60static gboolean gtk_prefers_dark() {
61 GtkSettings* settings = gtk_settings_get_default();
62 if (!settings) return FALSE;
63 gboolean prefer = FALSE;
64 g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
65 return prefer;
66}
67
68// Note: preferred color scheme is handled via a user script injection to support
69// older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
70
71extern void goOnUcmMessage(unsigned long id, const char* json);
72extern void goOnTitleChanged(unsigned long id, const char* title);
73extern void goOnURIChanged(unsigned long id, const char* uri);
74extern void goOnThemeChanged(unsigned long id, int prefer_dark);
75extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
76
77void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
78 (void)pspec;
79 WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
80 const gchar* title = webkit_web_view_get_title(view);
81 GtkWindow* win = GTK_WINDOW(user_data);
82 if (title && win) {
83 gtk_window_set_title(win, title);
84 }
85}
86
87void on_title_notify_id(GObject* obj, GParamSpec* pspec, gpointer user_data) {
88 (void)pspec; (void)obj;
89 const gchar* title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(obj));
90 if (title) { goOnTitleChanged((unsigned long)user_data, title); }
91}
92
93void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
94 (void)pspec; (void)obj;
95 const gchar* uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(obj));
96 if (uri) { goOnURIChanged((unsigned long)user_data, uri); }
97}
98
99// React to GTK theme preference changes at runtime
100void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
101 (void)pspec;
102 GtkSettings* settings = GTK_SETTINGS(obj);
103 if (!settings) return;
104 gboolean prefer = FALSE;
105 g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
106 goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
107}
108
109// NOTE: UCM message callback signature changed in WebKitGTK 6; will be reimplemented later.
110
111void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
112 (void)user_data;
113 const gchar* uri = webkit_uri_scheme_request_get_uri(request);
114 size_t n = 0;
115 char* mime = NULL;
116 void* buf = goResolveURIScheme((char*)uri, &n, &mime);
117 if (buf && n > 0 && mime) {
118 GInputStream* stream = g_memory_input_stream_new_from_data(buf, (gssize)n, g_free);
119 webkit_uri_scheme_request_finish(request, stream, (gint64)n, mime);
120 g_object_unref(stream);
121 g_free(mime);
122 } else {
123 GError* err = g_error_new_literal(g_quark_from_string("dumber"), 404, "Not found");
124 webkit_uri_scheme_request_finish_error(request, err);
125 g_error_free(err);
126 }
127}
128
129// Script message handler wiring for WebKitGTK 6
130// Forward declaration for callback used in g_signal_connect_data
131// WebKitGTK 6 delivers JSCValue* for script-message-received::NAME
132static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data);
133// Uses WebKitScriptMessage and script-message-received::NAME signal.
134static void ucm_on_script_message_cb(WebKitUserContentManager* ucm, JSCValue* val, gpointer user_data) {
135 (void)ucm;
136 if (!val) return;
137 gchar* s = jsc_value_to_string(val);
138 if (!s) return;
139 goOnUcmMessage((unsigned long)user_data, s);
140 g_free(s);
141}
142
143static gboolean register_ucm_handler(WebKitUserContentManager* ucm, const char* name, unsigned long id) {
144 if (!ucm || !name) return FALSE;
145 // WebKitGTK 6 signature: (ucm, name, world_name)
146 webkit_user_content_manager_register_script_message_handler(ucm, name, NULL);
147 gchar* signal = g_strdup_printf("script-message-received::%s", name);
148 if (!signal) return FALSE;
149 g_signal_connect_data(G_OBJECT(ucm), signal, G_CALLBACK(ucm_on_script_message_cb), (gpointer)id, NULL, 0);
150 g_free(signal);
151 return TRUE;
152}
153
154// ----- Conditional helpers for settings across WebKit versions -----
155#ifndef WEBKIT_CHECK_VERSION
156#define WEBKIT_CHECK_VERSION(major,minor,micro) (0)
157#endif
158
159static void maybe_set_hw_policy(WebKitSettings* settings, int mode) {
160#if WEBKIT_CHECK_VERSION(2,40,0)
161 // mode: 0=ON_DEMAND, 1=ALWAYS, 2=NEVER
162 if (!settings) return;
163 if (mode == 1) {
164 webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
165 } else if (mode == 2) {
166 webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
167 } else {
168 // Leave default policy (auto) without forcing a value to avoid referencing
169 // WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND on older headers.
170 }
171#else
172 (void)settings; (void)mode;
173#endif
174}
175
176static void maybe_set_draw_indicators(WebKitSettings* settings, int enable) {
177#if WEBKIT_CHECK_VERSION(2,36,0)
178 if (!settings) return;
179 webkit_settings_set_draw_compositing_indicators(settings, enable ? 1 : 0);
180#else
181 (void)settings; (void)enable;
182#endif
183}
184
185static void maybe_set_webgl(WebKitSettings* settings, int enable) {
186#if WEBKIT_CHECK_VERSION(2,20,0)
187 if (!settings) return;
188 webkit_settings_set_enable_webgl(settings, enable ? 1 : 0);
189#else
190 (void)settings; (void)enable;
191#endif
192}
193
194static void maybe_set_canvas_accel(WebKitSettings* settings, int enable) {
195#if WEBKIT_CHECK_VERSION(2,20,0)
196 if (!settings) return;
197 webkit_settings_set_enable_2d_canvas_acceleration(settings, enable ? 1 : 0);
198#else
199 (void)settings; (void)enable;
200#endif
201}
202
203static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) {
204#if WEBKIT_CHECK_VERSION(2,20,0)
205 if (!settings) return;
206 webkit_settings_set_media_playback_requires_user_gesture(settings, require ? 1 : 0);
207#else
208 (void)settings; (void)require;
209#endif
210}
211
212// Enable two-finger swipe back/forward navigation gestures when supported
213static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
214#if WEBKIT_CHECK_VERSION(2,38,0)
215 if (!settings) return;
216 webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
217#else
218 (void)settings; (void)enable;
219#endif
220}
221
222// Set cookie accept policy with compile-time compatibility across WebKit versions
223static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
224 if (!cm) return;
225#if defined(WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS)
226 WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_ACCEPT_POLICY_ALWAYS;
227 if (policy == 1) p = WEBKIT_COOKIE_ACCEPT_POLICY_NEVER; // never
228 else if (policy == 2) p = WEBKIT_COOKIE_ACCEPT_POLICY_NO_THIRD_PARTY; // no third-party
229 webkit_cookie_manager_set_accept_policy(cm, p);
230#elif defined(WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS)
231 WebKitCookieAcceptPolicy p = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
232 if (policy == 1) p = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
233 else if (policy == 2) p = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
234 webkit_cookie_manager_set_accept_policy(cm, p);
235#else
236 (void)policy; // Not supported in this header version
237#endif
238}
239
240// Memory pressure settings helpers (version-guarded)
241static WebKitMemoryPressureSettings* create_memory_pressure_settings(
242 unsigned int memory_limit_mb,
243 double conservative_threshold,
244 double strict_threshold,
245 double kill_threshold,
246 double poll_interval) {
247 // If headers don't expose memory pressure APIs, return NULL to signal unsupported
248#if WEBKIT_CHECK_VERSION(2,44,0)
249 WebKitMemoryPressureSettings* settings = webkit_memory_pressure_settings_new();
250 if (!settings) return NULL;
251
252 if (memory_limit_mb > 0) {
253 webkit_memory_pressure_settings_set_memory_limit(settings, (guint)(memory_limit_mb * 1024 * 1024));
254 }
255
256 webkit_memory_pressure_settings_set_conservative_threshold(settings, conservative_threshold);
257 webkit_memory_pressure_settings_set_strict_threshold(settings, strict_threshold);
258
259 if (kill_threshold > 0) {
260 webkit_memory_pressure_settings_set_kill_threshold(settings, kill_threshold);
261 }
262
263 webkit_memory_pressure_settings_set_poll_interval(settings, poll_interval);
264
265 return settings;
266#else
267 (void)memory_limit_mb; (void)conservative_threshold; (void)strict_threshold; (void)kill_threshold; (void)poll_interval;
268 return NULL;
269#endif
270}
271
272// Cache model configuration
273static void maybe_set_cache_model(WebKitWebContext* context, int model) {
274 if (!context) return;
275 WebKitCacheModel cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; // default
276
277 switch (model) {
278 case 0: cache_model = WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER; break;
279 case 1: cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER; break;
280 case 2:
281#ifdef WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER
282 cache_model = WEBKIT_CACHE_MODEL_PRIMARY_WEB_BROWSER;
283#else
284 cache_model = WEBKIT_CACHE_MODEL_WEB_BROWSER;
285#endif
286 break;
287 }
288
289 webkit_web_context_set_cache_model(context, cache_model);
290}
291
292// Additional settings for memory optimization
293static void maybe_set_page_cache(WebKitSettings* settings, int enable) {
294#if WEBKIT_CHECK_VERSION(2,6,0)
295 if (!settings) return;
296 webkit_settings_set_enable_page_cache(settings, enable ? TRUE : FALSE);
297#else
298 (void)settings; (void)enable;
299#endif
300}
301
302// Offline Web Application Cache is deprecated in modern WebKit; keep as no-op to avoid warnings.
303static void maybe_set_offline_app_cache(WebKitSettings* settings, int enable) {
304 (void)settings; (void)enable;
305}
306
307// Apply memory pressure settings to context if available
308static void maybe_set_memory_pressure_settings(WebKitWebContext* context, WebKitMemoryPressureSettings* settings) {
309 // No-op wrapper kept for compatibility; settings applied to NetworkSession instead.
310 (void)context; (void)settings;
311}
312
313// Trigger JavaScript GC if supported by headers
314static void maybe_collect_js(WebKitWebContext* context) {
315 // Disabled by default due to API availability variance across 6.0 builds.
316 (void)context;
317}
318*/
319import "C"
320
321import (
322 "errors"
323 "log"
324 "os"
325 "path/filepath"
326 "runtime"
327 "time"
328 "unsafe"
329)
330
331// Helper function to convert bool to int for C interop
332func boolToInt(b bool) int {
333 if b {
334 return 1
335 }
336 return 0
337}
338
339
340type nativeView struct {
341 win *C.GtkWidget
342 view *C.GtkWidget
343 wv *C.WebKitWebView
344 ucm *C.WebKitUserContentManager
345}
346
347type memoryStats struct {
348 pageLoadCount int
349 lastGCTime time.Time
350 memoryPressureSettings *C.WebKitMemoryPressureSettings
351}
352
353// WebView represents a browser view powered by WebKit2GTK.
354type WebView struct {
355 config *Config
356 zoom float64
357 url string
358 destroyed bool
359 native *nativeView
360 window *Window
361 id uintptr
362 msgHandler func(payload string)
363 titleHandler func(title string)
364 uriHandler func(uri string)
365 zoomHandler func(level float64)
366 memStats *memoryStats
367 gcTicker *time.Ticker
368 gcDone chan struct{}
369}
370
371// NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
372func NewWebView(cfg *Config) (*WebView, error) {
373 log.Printf("[webkit] Initializing GTK and creating WebView (CGO)")
374 if C.gtk_init_check() == 0 {
375 return nil, errors.New("failed to initialize GTK")
376 }
377
378 if cfg == nil {
379 cfg = &Config{}
380 }
381
382 // Prepare persistent website data/caches
383 dataDir := cfg.DataDir
384 cacheDir := cfg.CacheDir
385 if dataDir == "" {
386 dataDir = filepath.Join(os.TempDir(), "dumber-webkit-data")
387 }
388 if cacheDir == "" {
389 cacheDir = filepath.Join(os.TempDir(), "dumber-webkit-cache")
390 }
391 _ = os.MkdirAll(dataDir, 0o755)
392 _ = os.MkdirAll(cacheDir, 0o755)
393
394 cData := C.CString(dataDir)
395 cCache := C.CString(cacheDir)
396 cookieFile := filepath.Join(dataDir, "cookies.db")
397 cCookie := C.CString(cookieFile)
398 defer C.free(unsafe.Pointer(cData))
399 defer C.free(unsafe.Pointer(cCache))
400 defer C.free(unsafe.Pointer(cCookie))
401 // Use default WebContext to register the custom URI scheme handler; the
402 // WebView itself is created with a WebKitNetworkSession for persistence.
403 ctx := C.webkit_web_context_get_default()
404
405 // Apply cache model to WebContext
406 // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled
407 cacheModel := 1
408 if cfg.Memory.CacheModel != "" {
409 switch cfg.Memory.CacheModel {
410 case CacheModelWebBrowser:
411 cacheModel = 1
412 case CacheModelPrimaryWebBrowser:
413 cacheModel = 2
414 default: // CacheModelDocumentViewer or unset
415 cacheModel = 0
416 }
417 }
418 C.maybe_set_cache_model(ctx, C.int(cacheModel))
419
420 // Configure memory pressure settings if specified
421 var pressureSettings *C.WebKitMemoryPressureSettings
422 // Only configure memory pressure if explicitly requested via config
423 if cfg.Memory.MemoryLimitMB > 0 || cfg.Memory.ConservativeThreshold > 0 || cfg.Memory.StrictThreshold > 0 || cfg.Memory.KillThreshold > 0 || cfg.Memory.PollIntervalSeconds > 0 {
424 memLimitMB := cfg.Memory.MemoryLimitMB
425 conservativeThreshold := cfg.Memory.ConservativeThreshold
426 strictThreshold := cfg.Memory.StrictThreshold
427 killThreshold := cfg.Memory.KillThreshold
428 pollInterval := cfg.Memory.PollIntervalSeconds
429
430 pressureSettings = C.create_memory_pressure_settings(
431 C.uint(memLimitMB),
432 C.double(conservativeThreshold),
433 C.double(strictThreshold),
434 C.double(killThreshold),
435 C.double(pollInterval))
436
437 if pressureSettings != nil {
438 C.maybe_set_memory_pressure_settings(ctx, pressureSettings)
439 if cfg.Memory.EnableMemoryMonitoring {
440 log.Printf("[webkit] Memory pressure settings applied: limit=%dMB, conservative=%.2f, strict=%.2f, poll=%.1fs",
441 memLimitMB, conservativeThreshold, strictThreshold, pollInterval)
442 }
443 }
444 }
445
446 // Register custom URI scheme handler for "dumb://"
447 {
448 sch := C.CString("dumb")
449 C.webkit_web_context_register_uri_scheme(ctx, sch, (C.WebKitURISchemeRequestCallback)(C.on_uri_scheme), nil, nil)
450 C.free(unsafe.Pointer(sch))
451 }
452 // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
453
454 // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style)
455 var createdUcm *C.WebKitUserContentManager
456 viewWidget := C.new_webview_with_ucm_and_session(cData, cCache, cCookie, &createdUcm, pressureSettings)
457 if viewWidget == nil {
458 return nil, errors.New("failed to create WebKitWebView")
459 }
460
461 // Create a top-level window to host the view
462 win := C.new_window()
463 if win == nil {
464 return nil, errors.New("failed to create GtkWindow")
465 }
466
467 // Pack view widget into the window
468 // GTK4: containers removed; use gtk_window_set_child
469 C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget)
470 C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768)
471 C.connect_destroy_quit(win)
472
473 v := &WebView{
474 config: cfg,
475 zoom: 1.0,
476 native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm},
477 window: &Window{win: win},
478 memStats: &memoryStats{
479 pageLoadCount: 0,
480 lastGCTime: time.Now(),
481 memoryPressureSettings: pressureSettings,
482 },
483 gcDone: make(chan struct{}),
484 }
485 // Assign an ID for accelerator dispatch
486 v.id = nextViewID()
487 registerView(v.id, v)
488
489 // Register with global memory manager if monitoring is enabled
490 if cfg.Memory.EnableMemoryMonitoring {
491 if globalMemoryManager == nil {
492 InitializeGlobalMemoryManager(
493 cfg.Memory.EnableMemoryMonitoring,
494 cfg.Memory.ProcessRecycleThreshold,
495 30*time.Second, // Default monitoring interval
496 )
497 }
498 if globalMemoryManager != nil {
499 globalMemoryManager.RegisterWebView(v)
500 }
501 }
502 // Attach GTK4 input controllers (keyboard, mouse)
503 AttachKeyboardControllers(v)
504
505 // Watch for GTK theme changes and propagate to page at runtime
506 if settings := C.gtk_settings_get_default(); settings != nil {
507 cprop := C.CString("notify::gtk-application-prefer-dark-theme")
508 C.g_signal_connect_data(C.gpointer(unsafe.Pointer(settings)), cprop, C.GCallback(C.on_theme_changed), C.gpointer(v.id), nil, 0)
509 C.free(unsafe.Pointer(cprop))
510 }
511
512 // Native zoom shortcuts (independent of app services)
513 // Ctrl+='=' and Ctrl+'+' → Zoom In; Ctrl+'-' → Zoom Out; Ctrl+'0' → Reset
514 _ = v.RegisterKeyboardShortcut("cmdorctrl+=", func() {
515 nz := v.zoom
516 if nz <= 0 {
517 nz = 1.0
518 }
519 nz *= 1.1
520 if nz < 0.25 {
521 nz = 0.25
522 }
523 if nz > 5.0 {
524 nz = 5.0
525 }
526 _ = v.SetZoom(nz)
527 })
528 _ = v.RegisterKeyboardShortcut("cmdorctrl+plus", func() {
529 nz := v.zoom
530 if nz <= 0 {
531 nz = 1.0
532 }
533 nz *= 1.1
534 if nz < 0.25 {
535 nz = 0.25
536 }
537 if nz > 5.0 {
538 nz = 5.0
539 }
540 _ = v.SetZoom(nz)
541 })
542 _ = v.RegisterKeyboardShortcut("cmdorctrl-", func() {
543 nz := v.zoom
544 if nz <= 0 {
545 nz = 1.0
546 }
547 nz /= 1.1
548 if nz < 0.25 {
549 nz = 0.25
550 }
551 if nz > 5.0 {
552 nz = 5.0
553 }
554 _ = v.SetZoom(nz)
555 })
556 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
557 // Find in page (Ctrl/Cmd+F) reuses omnibox component in find mode
558 _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { _ = v.OpenFind("") })
559 // Navigation with Alt+Arrow keys
560 _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })
561 _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })
562 // Update window title when page title changes
563 cNotifyTitle1 := C.CString("notify::title")
564 C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
565 C.free(unsafe.Pointer(cNotifyTitle1))
566 // Also dispatch title change to Go with view id
567 cNotifyTitle2 := C.CString("notify::title")
568 C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle2, C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)
569 C.free(unsafe.Pointer(cNotifyTitle2))
570 // Notify URI changes to Go to keep current URL in sync
571 cNotifyURI := C.CString("notify::uri")
572 C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyURI, C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)
573 C.free(unsafe.Pointer(cNotifyURI))
574 // Apply hardware acceleration and related settings based on cfg.Rendering
575 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
576 // Hardware acceleration policy (guarded by version)
577 switch cfg.Rendering.Mode {
578 case "gpu":
579 C.maybe_set_hw_policy(settings, 1)
580 C.maybe_set_webgl(settings, 1)
581 C.maybe_set_canvas_accel(settings, 1)
582 case "cpu":
583 C.maybe_set_hw_policy(settings, 2)
584 C.maybe_set_webgl(settings, 0)
585 C.maybe_set_canvas_accel(settings, 0)
586 default: // auto
587 C.maybe_set_hw_policy(settings, 0)
588 C.maybe_set_webgl(settings, 1)
589 C.maybe_set_canvas_accel(settings, 1)
590 }
591 // Optional compositing indicators for debugging (guarded by version)
592 if cfg.Rendering.DebugGPU {
593 C.maybe_set_draw_indicators(settings, 1)
594 }
595 // Reduce media pipeline churn by requiring a user gesture for playback
596 C.maybe_set_media_user_gesture(settings, 1)
597 // Enable trackpad back/forward gestures when available
598 C.maybe_set_back_forward_gestures(settings, 1)
599
600 // Keep page cache enabled by default for performance
601 enablePageCache := true
602 if cfg.Memory.EnablePageCache {
603 enablePageCache = true
604 }
605 C.maybe_set_page_cache(settings, C.int(boolToInt(enablePageCache)))
606
607 // Offline app cache is deprecated; keep as no-op
608 C.maybe_set_offline_app_cache(settings, C.int(0))
609
610 if cfg.Memory.EnableMemoryMonitoring {
611 log.Printf("[webkit] Memory settings applied: page_cache=%v", enablePageCache)
612 }
613 }
614
615 if cfg.ZoomDefault > 0 {
616 v.zoom = cfg.ZoomDefault
617 C.webkit_web_view_set_zoom_level(v.native.wv, C.gdouble(v.zoom))
618 }
619 if cfg.EnableDeveloperExtras {
620 settings := C.webkit_web_view_get_settings(v.native.wv)
621 if settings != nil {
622 C.webkit_settings_set_enable_developer_extras(settings, C.gboolean(1))
623 }
624 }
625 // Apply default fonts if provided in cfg
626 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
627 if cfg.DefaultSansFont != "" {
628 csans := C.CString(cfg.DefaultSansFont)
629 C.webkit_settings_set_sans_serif_font_family(settings, (*C.gchar)(csans))
630 C.free(unsafe.Pointer(csans))
631 // Also set default font family to sans
632 csans2 := C.CString(cfg.DefaultSansFont)
633 C.webkit_settings_set_default_font_family(settings, (*C.gchar)(csans2))
634 C.free(unsafe.Pointer(csans2))
635 }
636 if cfg.DefaultSerifFont != "" {
637 cserif := C.CString(cfg.DefaultSerifFont)
638 C.webkit_settings_set_serif_font_family(settings, (*C.gchar)(cserif))
639 C.free(unsafe.Pointer(cserif))
640 }
641 if cfg.DefaultMonospaceFont != "" {
642 cmono := C.CString(cfg.DefaultMonospaceFont)
643 C.webkit_settings_set_monospace_font_family(settings, (*C.gchar)(cmono))
644 C.free(unsafe.Pointer(cmono))
645 }
646 if cfg.DefaultFontSize > 0 {
647 C.webkit_settings_set_default_font_size(settings, C.guint(cfg.DefaultFontSize))
648 }
649 }
650 if cfg.InitialURL != "" {
651 _ = v.LoadURL(cfg.InitialURL)
652 }
653 // Ensure native resources are released if user forgets to call Destroy
654 runtime.SetFinalizer(v, func(v *WebView) { _ = v.Destroy() })
655 log.Printf("[webkit] WebView created (CGO)")
656
657 // Initialize UCM scripts and handlers
658 if v.native != nil && v.native.ucm != nil {
659 v.enableUserContentManager()
660 }
661
662 // Start periodic JavaScript garbage collection if configured
663 if cfg.Memory.EnableGCInterval > 0 {
664 v.startPeriodicGC(time.Duration(cfg.Memory.EnableGCInterval) * time.Second)
665 if cfg.Memory.EnableMemoryMonitoring {
666 log.Printf("[webkit] Periodic GC enabled: interval=%ds", cfg.Memory.EnableGCInterval)
667 }
668 }
669
670 return v, nil
671}
672
673func (w *WebView) LoadURL(rawURL string) error {
674 if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
675 return ErrNotImplemented
676 }
677
678 // Track page loads for process recycling
679 if w.memStats != nil {
680 w.memStats.pageLoadCount++
681
682 // Check if we need to recycle this WebView
683 if w.config.Memory.ProcessRecycleThreshold > 0 &&
684 w.memStats.pageLoadCount >= w.config.Memory.ProcessRecycleThreshold {
685 if w.config.Memory.EnableMemoryMonitoring {
686 log.Printf("[webkit] Process recycle threshold reached (%d pages), consider creating new WebView",
687 w.memStats.pageLoadCount)
688 }
689 }
690 }
691
692 w.url = rawURL
693 curl := C.CString(rawURL)
694 defer C.free(unsafe.Pointer(curl))
695 C.webkit_web_view_load_uri(w.native.wv, (*C.gchar)(curl))
696 log.Printf("[webkit] LoadURL: %s", rawURL)
697 return nil
698}
699
700func (w *WebView) Show() error {
701 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
702 return ErrNotImplemented
703 }
704 C.gtk_widget_set_visible(w.native.view, C.gboolean(1))
705 C.gtk_widget_set_visible(w.native.win, C.gboolean(1))
706 C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
707 log.Printf("[webkit] Show window")
708 return nil
709}
710
711func (w *WebView) Hide() error {
712 if w == nil || w.destroyed || w.native == nil || w.native.win == nil {
713 return ErrNotImplemented
714 }
715 C.gtk_widget_set_visible(w.native.view, C.gboolean(0))
716 C.gtk_widget_set_visible(w.native.win, C.gboolean(0))
717 return nil
718}
719
720func (w *WebView) Destroy() error {
721 if w == nil || w.native == nil || w.native.win == nil {
722 return ErrNotImplemented
723 }
724
725 // Stop periodic GC if running
726 w.stopPeriodicGC()
727
728 // Clean up memory pressure settings
729 if w.memStats != nil && w.memStats.memoryPressureSettings != nil {
730 // WebKit will clean up the memory pressure settings when context is destroyed
731 w.memStats.memoryPressureSettings = nil
732 }
733
734 // Unregister from memory manager
735 if globalMemoryManager != nil {
736 globalMemoryManager.UnregisterWebView(w.id)
737 }
738
739 // GTK4: destroy windows via gtk_window_destroy
740 C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
741 w.destroyed = true
742 unregisterView(w.id)
743 log.Printf("[webkit] Destroy window")
744 return nil
745}
746
747// Window returns the associated native window.
748func (w *WebView) Window() *Window { return w.window }
749
750// GetCurrentURL returns the current URI from WebKit.
751func (w *WebView) GetCurrentURL() string {
752 if w == nil || w.native == nil || w.native.wv == nil {
753 return ""
754 }
755 uri := C.webkit_web_view_get_uri(w.native.wv)
756 if uri == nil {
757 return ""
758 }
759 return C.GoString((*C.char)(unsafe.Pointer(uri)))
760}
761
762func (w *WebView) GoBack() error {
763 if w == nil || w.native == nil || w.native.wv == nil {
764 return ErrNotImplemented
765 }
766 C.webkit_web_view_go_back(w.native.wv)
767 return nil
768}
769
770func (w *WebView) GoForward() error {
771 if w == nil || w.native == nil || w.native.wv == nil {
772 return ErrNotImplemented
773 }
774 C.webkit_web_view_go_forward(w.native.wv)
775 return nil
776}
777
778// RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
779func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
780
781func (w *WebView) dispatchScriptMessage(payload string) {
782 if w != nil && w.msgHandler != nil {
783 w.msgHandler(payload)
784 }
785}
786
787// RegisterTitleChangedHandler registers a callback invoked when the page title changes.
788func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb }
789
790func (w *WebView) dispatchTitleChanged(title string) {
791 if w != nil && w.titleHandler != nil {
792 w.titleHandler(title)
793 }
794}
795
796// RegisterURIChangedHandler registers a callback invoked when the current page URI changes.
797func (w *WebView) RegisterURIChangedHandler(cb func(uri string)) { w.uriHandler = cb }
798
799func (w *WebView) dispatchURIChanged(uri string) {
800 if w != nil && w.uriHandler != nil {
801 w.uriHandler(uri)
802 }
803}
804
805// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
806func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
807
808func (w *WebView) dispatchZoomChanged(level float64) {
809 if w != nil && w.zoomHandler != nil {
810 w.zoomHandler(level)
811 }
812}
813
814// enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
815func (w *WebView) enableUserContentManager() {
816 if w == nil || w.native == nil || w.native.ucm == nil {
817 return
818 }
819 // Register handler "dumber"
820 cname := C.CString("dumber")
821 ok := C.register_ucm_handler(w.native.ucm, cname, C.ulong(w.id))
822 if ok == 0 {
823 log.Printf("[webkit] Failed to register UCM handler; fallback bridge will handle messages")
824 }
825 C.free(unsafe.Pointer(cname))
826
827 // Inject color-scheme preference script at document-start to inform sites of system theme
828 preferDark := C.gtk_prefers_dark() != 0
829 if preferDark {
830 log.Printf("[theme] GTK prefers: dark")
831 } else {
832 log.Printf("[theme] GTK prefers: light")
833 }
834 var schemeJS string
835 if preferDark {
836 schemeJS = "(() => { try { const d=true; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='dark light'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:dark;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
837 } else {
838 schemeJS = "(() => { try { const d=false; const cs=d?'dark':'light'; console.log('[dumber] color-scheme set:' + cs); try{ window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({type:'theme', value: cs})) }catch(_){} const meta=document.createElement('meta'); meta.name='color-scheme'; meta.content='light dark'; document.documentElement.appendChild(meta); const s=document.createElement('style'); s.textContent=':root{color-scheme:light;}'; document.documentElement.appendChild(s); const qD='(prefers-color-scheme: dark)'; const qL='(prefers-color-scheme: light)'; const orig=window.matchMedia; window.matchMedia=function(q){ if(typeof q==='string'&&(q.includes(qD)||q.includes(qL))){ const m={matches:q.includes('dark')?d:!d,media:q,onchange:null,addListener(){},removeListener(){},addEventListener(){},removeEventListener(){},dispatchEvent(){return false;}}; return m;} return orig.call(window,q); }; } catch(e){ console.warn('[dumber] color-scheme injection failed', e) } })();"
839 }
840 cScheme := C.CString(schemeJS)
841 defer C.free(unsafe.Pointer(cScheme))
842 schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
843 if schemeScript != nil {
844 C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript)
845 C.webkit_user_script_unref(schemeScript)
846 }
847
848 // Add user script at document-start (omnibox/find reusable component)
849 src := C.CString(getOmniboxScript())
850 defer C.free(unsafe.Pointer(src))
851 script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
852 if script != nil {
853 C.webkit_user_content_manager_add_script(w.native.ucm, script)
854 C.webkit_user_script_unref(script)
855 log.Printf("[webkit] UCM omnibox script injected at document-start")
856 }
857
858 // Inject Wails runtime fetch interceptor for homepage bridging
859 wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();`
860 cWails := C.CString(wailsBridge)
861 defer C.free(unsafe.Pointer(cWails))
862 wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
863 if wailsScript != nil {
864 C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript)
865 C.webkit_user_script_unref(wailsScript)
866 }
867
868 // No JS fallback bridge — native UCM is active
869}
870
871// startPeriodicGC starts a ticker to trigger JavaScript garbage collection periodically
872func (w *WebView) startPeriodicGC(interval time.Duration) {
873 if w == nil || w.gcTicker != nil {
874 return // Already running or invalid
875 }
876
877 w.gcTicker = time.NewTicker(interval)
878
879 go func() {
880 defer func() {
881 if w.gcTicker != nil {
882 w.gcTicker.Stop()
883 w.gcTicker = nil
884 }
885 }()
886
887 for {
888 select {
889 case <-w.gcTicker.C:
890 w.triggerGC()
891 case <-w.gcDone:
892 return
893 }
894 }
895 }()
896}
897
898// stopPeriodicGC stops the periodic garbage collection
899func (w *WebView) stopPeriodicGC() {
900 if w == nil {
901 return
902 }
903
904 if w.gcDone != nil {
905 close(w.gcDone)
906 w.gcDone = nil
907 }
908
909 if w.gcTicker != nil {
910 w.gcTicker.Stop()
911 w.gcTicker = nil
912 }
913}
914
915// triggerGC manually triggers JavaScript garbage collection
916func (w *WebView) triggerGC() {
917 if w == nil || w.memStats == nil {
918 return
919 }
920
921 // Get default WebContext and trigger GC
922 ctx := C.webkit_web_context_get_default()
923 if ctx != nil {
924 C.maybe_collect_js(ctx)
925 w.memStats.lastGCTime = time.Now()
926
927 if w.config.Memory.EnableMemoryMonitoring {
928 log.Printf("[webkit] JavaScript GC triggered (page loads: %d)", w.memStats.pageLoadCount)
929 }
930 }
931}
932
933// GetMemoryStats returns current memory statistics for this WebView
934func (w *WebView) GetMemoryStats() map[string]interface{} {
935 if w == nil || w.memStats == nil {
936 return nil
937 }
938
939 return map[string]interface{}{
940 "page_load_count": w.memStats.pageLoadCount,
941 "last_gc_time": w.memStats.lastGCTime,
942 "has_memory_pressure_settings": w.memStats.memoryPressureSettings != nil,
943 }
944}
945
946// TriggerMemoryCleanup manually triggers garbage collection and memory cleanup
947func (w *WebView) TriggerMemoryCleanup() {
948 if w == nil {
949 return
950 }
951
952 w.triggerGC()
953
954 if w.config.Memory.EnableMemoryMonitoring {
955 log.Printf("[webkit] Manual memory cleanup triggered")
956 }
957}