1package webkit
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "strconv"
8 "sync"
9 "sync/atomic"
10
11 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
12 "github.com/diamondburned/gotk4/pkg/gdk/v4"
13 "github.com/diamondburned/gotk4/pkg/gtk/v4"
14)
15
16var (
17 viewIDCounter uint64
18 viewRegistry = make(map[uint64]*WebView)
19 viewMu sync.RWMutex
20)
21
22// WebView wraps a WebKitGTK WebView
23type WebView struct {
24 view *webkit.WebView
25 container *gtk.Box // Container that holds the WebView
26 window *Window // Optional window (only if CreateWindow=true)
27 id uint64
28
29 // State
30 config *Config
31 destroyed bool
32 mu sync.RWMutex
33
34 // Event handlers
35 onScriptMessage func(string)
36 onTitleChanged func(string)
37 onURIChanged func(string)
38 onFaviconChanged func([]byte)
39 onFaviconURIChanged func(pageURI, faviconURI string)
40 onZoomChanged func(float64)
41 onPopupCreate func(*webkit.NavigationAction) *WebView // New WebKit create signal handler
42 onReadyToShow func() // WebKit ready-to-show signal handler
43 onClose func()
44 onNavigationPolicy func(url string, isUserGesture bool) bool
45 onWindowTypeDetected func(WindowType, *WindowFeatures)
46 onWorkspaceNavigation func(direction string) bool // Workspace pane navigation
47 onPaneModeShortcut func(action string) bool // Pane mode shortcuts (enter, actions, exit)
48 isPaneModeActive func() bool // Check if pane mode is active
49}
50
51// NewWebView creates a new WebView with the given configuration
52func NewWebView(cfg *Config) (*WebView, error) {
53 if cfg == nil {
54 cfg = GetDefaultConfig()
55 }
56
57 // Generate unique ID immediately
58 id := atomic.AddUint64(&viewIDCounter, 1)
59 log.Printf("[webkit] Generated WebView ID: %d (CreateWindow=%v)", id, cfg.CreateWindow)
60
61 InitMainThread()
62
63 // Initialize persistent session - REQUIRED, no ephemeral fallback
64 // This must be done BEFORE creating the WebView
65 if cfg.DataDir == "" || cfg.CacheDir == "" {
66 return nil, fmt.Errorf("data and cache directories are required for persistent storage")
67 }
68
69 if err := InitPersistentSession(cfg.DataDir, cfg.CacheDir); err != nil {
70 return nil, fmt.Errorf("failed to initialize persistent storage: %w", err)
71 }
72
73 // Create WebView - it will automatically use the persistent session
74 // Per WebKitGTK 6.0: newly created session becomes the default for all WebViews
75 wkView := webkit.NewWebView()
76 if wkView == nil {
77 return nil, ErrWebViewNotInitialized
78 }
79
80 // Set background color based on user's theme preference to prevent white/black flash
81 // This must be done before any content loads
82 var bg gdk.RGBA
83 if PrefersDarkTheme() {
84 bg = gdk.NewRGBA(0.11, 0.11, 0.11, 1.0) // #1c1c1c - dark background
85 } else {
86 bg = gdk.NewRGBA(0.96, 0.96, 0.96, 1.0) // #f5f5f5 - light background
87 }
88 wkView.SetBackgroundColor(&bg)
89
90 // Verify the WebView is using the persistent session
91 viewSession := wkView.NetworkSession()
92 if viewSession == nil {
93 return nil, fmt.Errorf("WebView has no network session")
94 }
95 if viewSession.IsEphemeral() {
96 return nil, fmt.Errorf("WebView is using ephemeral session despite persistent session initialization")
97 }
98
99 // CRITICAL: Configure CookieManager on the WebView's actual session
100 // This ensures cookies are persisted regardless of whether the WebView
101 // uses our global session or creates its own
102 cookieManager := viewSession.CookieManager()
103 if cookieManager == nil {
104 return nil, fmt.Errorf("failed to get cookie manager from WebView's network session")
105 }
106 cookiePath := cfg.DataDir + "/cookies.db"
107 cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite)
108 cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
109
110 // Setup UserContentManager and inject GUI scripts (including webview ID)
111 // This must be done BEFORE any pages are loaded
112 if err := SetupUserContentManager(wkView, cfg.AppearanceConfigJSON, id); err != nil {
113 return nil, fmt.Errorf("failed to setup user content manager: %w", err)
114 }
115
116 // Create container (GtkBox) to hold the WebView
117 // This allows the WebView to be reparented into workspace panes
118 container := gtk.NewBox(gtk.OrientationVertical, 0)
119 container.SetHExpand(true)
120 container.SetVExpand(true)
121
122 // Configure WebView widget for expansion
123 wkView.SetHExpand(true)
124 wkView.SetVExpand(true)
125
126 // Add WebView to container
127 container.Append(wkView)
128
129 wv := &WebView{
130 view: wkView,
131 container: container,
132 window: nil, // Will be set below if CreateWindow is true
133 id: id,
134 config: cfg,
135 }
136
137 // Apply configuration
138 if err := wv.applyConfig(); err != nil {
139 return nil, err
140 }
141
142 // Setup event handlers
143 wv.setupEventHandlers()
144
145 // Attach keyboard bridge to forward keyboard events to JavaScript
146 wv.AttachKeyboardBridge()
147
148 // Attach mouse gesture controls for hardware navigation buttons
149 wv.AttachMouseGestures()
150
151 // Attach touchpad gesture controls for swipe navigation
152 wv.AttachTouchpadGestures()
153
154 // Only create window if requested (standalone WebViews)
155 // Workspace panes will set CreateWindow=false and manage the container themselves
156 if cfg.CreateWindow {
157 window, err := NewWindow("Dumber Browser")
158 if err != nil {
159 return nil, err
160 }
161 wv.window = window
162 // Add container as child of window
163 window.SetChild(container)
164 log.Printf("[webkit] Created standalone window for WebView ID %d", id)
165 } else {
166 log.Printf("[webkit] Created WebView ID %d without window (for workspace embedding)", id)
167 }
168
169 // Register in global registry
170 viewMu.Lock()
171 viewRegistry[id] = wv
172 viewMu.Unlock()
173
174 return wv, nil
175}
176
177// applyConfig applies the configuration to the WebView settings
178func (w *WebView) applyConfig() error {
179 settings := w.view.Settings()
180 if settings == nil {
181 return fmt.Errorf("webkit: failed to get settings")
182 }
183
184 // Apply settings from config
185 settings.SetEnableJavascript(w.config.EnableJavaScript)
186 settings.SetEnableWebgl(w.config.EnableWebGL)
187 settings.SetDefaultFontSize(uint32(w.config.DefaultFontSize))
188 settings.SetMinimumFontSize(uint32(w.config.MinimumFontSize))
189
190 if w.config.UserAgent != "" {
191 settings.SetUserAgent(w.config.UserAgent)
192 }
193
194 // Enable developer tools (F12, inspector)
195 settings.SetEnableDeveloperExtras(true)
196
197 // Enable hardware acceleration if configured
198 settings.SetHardwareAccelerationPolicy(webkit.HardwareAccelerationPolicyAlways)
199
200 return nil
201}
202
203// setupEventHandlers connects GTK signals to internal handlers
204func (w *WebView) setupEventHandlers() {
205 // Title changed - connect to notify::title signal
206 w.view.Connect("notify::title", func() {
207 if w.onTitleChanged != nil {
208 title := w.view.Title()
209 w.onTitleChanged(title)
210 }
211 })
212
213 // URI changed - connect to notify::uri signal
214 w.view.Connect("notify::uri", func() {
215 if w.onURIChanged != nil {
216 uri := w.view.URI()
217 w.onURIChanged(uri)
218 }
219 })
220
221 // Zoom level changed - connect to notify::zoom-level signal
222 w.view.Connect("notify::zoom-level", func() {
223 if w.onZoomChanged != nil {
224 zoomLevel := w.view.ZoomLevel()
225 w.onZoomChanged(zoomLevel)
226 }
227 })
228
229 // Favicon changed - connect to FaviconDatabase
230 w.setupFaviconHandlers()
231
232 // Load changed
233 w.view.ConnectLoadChanged(func(event webkit.LoadEvent) {
234 // Handle load events if needed
235 })
236
237 // TLS error handling - connect to load-failed-with-tls-errors signal
238 w.setupTLSErrorHandler()
239
240 // Close
241 w.view.ConnectClose(func() {
242 if w.onClose != nil {
243 w.onClose()
244 }
245 })
246
247 // Create signal - for popup lifecycle management
248 w.view.ConnectCreate(func(navigationAction *webkit.NavigationAction) gtk.Widgetter {
249 log.Printf("[webkit] ConnectCreate callback fired for parent WebView ID=%d", w.id)
250 if w.onPopupCreate != nil {
251 log.Printf("[webkit] Calling onPopupCreate handler")
252 newWebView := w.onPopupCreate(navigationAction)
253 if newWebView != nil {
254 log.Printf("[webkit] onPopupCreate returned WebView wrapper ID=%d", newWebView.id)
255 log.Printf("[webkit] Extracting underlying gotk4 WebView from wrapper")
256 underlyingView := newWebView.view
257 log.Printf("[webkit] Extracted gotk4 WebView=%p, about to return to WebKit", underlyingView)
258 // Return the underlying WebView widget to WebKit
259 return underlyingView
260 }
261 log.Printf("[webkit] onPopupCreate returned nil, blocking popup")
262 }
263 // Return nil to cancel popup creation
264 log.Printf("[webkit] No onPopupCreate handler, blocking popup")
265 return nil
266 })
267
268 // Ready-to-show signal - emitted when popup is ready to be displayed
269 w.view.ConnectReadyToShow(func() {
270 if w.onReadyToShow != nil {
271 w.onReadyToShow()
272 }
273 })
274
275 // Script message received - connect to UserContentManager's script-message-received signal
276 // This receives messages from JavaScript via webkit.messageHandlers.dumber.postMessage()
277 // The signal name includes the handler name as a detail: "script-message-received::dumber"
278 ucm := w.view.UserContentManager()
279 if ucm != nil {
280 // GTK signals pass the sender as the first parameter, the actual signal data as subsequent parameters
281 ucm.Connect("script-message-received::dumber", func(sender interface{}, jscValue interface{}) {
282 if w.onScriptMessage != nil && jscValue != nil {
283 // Convert JSCValue to string using gotk4 javascriptcore bindings
284 valueStr := JSCValueToString(jscValue)
285 if valueStr != "" {
286 w.onScriptMessage(valueStr)
287 }
288 }
289 })
290 }
291}
292
293// LoadURL loads the given URL in the WebView
294func (w *WebView) LoadURL(url string) error {
295 w.mu.RLock()
296 defer w.mu.RUnlock()
297
298 if w.destroyed {
299 return ErrWebViewDestroyed
300 }
301
302 if url == "" {
303 return ErrInvalidURL
304 }
305
306 w.view.LoadURI(url)
307 return nil
308}
309
310// GetCurrentURL returns the current URL
311func (w *WebView) GetCurrentURL() string {
312 w.mu.RLock()
313 defer w.mu.RUnlock()
314
315 if w.destroyed {
316 return ""
317 }
318
319 return w.view.URI()
320}
321
322// GetTitle returns the current page title
323func (w *WebView) GetTitle() string {
324 w.mu.RLock()
325 defer w.mu.RUnlock()
326
327 if w.destroyed {
328 return ""
329 }
330
331 return w.view.Title()
332}
333
334// GoBack navigates back in history
335func (w *WebView) GoBack() error {
336 w.mu.RLock()
337 defer w.mu.RUnlock()
338
339 if w.destroyed {
340 return ErrWebViewDestroyed
341 }
342
343 w.view.GoBack()
344 return nil
345}
346
347// GoForward navigates forward in history
348func (w *WebView) GoForward() error {
349 w.mu.RLock()
350 defer w.mu.RUnlock()
351
352 if w.destroyed {
353 return ErrWebViewDestroyed
354 }
355
356 w.view.GoForward()
357 return nil
358}
359
360// Reload reloads the current page
361func (w *WebView) Reload() error {
362 w.mu.RLock()
363 defer w.mu.RUnlock()
364
365 if w.destroyed {
366 return ErrWebViewDestroyed
367 }
368
369 w.view.Reload()
370 return nil
371}
372
373// ReloadBypassCache reloads the current page, bypassing cache
374func (w *WebView) ReloadBypassCache() error {
375 w.mu.RLock()
376 defer w.mu.RUnlock()
377
378 if w.destroyed {
379 return ErrWebViewDestroyed
380 }
381
382 w.view.ReloadBypassCache()
383 return nil
384}
385
386// Show makes the WebView visible
387func (w *WebView) Show() error {
388 w.mu.RLock()
389 defer w.mu.RUnlock()
390
391 if w.destroyed {
392 return ErrWebViewDestroyed
393 }
394
395 // Make the WebView widget visible
396 w.view.SetVisible(true)
397
398 // Show the window containing the WebView
399 if w.window != nil {
400 w.window.Show()
401 }
402
403 return nil
404}
405
406// Hide makes the WebView invisible
407func (w *WebView) Hide() error {
408 w.mu.RLock()
409 defer w.mu.RUnlock()
410
411 if w.destroyed {
412 return ErrWebViewDestroyed
413 }
414
415 w.view.SetVisible(false)
416 return nil
417}
418
419// Destroy destroys the WebView and releases resources
420func (w *WebView) Destroy() error {
421 w.mu.Lock()
422 defer w.mu.Unlock()
423
424 if w.destroyed {
425 return nil
426 }
427
428 w.destroyed = true
429
430 // Unregister from global registry
431 viewMu.Lock()
432 delete(viewRegistry, w.id)
433 viewMu.Unlock()
434
435 // The GTK widget will be cleaned up by Go GC
436 return nil
437}
438
439// IsDestroyed returns true if the WebView has been destroyed
440func (w *WebView) IsDestroyed() bool {
441 w.mu.RLock()
442 defer w.mu.RUnlock()
443 return w.destroyed
444}
445
446// ID returns the unique identifier for this WebView
447func (w *WebView) ID() uint64 {
448 return w.id
449}
450
451// IDString returns the WebView ID as a string
452// This is a convenience method for compatibility with code expecting string IDs
453func (w *WebView) IDString() string {
454 return fmt.Sprintf("%d", w.id)
455}
456
457// ParseWebViewID converts a string WebView ID to uint64
458// Returns 0 if the string cannot be parsed
459func ParseWebViewID(idStr string) uint64 {
460 id, err := strconv.ParseUint(idStr, 10, 64)
461 if err != nil {
462 return 0
463 }
464 return id
465}
466
467// AsWidget returns the WebView as a gtk.Widgetter
468func (w *WebView) AsWidget() gtk.Widgetter {
469 if w == nil || w.view == nil {
470 return nil
471 }
472 return w.view
473}
474
475// Event handler registration methods
476
477// RegisterScriptMessageHandler registers a handler for script messages
478func (w *WebView) RegisterScriptMessageHandler(handler func(string)) {
479 w.mu.Lock()
480 defer w.mu.Unlock()
481 w.onScriptMessage = handler
482}
483
484// RegisterTitleChangedHandler registers a handler for title changes
485func (w *WebView) RegisterTitleChangedHandler(handler func(string)) {
486 w.mu.Lock()
487 defer w.mu.Unlock()
488 w.onTitleChanged = handler
489}
490
491// RegisterURIChangedHandler registers a handler for URI changes
492func (w *WebView) RegisterURIChangedHandler(handler func(string)) {
493 w.mu.Lock()
494 defer w.mu.Unlock()
495 w.onURIChanged = handler
496}
497
498// RegisterFaviconChangedHandler registers a handler for favicon changes
499func (w *WebView) RegisterFaviconChangedHandler(handler func([]byte)) {
500 w.mu.Lock()
501 defer w.mu.Unlock()
502 w.onFaviconChanged = handler
503}
504
505// RegisterFaviconURIChangedHandler registers a handler for favicon URI changes
506func (w *WebView) RegisterFaviconURIChangedHandler(handler func(pageURI, faviconURI string)) {
507 w.mu.Lock()
508 defer w.mu.Unlock()
509 w.onFaviconURIChanged = handler
510}
511
512// RegisterZoomChangedHandler registers a handler for zoom changes
513func (w *WebView) RegisterZoomChangedHandler(handler func(float64)) {
514 w.mu.Lock()
515 defer w.mu.Unlock()
516 w.onZoomChanged = handler
517}
518
519// RegisterCloseHandler registers a handler for close requests
520func (w *WebView) RegisterCloseHandler(handler func()) {
521 w.mu.Lock()
522 defer w.mu.Unlock()
523 w.onClose = handler
524}
525
526// RegisterPopupCreateHandler registers a handler for WebKit's create signal
527// This is called when JavaScript calls window.open() or a link with target="_blank" is clicked
528// The handler should create and return a new WebView for the popup, or return nil to block it
529// The returned WebView should NOT be shown yet - WebKit will emit ready-to-show when it's ready
530func (w *WebView) RegisterPopupCreateHandler(handler func(*webkit.NavigationAction) *WebView) {
531 w.mu.Lock()
532 defer w.mu.Unlock()
533 w.onPopupCreate = handler
534}
535
536// RegisterReadyToShowHandler registers a handler for WebKit's ready-to-show signal
537// This is called when a popup WebView is ready to be displayed
538// At this point it's safe to insert the popup into the UI (workspace, window, etc.)
539func (w *WebView) RegisterReadyToShowHandler(handler func()) {
540 w.mu.Lock()
541 defer w.mu.Unlock()
542 w.onReadyToShow = handler
543}
544
545// RegisterNavigationPolicyHandler registers a handler for navigation policy decisions
546func (w *WebView) RegisterNavigationPolicyHandler(handler func(url string, isUserGesture bool) bool) {
547 w.mu.Lock()
548 defer w.mu.Unlock()
549 w.onNavigationPolicy = handler
550}
551
552// OnWindowTypeDetected registers a handler for window type detection
553func (w *WebView) OnWindowTypeDetected(handler func(WindowType, *WindowFeatures)) {
554 w.mu.Lock()
555 defer w.mu.Unlock()
556 w.onWindowTypeDetected = handler
557}
558
559// RunOnMainThread executes a function on the GTK main thread
560func (w *WebView) RunOnMainThread(fn func()) {
561 RunOnMainThread(fn)
562}
563
564// GetWebView returns the underlying webkit.WebView for advanced operations
565func (w *WebView) GetWebView() *webkit.WebView {
566 return w.view
567}
568
569// Widget returns the WebView widget (for compatibility with old code expecting uintptr)
570// Deprecated: Use AsWidget() instead
571func (w *WebView) Widget() gtk.Widgetter {
572 return w.AsWidget()
573}
574
575// RootWidget returns the root container widget for this WebView
576// This is the GtkBox container that holds the WebView and can be reparented
577func (w *WebView) RootWidget() gtk.Widgetter {
578 if w.container != nil {
579 return w.container
580 }
581 // Fallback to WebView if container is not available (shouldn't happen)
582 return w.AsWidget()
583}
584
585// SetZoom sets the zoom level of the WebView
586func (w *WebView) SetZoom(zoom float64) error {
587 w.mu.RLock()
588 defer w.mu.RUnlock()
589
590 if w.destroyed {
591 return ErrWebViewDestroyed
592 }
593
594 w.view.SetZoomLevel(zoom)
595 return nil
596}
597
598// GetZoom returns the current zoom level
599func (w *WebView) GetZoom() float64 {
600 w.mu.RLock()
601 defer w.mu.RUnlock()
602
603 if w.destroyed {
604 return 1.0
605 }
606
607 return w.view.ZoomLevel()
608}
609
610// UsesDomZoom indicates if this WebView uses DOM-based zoom
611// In gotk4/WebKitGTK, zoom is always viewport-based
612func (w *WebView) UsesDomZoom() bool {
613 return false
614}
615
616// SeedDomZoom is a no-op in gotk4 as we use viewport zoom
617func (w *WebView) SeedDomZoom(zoom float64) error {
618 // Not needed in gotk4 - zoom is handled differently
619 return nil
620}
621
622// InjectScript executes JavaScript in the WebView
623func (w *WebView) InjectScript(script string) error {
624 w.mu.RLock()
625 defer w.mu.RUnlock()
626
627 if w.destroyed {
628 return ErrWebViewDestroyed
629 }
630
631 // Execute JavaScript using our CGO wrapper
632 EvaluateJavascript(w.view, script)
633 return nil
634}
635
636// DispatchCustomEvent dispatches a custom event via JavaScript
637func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error {
638 // Serialize data to JSON
639 jsonData, err := json.Marshal(data)
640 if err != nil {
641 return fmt.Errorf("failed to marshal event data: %w", err)
642 }
643
644 log.Printf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData))
645
646 // Use document.dispatchEvent (not window) so events cross JavaScript world boundaries
647 // The GUI scripts run in an isolated world but share the same Document object
648 script := fmt.Sprintf(`
649 (function() {
650 try {
651 var detail = %s;
652 console.log('[dumber] Dispatching event: %s', detail);
653 document.dispatchEvent(new CustomEvent('%s', { detail: detail }));
654 } catch (e) {
655 console.error('[dumber] Failed to dispatch %s', e);
656 }
657 })();
658 `, string(jsonData), eventName, eventName, eventName)
659 return w.InjectScript(script)
660}
661
662// ShowDevTools opens the WebKit inspector/developer tools
663func (w *WebView) ShowDevTools() error {
664 w.mu.RLock()
665 defer w.mu.RUnlock()
666
667 if w.destroyed {
668 return ErrWebViewDestroyed
669 }
670
671 inspector := w.view.Inspector()
672 if inspector != nil {
673 inspector.Show()
674 }
675 return nil
676}
677
678// ShowPrintDialog shows the print dialog for the current page
679func (w *WebView) ShowPrintDialog() error {
680 w.mu.RLock()
681 defer w.mu.RUnlock()
682
683 if w.destroyed {
684 return ErrWebViewDestroyed
685 }
686
687 // Create and run print operation with GTK print dialog
688 printOp := webkit.NewPrintOperation(w.view)
689 printOp.RunDialog(nil) // nil = no parent window, uses active window
690 return nil
691}
692
693// RegisterKeyboardShortcut registers a keyboard shortcut handler
694// This is a compatibility method - actual shortcut handling is done at the window level
695func (w *WebView) RegisterKeyboardShortcut(key string, modifiers uint, handler func()) error {
696 // TODO: Implement keyboard shortcut registration if needed
697 // For now, shortcuts are handled at the window/application level
698 return nil
699}
700
701// SetWindowFeatures sets window features for popup windows
702func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
703 // This is typically used for popup windows
704 // The features would be applied when creating the window
705}
706
707// IsActive returns whether this WebView is currently active/focused
708func (w *WebView) IsActive() bool {
709 w.mu.RLock()
710 defer w.mu.RUnlock()
711
712 if w.destroyed {
713 return false
714 }
715
716 widget := w.view
717 if widget != nil {
718 return widget.IsFocus()
719 }
720 return false
721}
722
723// Window returns the parent Window of this WebView
724func (w *WebView) Window() *Window {
725 w.mu.RLock()
726 defer w.mu.RUnlock()
727
728 if w.destroyed {
729 return nil
730 }
731
732 return w.window
733}
734
735// GetFaviconDatabase returns the WebKit FaviconDatabase for this WebView
736// Returns nil if the favicon database is not available
737func (w *WebView) GetFaviconDatabase() *webkit.FaviconDatabase {
738 w.mu.RLock()
739 defer w.mu.RUnlock()
740
741 if w.destroyed || w.view == nil {
742 return nil
743 }
744
745 session := w.view.NetworkSession()
746 if session == nil {
747 return nil
748 }
749
750 dataManager := session.WebsiteDataManager()
751 if dataManager == nil {
752 return nil
753 }
754
755 return dataManager.FaviconDatabase()
756}
757
758// UpdateContentFilters updates the content filtering rules
759func (w *WebView) UpdateContentFilters(rules string) error {
760 w.mu.RLock()
761 defer w.mu.RUnlock()
762
763 if w.destroyed {
764 return ErrWebViewDestroyed
765 }
766
767 // Note: Content filtering is handled via InitializeContentBlocking()
768 // This method is kept for backward compatibility but is deprecated
769 log.Printf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead")
770 return nil
771}
772
773// InitializeContentBlocking initializes content blocking with filter lists
774func (w *WebView) InitializeContentBlocking(filterJSON []byte) error {
775 w.mu.RLock()
776 defer w.mu.RUnlock()
777
778 if w.destroyed {
779 return ErrWebViewDestroyed
780 }
781
782 if len(filterJSON) == 0 {
783 return fmt.Errorf("no filter rules provided")
784 }
785
786 // Apply filters using the content blocking API
787 return ApplyFiltersToWebView(w, filterJSON)
788}
789
790// OnNavigate registers a navigation handler
791func (w *WebView) OnNavigate(handler func(url string)) {
792 w.mu.Lock()
793 defer w.mu.Unlock()
794
795 // This wraps the URI changed handler
796 w.onURIChanged = handler
797}
798
799// RegisterWorkspaceNavigationHandler registers a handler for workspace pane navigation
800// The handler receives the direction ("up", "down", "left", "right") and returns true if handled
801func (w *WebView) RegisterWorkspaceNavigationHandler(handler func(direction string) bool) {
802 w.mu.Lock()
803 defer w.mu.Unlock()
804 w.onWorkspaceNavigation = handler
805}
806
807// RegisterPaneModeHandler registers a handler for pane mode shortcuts
808// The handler receives the action ("enter", "close", "split-right", etc.) and returns true if handled
809func (w *WebView) RegisterPaneModeHandler(handler func(action string) bool, isActiveChecker func() bool) {
810 w.mu.Lock()
811 defer w.mu.Unlock()
812 w.onPaneModeShortcut = handler
813 w.isPaneModeActive = isActiveChecker
814}
815
816// setupFaviconHandlers enables favicons for this WebView
817// Note: The actual favicon-changed handler is registered ONCE at the FaviconService level
818// to avoid duplicate handlers when multiple WebViews exist
819func (w *WebView) setupFaviconHandlers() {
820 // Get the NetworkSession from the WebView
821 session := w.view.NetworkSession()
822 if session == nil {
823 log.Printf("[webkit] Warning: No NetworkSession available for favicon handling")
824 return
825 }
826
827 // Get the WebsiteDataManager from the NetworkSession
828 dataManager := session.WebsiteDataManager()
829 if dataManager == nil {
830 log.Printf("[webkit] Warning: No WebsiteDataManager available for favicon handling")
831 return
832 }
833
834 // Enable favicons if not already enabled
835 if !dataManager.FaviconsEnabled() {
836 dataManager.SetFaviconsEnabled(true)
837 log.Printf("[webkit] Enabled favicons for WebView ID %d", w.id)
838 }
839
840}
841
842// GtkWebView returns the underlying gotk4 WebView for advanced operations
843// This is used when creating related views (popups) that need to share the same session
844func (w *WebView) GtkWebView() *webkit.WebView {
845 w.mu.RLock()
846 defer w.mu.RUnlock()
847 return w.view
848}
849
850// WrapBareWebView creates a minimal WebView wrapper around a bare gotk4 WebView
851// This is used during popup creation to return a WebView to WebKit before initialization
852// Full initialization (container, scripts, event handlers) should be done later
853func WrapBareWebView(bareView *webkit.WebView) *WebView {
854 if bareView == nil {
855 return nil
856 }
857
858 // Generate unique ID
859 id := atomic.AddUint64(&viewIDCounter, 1)
860 log.Printf("[webkit] Created minimal wrapper for bare WebView (ID: %d)", id)
861
862 // Create minimal wrapper - NO initialization yet
863 wv := &WebView{
864 view: bareView,
865 container: nil, // Will be created during full initialization
866 window: nil,
867 id: id,
868 config: nil, // Will be set during full initialization
869 }
870
871 // Register in global registry
872 viewMu.Lock()
873 viewRegistry[id] = wv
874 viewMu.Unlock()
875
876 return wv
877}
878
879// InitializeFromBare completes initialization of a bare WebView wrapper
880// This should be called in ready-to-show after WebKit has configured the view
881func (w *WebView) InitializeFromBare(cfg *Config) error {
882 w.mu.Lock()
883 defer w.mu.Unlock()
884
885 if w.config != nil {
886 return fmt.Errorf("WebView already initialized")
887 }
888
889 if cfg == nil {
890 cfg = GetDefaultConfig()
891 }
892
893 w.config = cfg
894
895 // Verify persistent session
896 viewSession := w.view.NetworkSession()
897 if viewSession == nil {
898 return fmt.Errorf("WebView has no network session")
899 }
900
901 // Setup UserContentManager and inject GUI scripts
902 if err := SetupUserContentManager(w.view, cfg.AppearanceConfigJSON, w.id); err != nil {
903 return fmt.Errorf("failed to setup user content manager: %w", err)
904 }
905
906 // Create container (GtkBox) to hold the WebView
907 container := gtk.NewBox(gtk.OrientationVertical, 0)
908 container.SetHExpand(true)
909 container.SetVExpand(true)
910
911 // Configure WebView widget for expansion
912 w.view.SetHExpand(true)
913 w.view.SetVExpand(true)
914
915 // Add WebView to container
916 container.Append(w.view)
917
918 w.container = container
919
920 // Apply configuration
921 if err := w.applyConfig(); err != nil {
922 return err
923 }
924
925 // Setup event handlers
926 w.setupEventHandlers()
927
928 // Attach keyboard bridge
929 w.AttachKeyboardBridge()
930
931 // Attach mouse gesture controls
932 w.AttachMouseGestures()
933
934 // Attach touchpad gesture controls
935 w.AttachTouchpadGestures()
936
937 log.Printf("[webkit] Completed initialization for WebView ID %d", w.id)
938
939 return nil
940}