feat(favicon): implement WebKit native favicon detection system
- Add WebKit's built-in favicon database integration for automatic favicon detection - Support all favicon formats (PNG, SVG, ICO) without external dependencies - Implement binary data caching with direct PNG storage - Add favicon URI change handlers for comprehensive coverage - Enhance favicon cache with URL-based path resolution - Replace legacy favicon fetching with native WebKit callbacks
- commit
- 147e40474890557387cfa387ebb136afee771af6
- author
- bamen · · 2025-09-23 16:20 UTC
- committer
- bamen · · 2025-09-23 16:07 UTC
- parent
- 75a1db0
- signature
- unsigned
5 files · +250 -49
M internal/cache/favicon.go +32 -0
| @@ -73,6 +73,38 @@ func (fc *FaviconCache) CacheAsync(faviconURL string) { | |||
| 73 | 73 | }() | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | + | // CachePNGData caches PNG favicon data directly for a URL | |
| 77 | + | func (fc *FaviconCache) CachePNGData(pageURL string, pngData []byte) error { | |
| 78 | + | if pageURL == "" || len(pngData) == 0 { | |
| 79 | + | return fmt.Errorf("invalid input: empty URL or data") | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | filename := fc.getFilename(pageURL) | |
| 83 | + | cachedPath := filepath.Join(fc.cacheDir, filename) | |
| 84 | + | ||
| 85 | + | // Write PNG data directly to cache | |
| 86 | + | return os.WriteFile(cachedPath, pngData, 0644) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // GetCachedPathForURL returns the cached path for a page URL (not favicon URL) | |
| 90 | + | func (fc *FaviconCache) GetCachedPathForURL(pageURL string) string { | |
| 91 | + | if pageURL == "" { | |
| 92 | + | return "" | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | filename := fc.getFilename(pageURL) | |
| 96 | + | cachedPath := filepath.Join(fc.cacheDir, filename) | |
| 97 | + | ||
| 98 | + | // Check if file exists and is not too old (7 days) | |
| 99 | + | if info, err := os.Stat(cachedPath); err == nil { | |
| 100 | + | if time.Since(info.ModTime()) < 7*24*time.Hour { | |
| 101 | + | return cachedPath | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | return "" | |
| 106 | + | } | |
| 107 | + | ||
| 76 | 108 | // getFilename generates a safe filename for a favicon URL - always PNG for fuzzel compatibility | |
| 77 | 109 | func (fc *FaviconCache) getFilename(faviconURL string) string { | |
| 78 | 110 | hash := fmt.Sprintf("%x", md5.Sum([]byte(faviconURL))) | |
| @@ -73,6 +73,38 @@ func (fc *FaviconCache) CacheAsync(faviconURL string) { | |||||
| 73 | }() | 73 | }() | ||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | + | // CachePNGData caches PNG favicon data directly for a URL | |||
| 77 | + | func (fc *FaviconCache) CachePNGData(pageURL string, pngData []byte) error { | |||
| 78 | + | if pageURL == "" || len(pngData) == 0 { | |||
| 79 | + | return fmt.Errorf("invalid input: empty URL or data") | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | filename := fc.getFilename(pageURL) | |||
| 83 | + | cachedPath := filepath.Join(fc.cacheDir, filename) | |||
| 84 | + | ||||
| 85 | + | // Write PNG data directly to cache | |||
| 86 | + | return os.WriteFile(cachedPath, pngData, 0644) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // GetCachedPathForURL returns the cached path for a page URL (not favicon URL) | |||
| 90 | + | func (fc *FaviconCache) GetCachedPathForURL(pageURL string) string { | |||
| 91 | + | if pageURL == "" { | |||
| 92 | + | return "" | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | filename := fc.getFilename(pageURL) | |||
| 96 | + | cachedPath := filepath.Join(fc.cacheDir, filename) | |||
| 97 | + | ||||
| 98 | + | // Check if file exists and is not too old (7 days) | |||
| 99 | + | if info, err := os.Stat(cachedPath); err == nil { | |||
| 100 | + | if time.Since(info.ModTime()) < 7*24*time.Hour { | |||
| 101 | + | return cachedPath | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | return "" | |||
| 106 | + | } | |||
| 107 | + | ||||
| 76 | // getFilename generates a safe filename for a favicon URL - always PNG for fuzzel compatibility | 108 | // getFilename generates a safe filename for a favicon URL - always PNG for fuzzel compatibility | ||
| 77 | func (fc *FaviconCache) getFilename(faviconURL string) string { | 109 | func (fc *FaviconCache) getFilename(faviconURL string) string { | ||
| 78 | hash := fmt.Sprintf("%x", md5.Sum([]byte(faviconURL))) | 110 | hash := fmt.Sprintf("%x", md5.Sum([]byte(faviconURL))) | ||
M internal/services/browser.go +73 -12
| @@ -3,6 +3,7 @@ package services | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "context" | |
| 6 | + | "crypto/md5" | |
| 6 | 7 | "database/sql" | |
| 7 | 8 | "embed" | |
| 8 | 9 | "errors" | |
| @@ -90,6 +91,19 @@ func (s *BrowserService) SetWindowTitleUpdater(updater WindowTitleUpdater) { | |||
| 90 | 91 | // AttachWebView connects a native WebKit WebView to this service for integration. | |
| 91 | 92 | func (s *BrowserService) AttachWebView(view *webkit.WebView) { | |
| 92 | 93 | s.webView = view | |
| 94 | + | ||
| 95 | + | // Register legacy favicon change handler (binary data - fallback) | |
| 96 | + | view.RegisterFaviconChangedHandler(func(data []byte) { | |
| 97 | + | if currentURL := view.GetCurrentURL(); currentURL != "" { | |
| 98 | + | log.Printf("[favicon] Favicon detected for %s, data size: %d bytes", currentURL, len(data)) | |
| 99 | + | s.handleFaviconChanged(currentURL, data) | |
| 100 | + | } | |
| 101 | + | }) | |
| 102 | + | ||
| 103 | + | // Register favicon URI change handler (WebKit native) | |
| 104 | + | view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) { | |
| 105 | + | s.handleFaviconURIChanged(pageURI, faviconURI) | |
| 106 | + | }) | |
| 93 | 107 | } | |
| 94 | 108 | ||
| 95 | 109 | // LoadGUIBundle loads the unified GUI bundle from assets | |
| @@ -189,8 +203,7 @@ func (s *BrowserService) Navigate(ctx context.Context, url string) (*NavigationR | |||
| 189 | 203 | fmt.Printf("Failed to record history: %v\n", err) | |
| 190 | 204 | } | |
| 191 | 205 | ||
| 192 | - | // Update favicon asynchronously after navigation | |
| 193 | - | go s.updateFavicon(ctx, url) | |
| 206 | + | // Favicon detection is now handled automatically by WebKit's native favicon database | |
| 194 | 207 | ||
| 195 | 208 | return &NavigationResult{ | |
| 196 | 209 | URL: url, | |
| @@ -576,11 +589,22 @@ func zoomKeyFromURL(raw string) string { | |||
| 576 | 589 | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | |
| 577 | 590 | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | |
| 578 | 591 | ||
| 579 | - | // updateFavicon fetches and stores favicon URL for a given page URL | |
| 580 | - | func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) { | |
| 592 | + | ||
| 593 | + | // handleFaviconChanged processes favicon data received from WebKit (legacy binary data method) | |
| 594 | + | func (s *BrowserService) handleFaviconChanged(pageURL string, pngData []byte) { | |
| 595 | + | log.Printf("[favicon] Processing favicon for %s, data size: %d bytes", pageURL, len(pngData)) | |
| 596 | + | ||
| 597 | + | if len(pngData) == 0 { | |
| 598 | + | log.Printf("[favicon] No favicon data for %s", pageURL) | |
| 599 | + | return | |
| 600 | + | } | |
| 601 | + | ||
| 602 | + | ctx := context.Background() | |
| 603 | + | ||
| 604 | + | // Parse URL for validation | |
| 581 | 605 | parsedURL, err := neturl.Parse(pageURL) | |
| 582 | 606 | if err != nil { | |
| 583 | - | return // Silently fail for invalid URLs | |
| 607 | + | return | |
| 584 | 608 | } | |
| 585 | 609 | ||
| 586 | 610 | // Skip favicon update for localhost, file://, or special schemes | |
| @@ -591,18 +615,55 @@ func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) { | |||
| 591 | 615 | return | |
| 592 | 616 | } | |
| 593 | 617 | ||
| 594 | - | // Standard favicon location | |
| 595 | - | faviconURL := fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host) | |
| 618 | + | // Cache the PNG data directly | |
| 619 | + | if faviconCache, err := cache.NewFaviconCache(); err == nil { | |
| 620 | + | if err := faviconCache.CachePNGData(pageURL, pngData); err != nil { | |
| 621 | + | // Log error but don't fail | |
| 622 | + | log.Printf("[favicon] Failed to cache favicon for %s: %v", pageURL, err) | |
| 623 | + | } else { | |
| 624 | + | log.Printf("[favicon] Successfully cached favicon for %s (%d bytes)", pageURL, len(pngData)) | |
| 625 | + | } | |
| 626 | + | } else { | |
| 627 | + | log.Printf("[favicon] Failed to create favicon cache: %v", err) | |
| 628 | + | } | |
| 596 | 629 | ||
| 597 | - | // Update in database | |
| 598 | - | faviconNullString := sql.NullString{String: faviconURL, Valid: true} | |
| 630 | + | // Store a reference to the cached file in the database | |
| 631 | + | // We use a consistent path format for the favicon URL | |
| 632 | + | faviconCacheURL := fmt.Sprintf("dumber://favicon/%x.png", md5.Sum([]byte(pageURL))) | |
| 633 | + | faviconNullString := sql.NullString{String: faviconCacheURL, Valid: true} | |
| 599 | 634 | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |
| 600 | 635 | // Silently fail - favicon is not critical | |
| 636 | + | log.Printf("[browser] Failed to update favicon URL in database for %s: %v", pageURL, err) | |
| 637 | + | } | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | // handleFaviconURIChanged processes favicon URI changes from WebKit's native favicon database | |
| 641 | + | func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI string) { | |
| 642 | + | log.Printf("[favicon] Processing favicon URI for %s: %s", pageURL, faviconURI) | |
| 643 | + | ||
| 644 | + | ctx := context.Background() | |
| 645 | + | ||
| 646 | + | // Parse URL for validation | |
| 647 | + | parsedURL, err := neturl.Parse(pageURL) | |
| 648 | + | if err != nil { | |
| 649 | + | log.Printf("[favicon] Invalid page URL: %s", pageURL) | |
| 601 | 650 | return | |
| 602 | 651 | } | |
| 603 | 652 | ||
| 604 | - | // Also cache the favicon for dmenu use | |
| 605 | - | if faviconCache, err := cache.NewFaviconCache(); err == nil { | |
| 606 | - | faviconCache.CacheAsync(faviconURL) | |
| 653 | + | // Skip favicon update for localhost, file://, or special schemes | |
| 654 | + | if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { | |
| 655 | + | return | |
| 656 | + | } | |
| 657 | + | if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" { | |
| 658 | + | return | |
| 659 | + | } | |
| 660 | + | ||
| 661 | + | // Store the favicon URI directly in the database | |
| 662 | + | // The frontend will handle SVG vs PNG/ICO detection | |
| 663 | + | faviconNullString := sql.NullString{String: faviconURI, Valid: true} | |
| 664 | + | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |
| 665 | + | log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err) | |
| 666 | + | } else { | |
| 667 | + | log.Printf("[browser] Successfully stored favicon URI for %s: %s", pageURL, faviconURI) | |
| 607 | 668 | } | |
| 608 | 669 | } | |
| @@ -3,6 +3,7 @@ package services | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "context" | 5 | "context" | ||
| 6 | + | "crypto/md5" | |||
| 6 | "database/sql" | 7 | "database/sql" | ||
| 7 | "embed" | 8 | "embed" | ||
| 8 | "errors" | 9 | "errors" | ||
| @@ -90,6 +91,19 @@ func (s *BrowserService) SetWindowTitleUpdater(updater WindowTitleUpdater) { | |||||
| 90 | // AttachWebView connects a native WebKit WebView to this service for integration. | 91 | // AttachWebView connects a native WebKit WebView to this service for integration. | ||
| 91 | func (s *BrowserService) AttachWebView(view *webkit.WebView) { | 92 | func (s *BrowserService) AttachWebView(view *webkit.WebView) { | ||
| 92 | s.webView = view | 93 | s.webView = view | ||
| 94 | + | ||||
| 95 | + | // Register legacy favicon change handler (binary data - fallback) | |||
| 96 | + | view.RegisterFaviconChangedHandler(func(data []byte) { | |||
| 97 | + | if currentURL := view.GetCurrentURL(); currentURL != "" { | |||
| 98 | + | log.Printf("[favicon] Favicon detected for %s, data size: %d bytes", currentURL, len(data)) | |||
| 99 | + | s.handleFaviconChanged(currentURL, data) | |||
| 100 | + | } | |||
| 101 | + | }) | |||
| 102 | + | ||||
| 103 | + | // Register favicon URI change handler (WebKit native) | |||
| 104 | + | view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) { | |||
| 105 | + | s.handleFaviconURIChanged(pageURI, faviconURI) | |||
| 106 | + | }) | |||
| 93 | } | 107 | } | ||
| 94 | 108 | ||||
| 95 | // LoadGUIBundle loads the unified GUI bundle from assets | 109 | // LoadGUIBundle loads the unified GUI bundle from assets | ||
| @@ -189,8 +203,7 @@ func (s *BrowserService) Navigate(ctx context.Context, url string) (*NavigationR | |||||
| 189 | fmt.Printf("Failed to record history: %v\n", err) | 203 | fmt.Printf("Failed to record history: %v\n", err) | ||
| 190 | } | 204 | } | ||
| 191 | 205 | ||||
| 192 | - | // Update favicon asynchronously after navigation | 206 | + | // Favicon detection is now handled automatically by WebKit's native favicon database |
| 193 | - | go s.updateFavicon(ctx, url) | |||
| 194 | 207 | ||||
| 195 | return &NavigationResult{ | 208 | return &NavigationResult{ | ||
| 196 | URL: url, | 209 | URL: url, | ||
| @@ -576,11 +589,22 @@ func zoomKeyFromURL(raw string) string { | |||||
| 576 | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | 589 | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | ||
| 577 | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | 590 | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | ||
| 578 | 591 | ||||
| 579 | - | // updateFavicon fetches and stores favicon URL for a given page URL | 592 | + | |
| 580 | - | func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) { | 593 | + | // handleFaviconChanged processes favicon data received from WebKit (legacy binary data method) |
| 594 | + | func (s *BrowserService) handleFaviconChanged(pageURL string, pngData []byte) { | |||
| 595 | + | log.Printf("[favicon] Processing favicon for %s, data size: %d bytes", pageURL, len(pngData)) | |||
| 596 | + | ||||
| 597 | + | if len(pngData) == 0 { | |||
| 598 | + | log.Printf("[favicon] No favicon data for %s", pageURL) | |||
| 599 | + | return | |||
| 600 | + | } | |||
| 601 | + | ||||
| 602 | + | ctx := context.Background() | |||
| 603 | + | ||||
| 604 | + | // Parse URL for validation | |||
| 581 | parsedURL, err := neturl.Parse(pageURL) | 605 | parsedURL, err := neturl.Parse(pageURL) | ||
| 582 | if err != nil { | 606 | if err != nil { | ||
| 583 | - | return // Silently fail for invalid URLs | 607 | + | return |
| 584 | } | 608 | } | ||
| 585 | 609 | ||||
| 586 | // Skip favicon update for localhost, file://, or special schemes | 610 | // Skip favicon update for localhost, file://, or special schemes | ||
| @@ -591,18 +615,55 @@ func (s *BrowserService) updateFavicon(ctx context.Context, pageURL string) { | |||||
| 591 | return | 615 | return | ||
| 592 | } | 616 | } | ||
| 593 | 617 | ||||
| 594 | - | // Standard favicon location | 618 | + | // Cache the PNG data directly |
| 595 | - | faviconURL := fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host) | 619 | + | if faviconCache, err := cache.NewFaviconCache(); err == nil { |
| 620 | + | if err := faviconCache.CachePNGData(pageURL, pngData); err != nil { | |||
| 621 | + | // Log error but don't fail | |||
| 622 | + | log.Printf("[favicon] Failed to cache favicon for %s: %v", pageURL, err) | |||
| 623 | + | } else { | |||
| 624 | + | log.Printf("[favicon] Successfully cached favicon for %s (%d bytes)", pageURL, len(pngData)) | |||
| 625 | + | } | |||
| 626 | + | } else { | |||
| 627 | + | log.Printf("[favicon] Failed to create favicon cache: %v", err) | |||
| 628 | + | } | |||
| 596 | 629 | ||||
| 597 | - | // Update in database | 630 | + | // Store a reference to the cached file in the database |
| 598 | - | faviconNullString := sql.NullString{String: faviconURL, Valid: true} | 631 | + | // We use a consistent path format for the favicon URL |
| 632 | + | faviconCacheURL := fmt.Sprintf("dumber://favicon/%x.png", md5.Sum([]byte(pageURL))) | |||
| 633 | + | faviconNullString := sql.NullString{String: faviconCacheURL, Valid: true} | |||
| 599 | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | 634 | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | ||
| 600 | // Silently fail - favicon is not critical | 635 | // Silently fail - favicon is not critical | ||
| 636 | + | log.Printf("[browser] Failed to update favicon URL in database for %s: %v", pageURL, err) | |||
| 637 | + | } | |||
| 638 | + | } | |||
| 639 | + | ||||
| 640 | + | // handleFaviconURIChanged processes favicon URI changes from WebKit's native favicon database | |||
| 641 | + | func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI string) { | |||
| 642 | + | log.Printf("[favicon] Processing favicon URI for %s: %s", pageURL, faviconURI) | |||
| 643 | + | ||||
| 644 | + | ctx := context.Background() | |||
| 645 | + | ||||
| 646 | + | // Parse URL for validation | |||
| 647 | + | parsedURL, err := neturl.Parse(pageURL) | |||
| 648 | + | if err != nil { | |||
| 649 | + | log.Printf("[favicon] Invalid page URL: %s", pageURL) | |||
| 601 | return | 650 | return | ||
| 602 | } | 651 | } | ||
| 603 | 652 | ||||
| 604 | - | // Also cache the favicon for dmenu use | 653 | + | // Skip favicon update for localhost, file://, or special schemes |
| 605 | - | if faviconCache, err := cache.NewFaviconCache(); err == nil { | 654 | + | if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { |
| 606 | - | faviconCache.CacheAsync(faviconURL) | 655 | + | return |
| 656 | + | } | |||
| 657 | + | if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" { | |||
| 658 | + | return | |||
| 659 | + | } | |||
| 660 | + | ||||
| 661 | + | // Store the favicon URI directly in the database | |||
| 662 | + | // The frontend will handle SVG vs PNG/ICO detection | |||
| 663 | + | faviconNullString := sql.NullString{String: faviconURI, Valid: true} | |||
| 664 | + | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |||
| 665 | + | log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err) | |||
| 666 | + | } else { | |||
| 667 | + | log.Printf("[browser] Successfully stored favicon URI for %s: %s", pageURL, faviconURI) | |||
| 607 | } | 668 | } | ||
| 608 | } | 669 | } | ||
M pkg/webkit/webview.go +37 -16
| @@ -21,22 +21,24 @@ func nextStubViewID() uintptr { | |||
| 21 | 21 | // WebView represents a browser view powered by WebKit2GTK. | |
| 22 | 22 | // Methods are currently stubs returning ErrNotImplemented to satisfy TDD ordering. | |
| 23 | 23 | type WebView struct { | |
| 24 | - | config *Config | |
| 25 | - | visible bool | |
| 26 | - | zoom float64 | |
| 27 | - | url string | |
| 28 | - | destroyed bool | |
| 29 | - | window *Window | |
| 30 | - | msgHandler func(payload string) | |
| 31 | - | titleHandler func(title string) | |
| 32 | - | uriHandler func(uri string) | |
| 33 | - | zoomHandler func(level float64) | |
| 34 | - | popupHandler func(string) *WebView | |
| 35 | - | closeHandler func() | |
| 36 | - | useDomZoom bool | |
| 37 | - | domZoomSeed float64 | |
| 38 | - | container uintptr | |
| 39 | - | id uintptr // WebView unique identifier | |
| 24 | + | config *Config | |
| 25 | + | visible bool | |
| 26 | + | zoom float64 | |
| 27 | + | url string | |
| 28 | + | destroyed bool | |
| 29 | + | window *Window | |
| 30 | + | msgHandler func(payload string) | |
| 31 | + | titleHandler func(title string) | |
| 32 | + | uriHandler func(uri string) | |
| 33 | + | faviconHandler func(data []byte) | |
| 34 | + | faviconURIHandler func(pageURI, faviconURI string) | |
| 35 | + | zoomHandler func(level float64) | |
| 36 | + | popupHandler func(string) *WebView | |
| 37 | + | closeHandler func() | |
| 38 | + | useDomZoom bool | |
| 39 | + | domZoomSeed float64 | |
| 40 | + | container uintptr | |
| 41 | + | id uintptr // WebView unique identifier | |
| 40 | 42 | ||
| 41 | 43 | // Window type fields (no-op in stub) | |
| 42 | 44 | windowType WindowType | |
| @@ -112,6 +114,11 @@ func (w *WebView) Destroy() error { | |||
| 112 | 114 | return nil | |
| 113 | 115 | } | |
| 114 | 116 | ||
| 117 | + | // IsDestroyed returns whether this WebView has been destroyed | |
| 118 | + | func (w *WebView) IsDestroyed() bool { | |
| 119 | + | return w != nil && w.destroyed | |
| 120 | + | } | |
| 121 | + | ||
| 115 | 122 | // Window returns the associated native window wrapper (non-nil). | |
| 116 | 123 | func (w *WebView) Window() *Window { return w.window } | |
| 117 | 124 | ||
| @@ -182,6 +189,20 @@ func (w *WebView) dispatchURIChanged(uri string) { //nolint:unused // Called fro | |||
| 182 | 189 | } | |
| 183 | 190 | } | |
| 184 | 191 | ||
| 192 | + | // RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes. | |
| 193 | + | func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) { w.faviconHandler = cb } | |
| 194 | + | ||
| 195 | + | // RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI. | |
| 196 | + | func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) { | |
| 197 | + | w.faviconURIHandler = cb | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func (w *WebView) dispatchFaviconChanged(data []byte) { //nolint:unused // Called from CGO WebKit callbacks | |
| 201 | + | if w != nil && w.faviconHandler != nil { | |
| 202 | + | w.faviconHandler(data) | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | ||
| 185 | 206 | // RegisterZoomChangedHandler registers a callback invoked when zoom level changes. | |
| 186 | 207 | func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb } | |
| 187 | 208 | ||
| @@ -21,22 +21,24 @@ func nextStubViewID() uintptr { | |||||
| 21 | // WebView represents a browser view powered by WebKit2GTK. | 21 | // WebView represents a browser view powered by WebKit2GTK. | ||
| 22 | // Methods are currently stubs returning ErrNotImplemented to satisfy TDD ordering. | 22 | // Methods are currently stubs returning ErrNotImplemented to satisfy TDD ordering. | ||
| 23 | type WebView struct { | 23 | type WebView struct { | ||
| 24 | - | config *Config | 24 | + | config *Config |
| 25 | - | visible bool | 25 | + | visible bool |
| 26 | - | zoom float64 | 26 | + | zoom float64 |
| 27 | - | url string | 27 | + | url string |
| 28 | - | destroyed bool | 28 | + | destroyed bool |
| 29 | - | window *Window | 29 | + | window *Window |
| 30 | - | msgHandler func(payload string) | 30 | + | msgHandler func(payload string) |
| 31 | - | titleHandler func(title string) | 31 | + | titleHandler func(title string) |
| 32 | - | uriHandler func(uri string) | 32 | + | uriHandler func(uri string) |
| 33 | - | zoomHandler func(level float64) | 33 | + | faviconHandler func(data []byte) |
| 34 | - | popupHandler func(string) *WebView | 34 | + | faviconURIHandler func(pageURI, faviconURI string) |
| 35 | - | closeHandler func() | 35 | + | zoomHandler func(level float64) |
| 36 | - | useDomZoom bool | 36 | + | popupHandler func(string) *WebView |
| 37 | - | domZoomSeed float64 | 37 | + | closeHandler func() |
| 38 | - | container uintptr | 38 | + | useDomZoom bool |
| 39 | - | id uintptr // WebView unique identifier | 39 | + | domZoomSeed float64 |
| 40 | + | container uintptr | |||
| 41 | + | id uintptr // WebView unique identifier | |||
| 40 | 42 | ||||
| 41 | // Window type fields (no-op in stub) | 43 | // Window type fields (no-op in stub) | ||
| 42 | windowType WindowType | 44 | windowType WindowType | ||
| @@ -112,6 +114,11 @@ func (w *WebView) Destroy() error { | |||||
| 112 | return nil | 114 | return nil | ||
| 113 | } | 115 | } | ||
| 114 | 116 | ||||
| 117 | + | // IsDestroyed returns whether this WebView has been destroyed | |||
| 118 | + | func (w *WebView) IsDestroyed() bool { | |||
| 119 | + | return w != nil && w.destroyed | |||
| 120 | + | } | |||
| 121 | + | ||||
| 115 | // Window returns the associated native window wrapper (non-nil). | 122 | // Window returns the associated native window wrapper (non-nil). | ||
| 116 | func (w *WebView) Window() *Window { return w.window } | 123 | func (w *WebView) Window() *Window { return w.window } | ||
| 117 | 124 | ||||
| @@ -182,6 +189,20 @@ func (w *WebView) dispatchURIChanged(uri string) { //nolint:unused // Called fro | |||||
| 182 | } | 189 | } | ||
| 183 | } | 190 | } | ||
| 184 | 191 | ||||
| 192 | + | // RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes. | |||
| 193 | + | func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) { w.faviconHandler = cb } | |||
| 194 | + | ||||
| 195 | + | // RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI. | |||
| 196 | + | func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) { | |||
| 197 | + | w.faviconURIHandler = cb | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func (w *WebView) dispatchFaviconChanged(data []byte) { //nolint:unused // Called from CGO WebKit callbacks | |||
| 201 | + | if w != nil && w.faviconHandler != nil { | |||
| 202 | + | w.faviconHandler(data) | |||
| 203 | + | } | |||
| 204 | + | } | |||
| 205 | + | ||||
| 185 | // RegisterZoomChangedHandler registers a callback invoked when zoom level changes. | 206 | // RegisterZoomChangedHandler registers a callback invoked when zoom level changes. | ||
| 186 | func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb } | 207 | func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb } | ||
| 187 | 208 | ||||
M pkg/webkit/webview_callbacks.h +16 -0
| @@ -9,6 +9,7 @@ | |||
| 9 | 9 | extern void goOnTitleChanged(unsigned long id, char* title); | |
| 10 | 10 | extern void goOnURIChanged(unsigned long id, char* uri); | |
| 11 | 11 | extern void goOnThemeChanged(unsigned long id, int prefer_dark); | |
| 12 | + | extern void goOnFaviconURIChanged(unsigned long id, char* page_uri, char* favicon_uri); | |
| 12 | 13 | extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime); | |
| 13 | 14 | extern void goQuitMainLoop(); | |
| 14 | 15 | ||
| @@ -62,6 +63,21 @@ static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_ | |||
| 62 | 63 | } | |
| 63 | 64 | } | |
| 64 | 65 | ||
| 66 | + | static inline void on_favicon_changed(WebKitFaviconDatabase* database, gchar* page_uri, gchar* favicon_uri, gpointer user_data) { | |
| 67 | + | (void)database; | |
| 68 | + | printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n", | |
| 69 | + | page_uri ? page_uri : "NULL", favicon_uri ? favicon_uri : "NULL", (unsigned long)user_data); | |
| 70 | + | ||
| 71 | + | if (!favicon_uri || !page_uri) { | |
| 72 | + | printf("[favicon-c] Invalid favicon or page URI for WebView ID %lu\n", (unsigned long)user_data); | |
| 73 | + | return; | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // Pass the page URI and favicon URI to Go | |
| 77 | + | // WebKit manages the actual favicon data - we just need to store the URI mapping | |
| 78 | + | goOnFaviconURIChanged((unsigned long)user_data, page_uri, favicon_uri); | |
| 79 | + | } | |
| 80 | + | ||
| 65 | 81 | // GTK4 close-request signal handler | |
| 66 | 82 | static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) { | |
| 67 | 83 | (void)window; (void)user_data; | |
| @@ -9,6 +9,7 @@ | |||||
| 9 | extern void goOnTitleChanged(unsigned long id, char* title); | 9 | extern void goOnTitleChanged(unsigned long id, char* title); | ||
| 10 | extern void goOnURIChanged(unsigned long id, char* uri); | 10 | extern void goOnURIChanged(unsigned long id, char* uri); | ||
| 11 | extern void goOnThemeChanged(unsigned long id, int prefer_dark); | 11 | extern void goOnThemeChanged(unsigned long id, int prefer_dark); | ||
| 12 | + | extern void goOnFaviconURIChanged(unsigned long id, char* page_uri, char* favicon_uri); | |||
| 12 | extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime); | 13 | extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime); | ||
| 13 | extern void goQuitMainLoop(); | 14 | extern void goQuitMainLoop(); | ||
| 14 | 15 | ||||
| @@ -62,6 +63,21 @@ static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_ | |||||
| 62 | } | 63 | } | ||
| 63 | } | 64 | } | ||
| 64 | 65 | ||||
| 66 | + | static inline void on_favicon_changed(WebKitFaviconDatabase* database, gchar* page_uri, gchar* favicon_uri, gpointer user_data) { | |||
| 67 | + | (void)database; | |||
| 68 | + | printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n", | |||
| 69 | + | page_uri ? page_uri : "NULL", favicon_uri ? favicon_uri : "NULL", (unsigned long)user_data); | |||
| 70 | + | ||||
| 71 | + | if (!favicon_uri || !page_uri) { | |||
| 72 | + | printf("[favicon-c] Invalid favicon or page URI for WebView ID %lu\n", (unsigned long)user_data); | |||
| 73 | + | return; | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | // Pass the page URI and favicon URI to Go | |||
| 77 | + | // WebKit manages the actual favicon data - we just need to store the URI mapping | |||
| 78 | + | goOnFaviconURIChanged((unsigned long)user_data, page_uri, favicon_uri); | |||
| 79 | + | } | |||
| 80 | + | ||||
| 65 | // GTK4 close-request signal handler | 81 | // GTK4 close-request signal handler | ||
| 66 | static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) { | 82 | static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) { | ||
| 67 | (void)window; (void)user_data; | 83 | (void)window; (void)user_data; | ||
M pkg/webkit/webview_cgo.go +92 -21
| @@ -336,6 +336,28 @@ static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) { | |||
| 336 | 336 | g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0); | |
| 337 | 337 | } | |
| 338 | 338 | ||
| 339 | + | static void connect_favicon_changed_with_id(GtkWidget* widget, unsigned long id) { | |
| 340 | + | if (!widget) return; | |
| 341 | + | WebKitWebView* web_view = WEBKIT_WEB_VIEW(widget); | |
| 342 | + | WebKitNetworkSession* network_session = webkit_web_view_get_network_session(web_view); | |
| 343 | + | if (network_session) { | |
| 344 | + | WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(network_session); | |
| 345 | + | if (data_manager) { | |
| 346 | + | WebKitFaviconDatabase* favicon_db = webkit_website_data_manager_get_favicon_database(data_manager); | |
| 347 | + | if (favicon_db) { | |
| 348 | + | g_signal_connect_data(G_OBJECT(favicon_db), "favicon-changed", G_CALLBACK(on_favicon_changed), (gpointer)id, NULL, 0); | |
| 349 | + | printf("[favicon-c] Connected favicon-changed signal for WebView ID %lu\n", id); | |
| 350 | + | } else { | |
| 351 | + | printf("[favicon-c] Failed to get favicon database\n"); | |
| 352 | + | } | |
| 353 | + | } else { | |
| 354 | + | printf("[favicon-c] Failed to get website data manager for favicon signal\n"); | |
| 355 | + | } | |
| 356 | + | } else { | |
| 357 | + | printf("[favicon-c] Failed to get network session\n"); | |
| 358 | + | } | |
| 359 | + | } | |
| 360 | + | ||
| 339 | 361 | static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) { | |
| 340 | 362 | if (!settings) return; | |
| 341 | 363 | g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0); | |
| @@ -358,6 +380,17 @@ static void register_uri_scheme_security(WebKitWebContext* context, const char* | |||
| 358 | 380 | webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme); | |
| 359 | 381 | } | |
| 360 | 382 | ||
| 383 | + | static void enable_favicon_database(WebKitNetworkSession* session) { | |
| 384 | + | if (!session) return; | |
| 385 | + | WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(session); | |
| 386 | + | if (data_manager) { | |
| 387 | + | webkit_website_data_manager_set_favicons_enabled(data_manager, TRUE); | |
| 388 | + | printf("[favicon-c] Favicon database enabled\n"); | |
| 389 | + | } else { | |
| 390 | + | printf("[favicon-c] Failed to get website data manager\n"); | |
| 391 | + | } | |
| 392 | + | } | |
| 393 | + | ||
| 361 | 394 | // Script message handler wiring for WebKitGTK 6 | |
| 362 | 395 | // Forward declaration for callback used in g_signal_connect_data | |
| 363 | 396 | // WebKitGTK 6 delivers JSCValue* for script-message-received::NAME | |
| @@ -929,27 +962,29 @@ type memoryStats struct { | |||
| 929 | 962 | ||
| 930 | 963 | // WebView represents a browser view powered by WebKit2GTK. | |
| 931 | 964 | type WebView struct { | |
| 932 | - | config *Config | |
| 933 | - | zoom float64 | |
| 934 | - | url string | |
| 935 | - | destroyed bool | |
| 936 | - | native *nativeView | |
| 937 | - | window *Window | |
| 938 | - | id uintptr | |
| 939 | - | msgHandler func(payload string) | |
| 940 | - | titleHandler func(title string) | |
| 941 | - | uriHandler func(uri string) | |
| 942 | - | zoomHandler func(level float64) | |
| 943 | - | popupHandler func(string) *WebView | |
| 944 | - | closeHandler func() | |
| 945 | - | memStats *memoryStats | |
| 946 | - | gcTicker *time.Ticker | |
| 947 | - | gcDone chan struct{} | |
| 948 | - | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | |
| 949 | - | useDomZoom bool | |
| 950 | - | domZoomSeed float64 | |
| 951 | - | domBridgeScript *C.WebKitUserScript | |
| 952 | - | wasReparented bool // Track if this WebView has been reparented | |
| 965 | + | config *Config | |
| 966 | + | zoom float64 | |
| 967 | + | url string | |
| 968 | + | destroyed bool | |
| 969 | + | native *nativeView | |
| 970 | + | window *Window | |
| 971 | + | id uintptr | |
| 972 | + | msgHandler func(payload string) | |
| 973 | + | titleHandler func(title string) | |
| 974 | + | uriHandler func(uri string) | |
| 975 | + | faviconHandler func(data []byte) | |
| 976 | + | faviconURIHandler func(pageURI, faviconURI string) | |
| 977 | + | zoomHandler func(level float64) | |
| 978 | + | popupHandler func(string) *WebView | |
| 979 | + | closeHandler func() | |
| 980 | + | memStats *memoryStats | |
| 981 | + | gcTicker *time.Ticker | |
| 982 | + | gcDone chan struct{} | |
| 983 | + | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | |
| 984 | + | useDomZoom bool | |
| 985 | + | domZoomSeed float64 | |
| 986 | + | domBridgeScript *C.WebKitUserScript | |
| 987 | + | wasReparented bool // Track if this WebView has been reparented | |
| 953 | 988 | ||
| 954 | 989 | // Window type detection and relationships | |
| 955 | 990 | windowType WindowType | |
| @@ -1009,6 +1044,8 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1009 | 1044 | // WebView itself is created with a WebKitNetworkSession for persistence. | |
| 1010 | 1045 | ctx := C.webkit_web_context_get_default() | |
| 1011 | 1046 | ||
| 1047 | + | // Enable favicon database - we'll do this per WebView when created since we need NetworkSession | |
| 1048 | + | ||
| 1012 | 1049 | // Apply cache model to WebContext | |
| 1013 | 1050 | // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled | |
| 1014 | 1051 | cacheModel := 1 | |
| @@ -1156,6 +1193,13 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1156 | 1193 | C.connect_title_notify_with_id(viewWidget, C.ulong(v.id)) | |
| 1157 | 1194 | // Notify URI changes to Go to keep current URL in sync | |
| 1158 | 1195 | C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id)) | |
| 1196 | + | // Enable favicon database for this WebView's NetworkSession | |
| 1197 | + | networkSession := C.webkit_web_view_get_network_session(v.native.wv) | |
| 1198 | + | C.enable_favicon_database(networkSession) | |
| 1199 | + | ||
| 1200 | + | // Notify favicon changes to Go for caching | |
| 1201 | + | log.Printf("[favicon] Connecting favicon signal for WebView ID %d", v.id) | |
| 1202 | + | C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id)) | |
| 1159 | 1203 | // Apply hardware acceleration and related settings based on cfg.Rendering | |
| 1160 | 1204 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |
| 1161 | 1205 | // Hardware acceleration policy (guarded by version) | |
| @@ -1785,6 +1829,33 @@ func (w *WebView) dispatchURIChanged(uri string) { | |||
| 1785 | 1829 | } | |
| 1786 | 1830 | } | |
| 1787 | 1831 | ||
| 1832 | + | // RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes. | |
| 1833 | + | func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) { | |
| 1834 | + | if w != nil { | |
| 1835 | + | w.faviconHandler = cb | |
| 1836 | + | } | |
| 1837 | + | } | |
| 1838 | + | ||
| 1839 | + | // RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI. | |
| 1840 | + | func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) { | |
| 1841 | + | if w != nil { | |
| 1842 | + | w.faviconURIHandler = cb | |
| 1843 | + | } | |
| 1844 | + | } | |
| 1845 | + | ||
| 1846 | + | func (w *WebView) dispatchFaviconChanged(data []byte) { | |
| 1847 | + | if w != nil && w.faviconHandler != nil { | |
| 1848 | + | w.faviconHandler(data) | |
| 1849 | + | } | |
| 1850 | + | } | |
| 1851 | + | ||
| 1852 | + | func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) { | |
| 1853 | + | log.Printf("[favicon] Page %s has favicon at %s", pageURI, faviconURI) | |
| 1854 | + | if w != nil && w.faviconURIHandler != nil { | |
| 1855 | + | w.faviconURIHandler(pageURI, faviconURI) | |
| 1856 | + | } | |
| 1857 | + | } | |
| 1858 | + | ||
| 1788 | 1859 | // RunOnMainThread schedules fn to execute on the GTK main thread. | |
| 1789 | 1860 | func (w *WebView) RunOnMainThread(fn func()) { | |
| 1790 | 1861 | if w == nil || fn == nil { | |
| @@ -336,6 +336,28 @@ static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) { | |||||
| 336 | g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0); | 336 | g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0); | ||
| 337 | } | 337 | } | ||
| 338 | 338 | ||||
| 339 | + | static void connect_favicon_changed_with_id(GtkWidget* widget, unsigned long id) { | |||
| 340 | + | if (!widget) return; | |||
| 341 | + | WebKitWebView* web_view = WEBKIT_WEB_VIEW(widget); | |||
| 342 | + | WebKitNetworkSession* network_session = webkit_web_view_get_network_session(web_view); | |||
| 343 | + | if (network_session) { | |||
| 344 | + | WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(network_session); | |||
| 345 | + | if (data_manager) { | |||
| 346 | + | WebKitFaviconDatabase* favicon_db = webkit_website_data_manager_get_favicon_database(data_manager); | |||
| 347 | + | if (favicon_db) { | |||
| 348 | + | g_signal_connect_data(G_OBJECT(favicon_db), "favicon-changed", G_CALLBACK(on_favicon_changed), (gpointer)id, NULL, 0); | |||
| 349 | + | printf("[favicon-c] Connected favicon-changed signal for WebView ID %lu\n", id); | |||
| 350 | + | } else { | |||
| 351 | + | printf("[favicon-c] Failed to get favicon database\n"); | |||
| 352 | + | } | |||
| 353 | + | } else { | |||
| 354 | + | printf("[favicon-c] Failed to get website data manager for favicon signal\n"); | |||
| 355 | + | } | |||
| 356 | + | } else { | |||
| 357 | + | printf("[favicon-c] Failed to get network session\n"); | |||
| 358 | + | } | |||
| 359 | + | } | |||
| 360 | + | ||||
| 339 | static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) { | 361 | static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) { | ||
| 340 | if (!settings) return; | 362 | if (!settings) return; | ||
| 341 | g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0); | 363 | g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0); | ||
| @@ -358,6 +380,17 @@ static void register_uri_scheme_security(WebKitWebContext* context, const char* | |||||
| 358 | webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme); | 380 | webkit_security_manager_register_uri_scheme_as_cors_enabled(sm, scheme); | ||
| 359 | } | 381 | } | ||
| 360 | 382 | ||||
| 383 | + | static void enable_favicon_database(WebKitNetworkSession* session) { | |||
| 384 | + | if (!session) return; | |||
| 385 | + | WebKitWebsiteDataManager* data_manager = webkit_network_session_get_website_data_manager(session); | |||
| 386 | + | if (data_manager) { | |||
| 387 | + | webkit_website_data_manager_set_favicons_enabled(data_manager, TRUE); | |||
| 388 | + | printf("[favicon-c] Favicon database enabled\n"); | |||
| 389 | + | } else { | |||
| 390 | + | printf("[favicon-c] Failed to get website data manager\n"); | |||
| 391 | + | } | |||
| 392 | + | } | |||
| 393 | + | ||||
| 361 | // Script message handler wiring for WebKitGTK 6 | 394 | // Script message handler wiring for WebKitGTK 6 | ||
| 362 | // Forward declaration for callback used in g_signal_connect_data | 395 | // Forward declaration for callback used in g_signal_connect_data | ||
| 363 | // WebKitGTK 6 delivers JSCValue* for script-message-received::NAME | 396 | // WebKitGTK 6 delivers JSCValue* for script-message-received::NAME | ||
| @@ -929,27 +962,29 @@ type memoryStats struct { | |||||
| 929 | 962 | ||||
| 930 | // WebView represents a browser view powered by WebKit2GTK. | 963 | // WebView represents a browser view powered by WebKit2GTK. | ||
| 931 | type WebView struct { | 964 | type WebView struct { | ||
| 932 | - | config *Config | 965 | + | config *Config |
| 933 | - | zoom float64 | 966 | + | zoom float64 |
| 934 | - | url string | 967 | + | url string |
| 935 | - | destroyed bool | 968 | + | destroyed bool |
| 936 | - | native *nativeView | 969 | + | native *nativeView |
| 937 | - | window *Window | 970 | + | window *Window |
| 938 | - | id uintptr | 971 | + | id uintptr |
| 939 | - | msgHandler func(payload string) | 972 | + | msgHandler func(payload string) |
| 940 | - | titleHandler func(title string) | 973 | + | titleHandler func(title string) |
| 941 | - | uriHandler func(uri string) | 974 | + | uriHandler func(uri string) |
| 942 | - | zoomHandler func(level float64) | 975 | + | faviconHandler func(data []byte) |
| 943 | - | popupHandler func(string) *WebView | 976 | + | faviconURIHandler func(pageURI, faviconURI string) |
| 944 | - | closeHandler func() | 977 | + | zoomHandler func(level float64) |
| 945 | - | memStats *memoryStats | 978 | + | popupHandler func(string) *WebView |
| 946 | - | gcTicker *time.Ticker | 979 | + | closeHandler func() |
| 947 | - | gcDone chan struct{} | 980 | + | memStats *memoryStats |
| 948 | - | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | 981 | + | gcTicker *time.Ticker |
| 949 | - | useDomZoom bool | 982 | + | gcDone chan struct{} |
| 950 | - | domZoomSeed float64 | 983 | + | tlsExceptions map[string]bool // host -> whether user allowed certificate exception |
| 951 | - | domBridgeScript *C.WebKitUserScript | 984 | + | useDomZoom bool |
| 952 | - | wasReparented bool // Track if this WebView has been reparented | 985 | + | domZoomSeed float64 |
| 986 | + | domBridgeScript *C.WebKitUserScript | |||
| 987 | + | wasReparented bool // Track if this WebView has been reparented | |||
| 953 | 988 | ||||
| 954 | // Window type detection and relationships | 989 | // Window type detection and relationships | ||
| 955 | windowType WindowType | 990 | windowType WindowType | ||
| @@ -1009,6 +1044,8 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1009 | // WebView itself is created with a WebKitNetworkSession for persistence. | 1044 | // WebView itself is created with a WebKitNetworkSession for persistence. | ||
| 1010 | ctx := C.webkit_web_context_get_default() | 1045 | ctx := C.webkit_web_context_get_default() | ||
| 1011 | 1046 | ||||
| 1047 | + | // Enable favicon database - we'll do this per WebView when created since we need NetworkSession | |||
| 1048 | + | ||||
| 1012 | // Apply cache model to WebContext | 1049 | // Apply cache model to WebContext | ||
| 1013 | // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled | 1050 | // Default to WEBKIT_CACHE_MODEL_WEB_BROWSER to keep caching enabled | ||
| 1014 | cacheModel := 1 | 1051 | cacheModel := 1 | ||
| @@ -1156,6 +1193,13 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1156 | C.connect_title_notify_with_id(viewWidget, C.ulong(v.id)) | 1193 | C.connect_title_notify_with_id(viewWidget, C.ulong(v.id)) | ||
| 1157 | // Notify URI changes to Go to keep current URL in sync | 1194 | // Notify URI changes to Go to keep current URL in sync | ||
| 1158 | C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id)) | 1195 | C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id)) | ||
| 1196 | + | // Enable favicon database for this WebView's NetworkSession | |||
| 1197 | + | networkSession := C.webkit_web_view_get_network_session(v.native.wv) | |||
| 1198 | + | C.enable_favicon_database(networkSession) | |||
| 1199 | + | ||||
| 1200 | + | // Notify favicon changes to Go for caching | |||
| 1201 | + | log.Printf("[favicon] Connecting favicon signal for WebView ID %d", v.id) | |||
| 1202 | + | C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id)) | |||
| 1159 | // Apply hardware acceleration and related settings based on cfg.Rendering | 1203 | // Apply hardware acceleration and related settings based on cfg.Rendering | ||
| 1160 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | 1204 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | ||
| 1161 | // Hardware acceleration policy (guarded by version) | 1205 | // Hardware acceleration policy (guarded by version) | ||
| @@ -1785,6 +1829,33 @@ func (w *WebView) dispatchURIChanged(uri string) { | |||||
| 1785 | } | 1829 | } | ||
| 1786 | } | 1830 | } | ||
| 1787 | 1831 | ||||
| 1832 | + | // RegisterFaviconChangedHandler registers a callback invoked when the page favicon changes. | |||
| 1833 | + | func (w *WebView) RegisterFaviconChangedHandler(cb func(data []byte)) { | |||
| 1834 | + | if w != nil { | |||
| 1835 | + | w.faviconHandler = cb | |||
| 1836 | + | } | |||
| 1837 | + | } | |||
| 1838 | + | ||||
| 1839 | + | // RegisterFaviconURIChangedHandler registers a callback invoked when WebKit detects a favicon URI. | |||
| 1840 | + | func (w *WebView) RegisterFaviconURIChangedHandler(cb func(pageURI, faviconURI string)) { | |||
| 1841 | + | if w != nil { | |||
| 1842 | + | w.faviconURIHandler = cb | |||
| 1843 | + | } | |||
| 1844 | + | } | |||
| 1845 | + | ||||
| 1846 | + | func (w *WebView) dispatchFaviconChanged(data []byte) { | |||
| 1847 | + | if w != nil && w.faviconHandler != nil { | |||
| 1848 | + | w.faviconHandler(data) | |||
| 1849 | + | } | |||
| 1850 | + | } | |||
| 1851 | + | ||||
| 1852 | + | func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) { | |||
| 1853 | + | log.Printf("[favicon] Page %s has favicon at %s", pageURI, faviconURI) | |||
| 1854 | + | if w != nil && w.faviconURIHandler != nil { | |||
| 1855 | + | w.faviconURIHandler(pageURI, faviconURI) | |||
| 1856 | + | } | |||
| 1857 | + | } | |||
| 1858 | + | ||||
| 1788 | // RunOnMainThread schedules fn to execute on the GTK main thread. | 1859 | // RunOnMainThread schedules fn to execute on the GTK main thread. | ||
| 1789 | func (w *WebView) RunOnMainThread(fn func()) { | 1860 | func (w *WebView) RunOnMainThread(fn func()) { | ||
| 1790 | if w == nil || fn == nil { | 1861 | if w == nil || fn == nil { | ||