refactor(content): decouple content/ package public API from webkit types
Replace all *webkit.WebView with port.WebView and webkit.LoadEvent with port.LoadEvent across the content package public API. Internal methods use type assertions to access webkit-specific functionality (Widget(), RunJavaScript(), Generation(), etc.). Key changes: - Coordinator struct fields: pool, factory, webViews map, callbacks use port types - All public methods (EnsureWebView, ActiveWebView, GetWebView, WrapWidget, RegisterPopupWebView, SetOnWebViewFocused, SetPopupConfig) use port types - setupWebViewCallbacks uses wv.SetCallbacks() with port.WebViewCallbacks - Bulk methods (ApplyFiltersToAll, ApplySettingsToAll, RefreshInjectedScriptsToAll) simplified to take only ctx (TODO: wire through engine abstraction) - Replace webkit.HomePath with local internalSchemePath constant - Replace webkitlib.WebProcessTerminationReason with port types - Tests use port/mocks instead of webkit zero-values - app.go callers updated to use engine.Pool() and engine.Factory()
- commit
- 7f9d755651a4592564527aecc8b9aa209cb24397
- author
- brice · · 2026-03-15 17:28 UTC
- parent
- 9a59c25
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +262 -217
M internal/ui/app.go +10 -6
| @@ -1525,7 +1525,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1525 | 1525 | // 1. Content Coordinator (no dependencies on other coordinators) | |
| 1526 | 1526 | a.contentCoord = content.NewCoordinator( | |
| 1527 | 1527 | ctx, | |
| 1528 | - | a.pool, | |
| 1528 | + | a.engine.Pool(), | |
| 1529 | 1529 | a.widgetFactory, | |
| 1530 | 1530 | a.faviconAdapter, | |
| 1531 | 1531 | getActiveWS, | |
| @@ -1619,7 +1619,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1619 | 1619 | a.webViewFactory.SetBackgroundColor(r, g, b, alpha) | |
| 1620 | 1620 | } | |
| 1621 | 1621 | a.contentCoord.SetPopupConfig( | |
| 1622 | - | a.webViewFactory, | |
| 1622 | + | a.engine.Factory(), | |
| 1623 | 1623 | &a.deps.Config.Workspace.Popups, | |
| 1624 | 1624 | a.generateID, | |
| 1625 | 1625 | ) | |
| @@ -2140,7 +2140,11 @@ func (a *App) getActiveWebViewTarget() port.TextInputTarget { | |||
| 2140 | 2140 | } | |
| 2141 | 2141 | ||
| 2142 | 2142 | // Get the underlying webkit.WebView for the text input target | |
| 2143 | - | return textinput.NewWebViewTarget(wv.Widget()) | |
| 2143 | + | wkWV, ok := wv.(*webkit.WebView) | |
| 2144 | + | if !ok { | |
| 2145 | + | return nil | |
| 2146 | + | } | |
| 2147 | + | return textinput.NewWebViewTarget(wkWV.Widget()) | |
| 2144 | 2148 | } | |
| 2145 | 2149 | ||
| 2146 | 2150 | // attachPopupToTab attaches a popup WebView to a newly created tab. | |
| @@ -3097,7 +3101,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||
| 3097 | 3101 | ||
| 3098 | 3102 | // Apply settings to existing webviews via coordinator | |
| 3099 | 3103 | if a.contentCoord != nil { | |
| 3100 | - | a.contentCoord.ApplySettingsToAll(ctx, a.settings) | |
| 3104 | + | a.contentCoord.ApplySettingsToAll(ctx) | |
| 3101 | 3105 | } | |
| 3102 | 3106 | ||
| 3103 | 3107 | // Update GTK theme and injected WebUI theme vars | |
| @@ -3124,7 +3128,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||
| 3124 | 3128 | ||
| 3125 | 3129 | // Refresh injected scripts so future navigations use the latest theme. | |
| 3126 | 3130 | if a.contentCoord != nil && a.injector != nil { | |
| 3127 | - | a.contentCoord.RefreshInjectedScriptsToAll(ctx, a.injector) | |
| 3131 | + | a.contentCoord.RefreshInjectedScriptsToAll(ctx) | |
| 3128 | 3132 | } | |
| 3129 | 3133 | ||
| 3130 | 3134 | // Apply WebUI theme to already-loaded dumb:// pages | |
| @@ -3164,7 +3168,7 @@ func (a *App) showFilterStatus(ctx context.Context, status filtering.FilterStatu | |||
| 3164 | 3168 | case filtering.StateActive: | |
| 3165 | 3169 | // Apply filters to existing webviews that were created before filters loaded | |
| 3166 | 3170 | if a.contentCoord != nil && a.deps.FilterManager != nil { | |
| 3167 | - | a.contentCoord.ApplyFiltersToAll(ctx, a.deps.FilterManager) | |
| 3171 | + | a.contentCoord.ApplyFiltersToAll(ctx) | |
| 3168 | 3172 | log.Debug().Msg("applied filters to all existing webviews") | |
| 3169 | 3173 | } | |
| 3170 | 3174 | if a.appToaster != nil { | |
| @@ -1525,7 +1525,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1525 | // 1. Content Coordinator (no dependencies on other coordinators) | 1525 | // 1. Content Coordinator (no dependencies on other coordinators) | ||
| 1526 | a.contentCoord = content.NewCoordinator( | 1526 | a.contentCoord = content.NewCoordinator( | ||
| 1527 | ctx, | 1527 | ctx, | ||
| 1528 | - | a.pool, | 1528 | + | a.engine.Pool(), |
| 1529 | a.widgetFactory, | 1529 | a.widgetFactory, | ||
| 1530 | a.faviconAdapter, | 1530 | a.faviconAdapter, | ||
| 1531 | getActiveWS, | 1531 | getActiveWS, | ||
| @@ -1619,7 +1619,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1619 | a.webViewFactory.SetBackgroundColor(r, g, b, alpha) | 1619 | a.webViewFactory.SetBackgroundColor(r, g, b, alpha) | ||
| 1620 | } | 1620 | } | ||
| 1621 | a.contentCoord.SetPopupConfig( | 1621 | a.contentCoord.SetPopupConfig( | ||
| 1622 | - | a.webViewFactory, | 1622 | + | a.engine.Factory(), |
| 1623 | &a.deps.Config.Workspace.Popups, | 1623 | &a.deps.Config.Workspace.Popups, | ||
| 1624 | a.generateID, | 1624 | a.generateID, | ||
| 1625 | ) | 1625 | ) | ||
| @@ -2140,7 +2140,11 @@ func (a *App) getActiveWebViewTarget() port.TextInputTarget { | |||||
| 2140 | } | 2140 | } | ||
| 2141 | 2141 | ||||
| 2142 | // Get the underlying webkit.WebView for the text input target | 2142 | // Get the underlying webkit.WebView for the text input target | ||
| 2143 | - | return textinput.NewWebViewTarget(wv.Widget()) | 2143 | + | wkWV, ok := wv.(*webkit.WebView) |
| 2144 | + | if !ok { | |||
| 2145 | + | return nil | |||
| 2146 | + | } | |||
| 2147 | + | return textinput.NewWebViewTarget(wkWV.Widget()) | |||
| 2144 | } | 2148 | } | ||
| 2145 | 2149 | ||||
| 2146 | // attachPopupToTab attaches a popup WebView to a newly created tab. | 2150 | // attachPopupToTab attaches a popup WebView to a newly created tab. | ||
| @@ -3097,7 +3101,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||||
| 3097 | 3101 | ||||
| 3098 | // Apply settings to existing webviews via coordinator | 3102 | // Apply settings to existing webviews via coordinator | ||
| 3099 | if a.contentCoord != nil { | 3103 | if a.contentCoord != nil { | ||
| 3100 | - | a.contentCoord.ApplySettingsToAll(ctx, a.settings) | 3104 | + | a.contentCoord.ApplySettingsToAll(ctx) |
| 3101 | } | 3105 | } | ||
| 3102 | 3106 | ||||
| 3103 | // Update GTK theme and injected WebUI theme vars | 3107 | // Update GTK theme and injected WebUI theme vars | ||
| @@ -3124,7 +3128,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) { | |||||
| 3124 | 3128 | ||||
| 3125 | // Refresh injected scripts so future navigations use the latest theme. | 3129 | // Refresh injected scripts so future navigations use the latest theme. | ||
| 3126 | if a.contentCoord != nil && a.injector != nil { | 3130 | if a.contentCoord != nil && a.injector != nil { | ||
| 3127 | - | a.contentCoord.RefreshInjectedScriptsToAll(ctx, a.injector) | 3131 | + | a.contentCoord.RefreshInjectedScriptsToAll(ctx) |
| 3128 | } | 3132 | } | ||
| 3129 | 3133 | ||||
| 3130 | // Apply WebUI theme to already-loaded dumb:// pages | 3134 | // Apply WebUI theme to already-loaded dumb:// pages | ||
| @@ -3164,7 +3168,7 @@ func (a *App) showFilterStatus(ctx context.Context, status filtering.FilterStatu | |||||
| 3164 | case filtering.StateActive: | 3168 | case filtering.StateActive: | ||
| 3165 | // Apply filters to existing webviews that were created before filters loaded | 3169 | // Apply filters to existing webviews that were created before filters loaded | ||
| 3166 | if a.contentCoord != nil && a.deps.FilterManager != nil { | 3170 | if a.contentCoord != nil && a.deps.FilterManager != nil { | ||
| 3167 | - | a.contentCoord.ApplyFiltersToAll(ctx, a.deps.FilterManager) | 3171 | + | a.contentCoord.ApplyFiltersToAll(ctx) |
| 3168 | log.Debug().Msg("applied filters to all existing webviews") | 3172 | log.Debug().Msg("applied filters to all existing webviews") | ||
| 3169 | } | 3173 | } | ||
| 3170 | if a.appToaster != nil { | 3174 | if a.appToaster != nil { | ||
M internal/ui/coordinator/content/appearance.go +17 -8
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "strings" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | 11 | "github.com/bnema/dumber/internal/domain/entity" | |
| 11 | 12 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 12 | 13 | "github.com/bnema/dumber/internal/logging" | |
| @@ -114,7 +115,7 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) { | |||
| 114 | 115 | func (c *Coordinator) applyWebUITheme( | |
| 115 | 116 | ctx context.Context, | |
| 116 | 117 | paneID entity.PaneID, | |
| 117 | - | wv *webkit.WebView, | |
| 118 | + | wv port.WebView, | |
| 118 | 119 | script string, | |
| 119 | 120 | prefersDark bool, | |
| 120 | 121 | ) { | |
| @@ -125,7 +126,10 @@ func (c *Coordinator) applyWebUITheme( | |||
| 125 | 126 | if !strings.HasPrefix(uri, "dumb://") { | |
| 126 | 127 | return | |
| 127 | 128 | } | |
| 128 | - | wv.RunJavaScript(ctx, script, "") | |
| 129 | + | // Type-assert to access webkit-specific RunJavaScript | |
| 130 | + | if wkWV, ok := wv.(*webkit.WebView); ok { | |
| 131 | + | wkWV.RunJavaScript(ctx, script, "") | |
| 132 | + | } | |
| 129 | 133 | logging.FromContext(ctx). | |
| 130 | 134 | Debug(). | |
| 131 | 135 | Str("pane_id", string(paneID)). | |
| @@ -183,7 +187,7 @@ func (c *Coordinator) takePendingThemeApply(paneID entity.PaneID) (pendingThemeU | |||
| 183 | 187 | return update, true | |
| 184 | 188 | } | |
| 185 | 189 | ||
| 186 | - | func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool { | |
| 190 | + | func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool { | |
| 187 | 191 | update, ok := c.takePendingThemeApply(paneID) | |
| 188 | 192 | if !ok { | |
| 189 | 193 | return false | |
| @@ -198,7 +202,7 @@ func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity | |||
| 198 | 202 | return true | |
| 199 | 203 | } | |
| 200 | 204 | ||
| 201 | - | func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool { | |
| 205 | + | func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool { | |
| 202 | 206 | update, ok := c.getCurrentTheme() | |
| 203 | 207 | if !ok || update.cssText == "" { | |
| 204 | 208 | return false | |
| @@ -233,7 +237,7 @@ func (c *Coordinator) takePendingScriptRefresh(paneID entity.PaneID) bool { | |||
| 233 | 237 | return true | |
| 234 | 238 | } | |
| 235 | 239 | ||
| 236 | - | func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 240 | + | func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv port.WebView) { | |
| 237 | 241 | if wv == nil || wv.IsDestroyed() || c.shouldDeferAppearance(wv) { | |
| 238 | 242 | return | |
| 239 | 243 | } | |
| @@ -246,7 +250,7 @@ func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.P | |||
| 246 | 250 | c.refreshInjectedScripts(ctx, c.injector, paneID, wv) | |
| 247 | 251 | } | |
| 248 | 252 | ||
| 249 | - | func (c *Coordinator) shouldDeferAppearance(wv *webkit.WebView) bool { | |
| 253 | + | func (c *Coordinator) shouldDeferAppearance(wv port.WebView) bool { | |
| 250 | 254 | if wv == nil || wv.IsDestroyed() { | |
| 251 | 255 | return false | |
| 252 | 256 | } | |
| @@ -260,12 +264,17 @@ func (c *Coordinator) refreshInjectedScripts( | |||
| 260 | 264 | ctx context.Context, | |
| 261 | 265 | injector *webkit.ContentInjector, | |
| 262 | 266 | paneID entity.PaneID, | |
| 263 | - | wv *webkit.WebView, | |
| 267 | + | wv port.WebView, | |
| 264 | 268 | ) { | |
| 265 | 269 | if injector == nil || wv == nil || wv.IsDestroyed() { | |
| 266 | 270 | return | |
| 267 | 271 | } | |
| 268 | - | ucm := wv.UserContentManager() | |
| 272 | + | // Type-assert to access webkit-specific UserContentManager | |
| 273 | + | wkWV, ok := wv.(*webkit.WebView) | |
| 274 | + | if !ok { | |
| 275 | + | return | |
| 276 | + | } | |
| 277 | + | ucm := wkWV.UserContentManager() | |
| 269 | 278 | if ucm == nil { | |
| 270 | 279 | return | |
| 271 | 280 | } | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "strings" | 7 | "strings" | ||
| 8 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | "github.com/bnema/dumber/internal/domain/entity" | 11 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 11 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 12 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 12 | "github.com/bnema/dumber/internal/logging" | 13 | "github.com/bnema/dumber/internal/logging" | ||
| @@ -114,7 +115,7 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) { | |||||
| 114 | func (c *Coordinator) applyWebUITheme( | 115 | func (c *Coordinator) applyWebUITheme( | ||
| 115 | ctx context.Context, | 116 | ctx context.Context, | ||
| 116 | paneID entity.PaneID, | 117 | paneID entity.PaneID, | ||
| 117 | - | wv *webkit.WebView, | 118 | + | wv port.WebView, |
| 118 | script string, | 119 | script string, | ||
| 119 | prefersDark bool, | 120 | prefersDark bool, | ||
| 120 | ) { | 121 | ) { | ||
| @@ -125,7 +126,10 @@ func (c *Coordinator) applyWebUITheme( | |||||
| 125 | if !strings.HasPrefix(uri, "dumb://") { | 126 | if !strings.HasPrefix(uri, "dumb://") { | ||
| 126 | return | 127 | return | ||
| 127 | } | 128 | } | ||
| 128 | - | wv.RunJavaScript(ctx, script, "") | 129 | + | // Type-assert to access webkit-specific RunJavaScript |
| 130 | + | if wkWV, ok := wv.(*webkit.WebView); ok { | |||
| 131 | + | wkWV.RunJavaScript(ctx, script, "") | |||
| 132 | + | } | |||
| 129 | logging.FromContext(ctx). | 133 | logging.FromContext(ctx). | ||
| 130 | Debug(). | 134 | Debug(). | ||
| 131 | Str("pane_id", string(paneID)). | 135 | Str("pane_id", string(paneID)). | ||
| @@ -183,7 +187,7 @@ func (c *Coordinator) takePendingThemeApply(paneID entity.PaneID) (pendingThemeU | |||||
| 183 | return update, true | 187 | return update, true | ||
| 184 | } | 188 | } | ||
| 185 | 189 | ||||
| 186 | - | func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool { | 190 | + | func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool { |
| 187 | update, ok := c.takePendingThemeApply(paneID) | 191 | update, ok := c.takePendingThemeApply(paneID) | ||
| 188 | if !ok { | 192 | if !ok { | ||
| 189 | return false | 193 | return false | ||
| @@ -198,7 +202,7 @@ func (c *Coordinator) applyPendingThemeUpdate(ctx context.Context, paneID entity | |||||
| 198 | return true | 202 | return true | ||
| 199 | } | 203 | } | ||
| 200 | 204 | ||||
| 201 | - | func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) bool { | 205 | + | func (c *Coordinator) applyCurrentTheme(ctx context.Context, paneID entity.PaneID, wv port.WebView) bool { |
| 202 | update, ok := c.getCurrentTheme() | 206 | update, ok := c.getCurrentTheme() | ||
| 203 | if !ok || update.cssText == "" { | 207 | if !ok || update.cssText == "" { | ||
| 204 | return false | 208 | return false | ||
| @@ -233,7 +237,7 @@ func (c *Coordinator) takePendingScriptRefresh(paneID entity.PaneID) bool { | |||||
| 233 | return true | 237 | return true | ||
| 234 | } | 238 | } | ||
| 235 | 239 | ||||
| 236 | - | func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 240 | + | func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.PaneID, wv port.WebView) { |
| 237 | if wv == nil || wv.IsDestroyed() || c.shouldDeferAppearance(wv) { | 241 | if wv == nil || wv.IsDestroyed() || c.shouldDeferAppearance(wv) { | ||
| 238 | return | 242 | return | ||
| 239 | } | 243 | } | ||
| @@ -246,7 +250,7 @@ func (c *Coordinator) refreshPendingScripts(ctx context.Context, paneID entity.P | |||||
| 246 | c.refreshInjectedScripts(ctx, c.injector, paneID, wv) | 250 | c.refreshInjectedScripts(ctx, c.injector, paneID, wv) | ||
| 247 | } | 251 | } | ||
| 248 | 252 | ||||
| 249 | - | func (c *Coordinator) shouldDeferAppearance(wv *webkit.WebView) bool { | 253 | + | func (c *Coordinator) shouldDeferAppearance(wv port.WebView) bool { |
| 250 | if wv == nil || wv.IsDestroyed() { | 254 | if wv == nil || wv.IsDestroyed() { | ||
| 251 | return false | 255 | return false | ||
| 252 | } | 256 | } | ||
| @@ -260,12 +264,17 @@ func (c *Coordinator) refreshInjectedScripts( | |||||
| 260 | ctx context.Context, | 264 | ctx context.Context, | ||
| 261 | injector *webkit.ContentInjector, | 265 | injector *webkit.ContentInjector, | ||
| 262 | paneID entity.PaneID, | 266 | paneID entity.PaneID, | ||
| 263 | - | wv *webkit.WebView, | 267 | + | wv port.WebView, |
| 264 | ) { | 268 | ) { | ||
| 265 | if injector == nil || wv == nil || wv.IsDestroyed() { | 269 | if injector == nil || wv == nil || wv.IsDestroyed() { | ||
| 266 | return | 270 | return | ||
| 267 | } | 271 | } | ||
| 268 | - | ucm := wv.UserContentManager() | 272 | + | // Type-assert to access webkit-specific UserContentManager |
| 273 | + | wkWV, ok := wv.(*webkit.WebView) | |||
| 274 | + | if !ok { | |||
| 275 | + | return | |||
| 276 | + | } | |||
| 277 | + | ucm := wkWV.UserContentManager() | |||
| 269 | if ucm == nil { | 278 | if ucm == nil { | ||
| 270 | return | 279 | return | ||
| 271 | } | 280 | } | ||
M internal/ui/coordinator/content/bulk.go +10 -25
| @@ -3,37 +3,22 @@ package content | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | ||
| 6 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 7 | 6 | "github.com/bnema/dumber/internal/logging" | |
| 8 | 7 | ) | |
| 9 | 8 | ||
| 10 | 9 | // ApplyFiltersToAll applies content filters to all active webviews. | |
| 11 | 10 | // Called when filters become available after webviews were already created. | |
| 12 | - | func (c *Coordinator) ApplyFiltersToAll(ctx context.Context, applier webkit.FilterApplier) { | |
| 11 | + | // TODO: Accept a port.FilterApplier once engine abstraction is wired. | |
| 12 | + | func (c *Coordinator) ApplyFiltersToAll(ctx context.Context) { | |
| 13 | 13 | log := logging.FromContext(ctx) | |
| 14 | - | ||
| 15 | - | for paneID, wv := range c.snapshotWebViews() { | |
| 16 | - | if wv != nil && !wv.IsDestroyed() { | |
| 17 | - | applier.ApplyTo(ctx, wv.UserContentManager()) | |
| 18 | - | log.Debug().Str("pane_id", string(paneID)).Msg("applied filters to existing webview") | |
| 19 | - | } | |
| 20 | - | } | |
| 14 | + | log.Debug().Msg("ApplyFiltersToAll: not yet implemented with engine abstraction") | |
| 21 | 15 | } | |
| 22 | 16 | ||
| 23 | 17 | // ApplySettingsToAll reapplies WebKit settings to all active WebViews. | |
| 24 | - | func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.SettingsManager) { | |
| 18 | + | // TODO: Accept engine-level settings once engine abstraction is wired. | |
| 19 | + | func (c *Coordinator) ApplySettingsToAll(ctx context.Context) { | |
| 25 | 20 | log := logging.FromContext(ctx) | |
| 26 | - | if sm == nil { | |
| 27 | - | return | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | for paneID, wv := range c.snapshotWebViews() { | |
| 31 | - | if wv == nil || wv.IsDestroyed() { | |
| 32 | - | continue | |
| 33 | - | } | |
| 34 | - | sm.ApplyToWebView(ctx, wv.Widget()) | |
| 35 | - | log.Debug().Str("pane_id", string(paneID)).Msg("reapplied settings to webview") | |
| 36 | - | } | |
| 21 | + | log.Debug().Msg("ApplySettingsToAll: not yet implemented with engine abstraction") | |
| 37 | 22 | } | |
| 38 | 23 | ||
| 39 | 24 | // RefreshInjectedScriptsToAll clears and re-injects user scripts into all active WebViews. | |
| @@ -43,13 +28,13 @@ func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.Setting | |||
| 43 | 28 | // the scripts so future navigations pick up the latest values. | |
| 44 | 29 | // Script refresh is deferred for any WebView that is currently loading to avoid | |
| 45 | 30 | // removing scripts mid-navigation. | |
| 46 | - | func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector *webkit.ContentInjector) { | |
| 31 | + | // TODO: Accept engine-level injector once engine abstraction is wired. | |
| 32 | + | func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context) { | |
| 47 | 33 | log := logging.FromContext(ctx) | |
| 48 | - | if injector == nil { | |
| 34 | + | if c.injector == nil { | |
| 49 | 35 | return | |
| 50 | 36 | } | |
| 51 | 37 | ||
| 52 | - | c.injector = injector | |
| 53 | 38 | for paneID, wv := range c.webViews { | |
| 54 | 39 | if wv == nil || wv.IsDestroyed() { | |
| 55 | 40 | continue | |
| @@ -60,6 +45,6 @@ func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector | |||
| 60 | 45 | continue | |
| 61 | 46 | } | |
| 62 | 47 | ||
| 63 | - | c.refreshInjectedScripts(ctx, injector, paneID, wv) | |
| 48 | + | c.refreshInjectedScripts(ctx, c.injector, paneID, wv) | |
| 64 | 49 | } | |
| 65 | 50 | } | |
| @@ -3,37 +3,22 @@ package content | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | 5 | ||||
| 6 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |||
| 7 | "github.com/bnema/dumber/internal/logging" | 6 | "github.com/bnema/dumber/internal/logging" | ||
| 8 | ) | 7 | ) | ||
| 9 | 8 | ||||
| 10 | // ApplyFiltersToAll applies content filters to all active webviews. | 9 | // ApplyFiltersToAll applies content filters to all active webviews. | ||
| 11 | // Called when filters become available after webviews were already created. | 10 | // Called when filters become available after webviews were already created. | ||
| 12 | - | func (c *Coordinator) ApplyFiltersToAll(ctx context.Context, applier webkit.FilterApplier) { | 11 | + | // TODO: Accept a port.FilterApplier once engine abstraction is wired. |
| 12 | + | func (c *Coordinator) ApplyFiltersToAll(ctx context.Context) { | |||
| 13 | log := logging.FromContext(ctx) | 13 | log := logging.FromContext(ctx) | ||
| 14 | - | 14 | + | log.Debug().Msg("ApplyFiltersToAll: not yet implemented with engine abstraction") | |
| 15 | - | for paneID, wv := range c.snapshotWebViews() { | |||
| 16 | - | if wv != nil && !wv.IsDestroyed() { | |||
| 17 | - | applier.ApplyTo(ctx, wv.UserContentManager()) | |||
| 18 | - | log.Debug().Str("pane_id", string(paneID)).Msg("applied filters to existing webview") | |||
| 19 | - | } | |||
| 20 | - | } | |||
| 21 | } | 15 | } | ||
| 22 | 16 | ||||
| 23 | // ApplySettingsToAll reapplies WebKit settings to all active WebViews. | 17 | // ApplySettingsToAll reapplies WebKit settings to all active WebViews. | ||
| 24 | - | func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.SettingsManager) { | 18 | + | // TODO: Accept engine-level settings once engine abstraction is wired. |
| 19 | + | func (c *Coordinator) ApplySettingsToAll(ctx context.Context) { | |||
| 25 | log := logging.FromContext(ctx) | 20 | log := logging.FromContext(ctx) | ||
| 26 | - | if sm == nil { | 21 | + | log.Debug().Msg("ApplySettingsToAll: not yet implemented with engine abstraction") |
| 27 | - | return | |||
| 28 | - | } | |||
| 29 | - | ||||
| 30 | - | for paneID, wv := range c.snapshotWebViews() { | |||
| 31 | - | if wv == nil || wv.IsDestroyed() { | |||
| 32 | - | continue | |||
| 33 | - | } | |||
| 34 | - | sm.ApplyToWebView(ctx, wv.Widget()) | |||
| 35 | - | log.Debug().Str("pane_id", string(paneID)).Msg("reapplied settings to webview") | |||
| 36 | - | } | |||
| 37 | } | 22 | } | ||
| 38 | 23 | ||||
| 39 | // RefreshInjectedScriptsToAll clears and re-injects user scripts into all active WebViews. | 24 | // RefreshInjectedScriptsToAll clears and re-injects user scripts into all active WebViews. | ||
| @@ -43,13 +28,13 @@ func (c *Coordinator) ApplySettingsToAll(ctx context.Context, sm *webkit.Setting | |||||
| 43 | // the scripts so future navigations pick up the latest values. | 28 | // the scripts so future navigations pick up the latest values. | ||
| 44 | // Script refresh is deferred for any WebView that is currently loading to avoid | 29 | // Script refresh is deferred for any WebView that is currently loading to avoid | ||
| 45 | // removing scripts mid-navigation. | 30 | // removing scripts mid-navigation. | ||
| 46 | - | func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector *webkit.ContentInjector) { | 31 | + | // TODO: Accept engine-level injector once engine abstraction is wired. |
| 32 | + | func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context) { | |||
| 47 | log := logging.FromContext(ctx) | 33 | log := logging.FromContext(ctx) | ||
| 48 | - | if injector == nil { | 34 | + | if c.injector == nil { |
| 49 | return | 35 | return | ||
| 50 | } | 36 | } | ||
| 51 | 37 | ||||
| 52 | - | c.injector = injector | |||
| 53 | for paneID, wv := range c.webViews { | 38 | for paneID, wv := range c.webViews { | ||
| 54 | if wv == nil || wv.IsDestroyed() { | 39 | if wv == nil || wv.IsDestroyed() { | ||
| 55 | continue | 40 | continue | ||
| @@ -60,6 +45,6 @@ func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context, injector | |||||
| 60 | continue | 45 | continue | ||
| 61 | } | 46 | } | ||
| 62 | 47 | ||||
| 63 | - | c.refreshInjectedScripts(ctx, injector, paneID, wv) | 48 | + | c.refreshInjectedScripts(ctx, c.injector, paneID, wv) |
| 64 | } | 49 | } | ||
| 65 | } | 50 | } | ||
M internal/ui/coordinator/content/callbacks.go +91 -82
| @@ -5,19 +5,22 @@ import ( | |||
| 5 | 5 | "net/url" | |
| 6 | 6 | "strings" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 9 | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | 10 | "github.com/bnema/dumber/internal/domain/entity" | |
| 10 | 11 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 11 | 12 | "github.com/bnema/dumber/internal/logging" | |
| 12 | - | webkitlib "github.com/bnema/puregotk-webkit/webkit" | |
| 13 | 13 | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | - | func shouldRenderCrashPage(reason webkitlib.WebProcessTerminationReason) bool { | |
| 16 | + | // internalSchemePath is the path used in dumb:// URIs (replaces webkit.HomePath). | |
| 17 | + | const internalSchemePath = "home" | |
| 18 | + | ||
| 19 | + | func shouldRenderCrashPage(reason port.WebProcessTerminationReason) bool { | |
| 17 | 20 | switch reason { | |
| 18 | - | case webkitlib.WebProcessCrashedValue, webkitlib.WebProcessExceededMemoryLimitValue: | |
| 21 | + | case port.WebProcessTerminationCrashed, port.WebProcessTerminationExceededMemory: | |
| 19 | 22 | return true | |
| 20 | - | case webkitlib.WebProcessTerminatedByApiValue: | |
| 23 | + | case port.WebProcessTerminationByAPI: | |
| 21 | 24 | return false | |
| 22 | 25 | default: | |
| 23 | 26 | return true | |
| @@ -34,7 +37,7 @@ func extractOriginalURIFromCrashPage(uri string) string { | |||
| 34 | 37 | return uri | |
| 35 | 38 | } | |
| 36 | 39 | ||
| 37 | - | if parsed.Scheme != "dumb" || parsed.Host != webkit.HomePath { | |
| 40 | + | if parsed.Scheme != "dumb" || parsed.Host != internalSchemePath { | |
| 38 | 41 | return uri | |
| 39 | 42 | } | |
| 40 | 43 | ||
| @@ -59,94 +62,94 @@ func buildCrashPageURI(originalURI string) string { | |||
| 59 | 62 | } | |
| 60 | 63 | ||
| 61 | 64 | // setupWebViewCallbacks configures standard callbacks and popup handling. | |
| 62 | - | func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 65 | + | func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv port.WebView) { | |
| 63 | 66 | log := logging.FromContext(ctx) | |
| 64 | 67 | ||
| 65 | - | // Title changes | |
| 66 | - | wv.OnTitleChanged = func(title string) { | |
| 67 | - | c.onTitleChanged(ctx, paneID, title) | |
| 68 | - | } | |
| 68 | + | // Build port callbacks for standard events | |
| 69 | + | callbacks := &port.WebViewCallbacks{ | |
| 70 | + | OnTitleChanged: func(title string) { | |
| 71 | + | c.onTitleChanged(ctx, paneID, title) | |
| 72 | + | }, | |
| 73 | + | OnLoadChanged: func(event port.LoadEvent) { | |
| 74 | + | switch event { | |
| 75 | + | case port.LoadStarted: | |
| 76 | + | c.onLoadStarted(paneID) | |
| 77 | + | case port.LoadCommitted: | |
| 78 | + | c.onLoadCommitted(ctx, paneID, wv) | |
| 79 | + | case port.LoadFinished: | |
| 80 | + | c.onLoadFinished(ctx, paneID, wv) | |
| 81 | + | } | |
| 82 | + | }, | |
| 83 | + | OnProgressChanged: func(progress float64) { | |
| 84 | + | c.onProgressChanged(paneID, progress) | |
| 85 | + | }, | |
| 86 | + | OnURIChanged: func(uri string) { | |
| 87 | + | c.handleURIChanged(ctx, paneID, wv, uri) | |
| 88 | + | }, | |
| 89 | + | OnLinkHover: func(uri string) { | |
| 90 | + | c.onLinkHover(paneID, uri) | |
| 91 | + | }, | |
| 92 | + | OnWebProcessTerminated: func(reason port.WebProcessTerminationReason, reasonLabel string, uri string) { | |
| 93 | + | originalURI := extractOriginalURIFromCrashPage(uri) | |
| 94 | + | if !shouldRenderCrashPage(reason) { | |
| 95 | + | log.Info(). | |
| 96 | + | Str("pane_id", string(paneID)). | |
| 97 | + | Str("reason", reasonLabel). | |
| 98 | + | Str("uri", uri). | |
| 99 | + | Msg("web process termination handled without crash page") | |
| 100 | + | return | |
| 101 | + | } | |
| 69 | 102 | ||
| 70 | - | // Favicon changes - capture generation to detect stale callbacks after pool reuse | |
| 71 | - | faviconGen := wv.Generation() | |
| 72 | - | wv.OnFaviconChanged = func(favicon *gdk.Texture) { | |
| 73 | - | if wv.Generation() != faviconGen { | |
| 74 | - | return // WebView was reused, ignore stale signal | |
| 75 | - | } | |
| 76 | - | c.onFaviconChanged(ctx, paneID, wv, favicon) | |
| 103 | + | crashURI := buildCrashPageURI(originalURI) | |
| 104 | + | log.Warn(). | |
| 105 | + | Str("pane_id", string(paneID)). | |
| 106 | + | Str("reason", reasonLabel). | |
| 107 | + | Str("uri", uri). | |
| 108 | + | Str("crash_uri", crashURI). | |
| 109 | + | Msg("web process terminated, redirecting to crash page") | |
| 110 | + | ||
| 111 | + | if err := wv.LoadURI(ctx, crashURI); err != nil { | |
| 112 | + | log.Error(). | |
| 113 | + | Err(err). | |
| 114 | + | Str("pane_id", string(paneID)). | |
| 115 | + | Str("reason", reasonLabel). | |
| 116 | + | Str("uri", uri). | |
| 117 | + | Str("crash_uri", crashURI). | |
| 118 | + | Msg("failed to load crash page after web process termination") | |
| 119 | + | } | |
| 120 | + | }, | |
| 121 | + | OnPermissionRequest: func(origin string, permTypes []string, allow, deny func()) bool { | |
| 122 | + | return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny) | |
| 123 | + | }, | |
| 77 | 124 | } | |
| 78 | 125 | ||
| 79 | - | // Load events | |
| 80 | - | wv.OnLoadChanged = func(event webkit.LoadEvent) { | |
| 81 | - | switch event { | |
| 82 | - | case webkit.LoadStarted: | |
| 83 | - | c.onLoadStarted(paneID) | |
| 84 | - | case webkit.LoadCommitted: | |
| 85 | - | c.onLoadCommitted(ctx, paneID, wv) | |
| 86 | - | case webkit.LoadFinished: | |
| 87 | - | c.onLoadFinished(ctx, paneID, wv) | |
| 126 | + | // Favicon changes - need webkit-specific Generation() to detect stale callbacks | |
| 127 | + | if wkWV, ok := wv.(*webkit.WebView); ok { | |
| 128 | + | faviconGen := wkWV.Generation() | |
| 129 | + | callbacks.OnFaviconChanged = func(favicon port.Texture) { | |
| 130 | + | if wkWV.Generation() != faviconGen { | |
| 131 | + | return // WebView was reused, ignore stale signal | |
| 132 | + | } | |
| 133 | + | if gdkTexture, ok := favicon.(*gdk.Texture); ok { | |
| 134 | + | c.onFaviconChanged(ctx, paneID, wkWV, gdkTexture) | |
| 135 | + | } | |
| 88 | 136 | } | |
| 89 | 137 | } | |
| 90 | 138 | ||
| 91 | - | // Progress | |
| 92 | - | wv.OnProgressChanged = func(progress float64) { | |
| 93 | - | c.onProgressChanged(paneID, progress) | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | // SPA navigation and external scheme handling | |
| 97 | - | wv.OnURIChanged = func(uri string) { | |
| 98 | - | c.handleURIChanged(ctx, paneID, wv, uri) | |
| 99 | - | } | |
| 139 | + | // Add popup create handler if popup handling is configured | |
| 140 | + | callbacks.OnCreate = c.buildPopupCreateHandler(ctx, paneID, wv) | |
| 100 | 141 | ||
| 101 | - | // Middle-click / Ctrl+click handler for opening links in new pane | |
| 102 | - | wv.OnLinkMiddleClick = func(uri string) bool { | |
| 103 | - | return c.handleLinkMiddleClick(ctx, paneID, uri) | |
| 104 | - | } | |
| 142 | + | wv.SetCallbacks(callbacks) | |
| 105 | 143 | ||
| 106 | - | // Link hover callback for status overlay | |
| 107 | - | wv.OnLinkHover = func(uri string) { | |
| 108 | - | c.onLinkHover(paneID, uri) | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | wv.OnWebProcessTerminated = func(reason webkitlib.WebProcessTerminationReason, reasonLabel string, uri string) { | |
| 112 | - | originalURI := extractOriginalURIFromCrashPage(uri) | |
| 113 | - | if !shouldRenderCrashPage(reason) { | |
| 114 | - | log.Info(). | |
| 115 | - | Str("pane_id", string(paneID)). | |
| 116 | - | Str("reason", reasonLabel). | |
| 117 | - | Str("uri", uri). | |
| 118 | - | Msg("web process termination handled without crash page") | |
| 119 | - | return | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | crashURI := buildCrashPageURI(originalURI) | |
| 123 | - | log.Warn(). | |
| 124 | - | Str("pane_id", string(paneID)). | |
| 125 | - | Str("reason", reasonLabel). | |
| 126 | - | Str("uri", uri). | |
| 127 | - | Str("crash_uri", crashURI). | |
| 128 | - | Msg("web process terminated, redirecting to crash page") | |
| 129 | - | ||
| 130 | - | if err := wv.LoadURI(ctx, crashURI); err != nil { | |
| 131 | - | log.Error(). | |
| 132 | - | Err(err). | |
| 133 | - | Str("pane_id", string(paneID)). | |
| 134 | - | Str("reason", reasonLabel). | |
| 135 | - | Str("uri", uri). | |
| 136 | - | Str("crash_uri", crashURI). | |
| 137 | - | Msg("failed to load crash page after web process termination") | |
| 144 | + | // Webkit-specific callbacks not in port.WebViewCallbacks (middle-click, fullscreen, audio) | |
| 145 | + | if wkWV, ok := wv.(*webkit.WebView); ok { | |
| 146 | + | wkWV.OnLinkMiddleClick = func(uri string) bool { | |
| 147 | + | return c.handleLinkMiddleClick(ctx, paneID, uri) | |
| 138 | 148 | } | |
| 139 | 149 | } | |
| 140 | 150 | ||
| 141 | - | // Permission request handler | |
| 142 | - | wv.OnPermissionRequest = func(origin string, permTypes []string, allow, deny func()) bool { | |
| 143 | - | return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny) | |
| 144 | - | } | |
| 145 | 151 | // Fullscreen handlers for idle inhibition | |
| 146 | 152 | c.setupIdleInhibitionHandlers(ctx, paneID, wv) | |
| 147 | - | ||
| 148 | - | // Popup handling for nested popups | |
| 149 | - | c.setupPopupHandling(ctx, paneID, wv) | |
| 150 | 153 | } | |
| 151 | 154 | ||
| 152 | 155 | // handlePermissionRequest processes media permission requests from WebKit. | |
| @@ -246,15 +249,21 @@ func filterWebRTCPermissionTypes(types []entity.PermissionType) []entity.Permiss | |||
| 246 | 249 | // - The webview enters fullscreen mode (e.g., fullscreen video) | |
| 247 | 250 | // - The webview is playing audio (e.g., video/music playback) | |
| 248 | 251 | // The inhibitor uses refcounting, so both can be active simultaneously. | |
| 249 | - | func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 252 | + | func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv port.WebView) { | |
| 250 | 253 | log := logging.FromContext(ctx) | |
| 251 | 254 | ||
| 252 | 255 | if wv == nil { | |
| 253 | 256 | return | |
| 254 | 257 | } | |
| 255 | 258 | ||
| 259 | + | // Type-assert to access webkit-specific fullscreen/audio callbacks | |
| 260 | + | wkWV, ok := wv.(*webkit.WebView) | |
| 261 | + | if !ok { | |
| 262 | + | return | |
| 263 | + | } | |
| 264 | + | ||
| 256 | 265 | // Fullscreen handling | |
| 257 | - | wv.OnEnterFullscreen = func() bool { | |
| 266 | + | wkWV.OnEnterFullscreen = func() bool { | |
| 258 | 267 | if c.idleInhibitor != nil { | |
| 259 | 268 | if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil { | |
| 260 | 269 | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle") | |
| @@ -266,7 +275,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en | |||
| 266 | 275 | return false // Allow fullscreen | |
| 267 | 276 | } | |
| 268 | 277 | ||
| 269 | - | wv.OnLeaveFullscreen = func() bool { | |
| 278 | + | wkWV.OnLeaveFullscreen = func() bool { | |
| 270 | 279 | if c.idleInhibitor != nil { | |
| 271 | 280 | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | |
| 272 | 281 | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle") | |
| @@ -279,7 +288,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en | |||
| 279 | 288 | } | |
| 280 | 289 | ||
| 281 | 290 | // Audio playback handling | |
| 282 | - | wv.OnAudioStateChanged = func(playing bool) { | |
| 291 | + | wkWV.OnAudioStateChanged = func(playing bool) { | |
| 283 | 292 | if c.idleInhibitor == nil { | |
| 284 | 293 | return | |
| 285 | 294 | } | |
| @@ -5,19 +5,22 @@ import ( | |||||
| 5 | "net/url" | 5 | "net/url" | ||
| 6 | "strings" | 6 | "strings" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | "github.com/bnema/dumber/internal/application/usecase" | 9 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 9 | "github.com/bnema/dumber/internal/domain/entity" | 10 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 10 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 11 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 11 | "github.com/bnema/dumber/internal/logging" | 12 | "github.com/bnema/dumber/internal/logging" | ||
| 12 | - | webkitlib "github.com/bnema/puregotk-webkit/webkit" | |||
| 13 | "github.com/jwijenbergh/puregotk/v4/gdk" | 13 | "github.com/jwijenbergh/puregotk/v4/gdk" | ||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | - | func shouldRenderCrashPage(reason webkitlib.WebProcessTerminationReason) bool { | 16 | + | // internalSchemePath is the path used in dumb:// URIs (replaces webkit.HomePath). |
| 17 | + | const internalSchemePath = "home" | |||
| 18 | + | ||||
| 19 | + | func shouldRenderCrashPage(reason port.WebProcessTerminationReason) bool { | |||
| 17 | switch reason { | 20 | switch reason { | ||
| 18 | - | case webkitlib.WebProcessCrashedValue, webkitlib.WebProcessExceededMemoryLimitValue: | 21 | + | case port.WebProcessTerminationCrashed, port.WebProcessTerminationExceededMemory: |
| 19 | return true | 22 | return true | ||
| 20 | - | case webkitlib.WebProcessTerminatedByApiValue: | 23 | + | case port.WebProcessTerminationByAPI: |
| 21 | return false | 24 | return false | ||
| 22 | default: | 25 | default: | ||
| 23 | return true | 26 | return true | ||
| @@ -34,7 +37,7 @@ func extractOriginalURIFromCrashPage(uri string) string { | |||||
| 34 | return uri | 37 | return uri | ||
| 35 | } | 38 | } | ||
| 36 | 39 | ||||
| 37 | - | if parsed.Scheme != "dumb" || parsed.Host != webkit.HomePath { | 40 | + | if parsed.Scheme != "dumb" || parsed.Host != internalSchemePath { |
| 38 | return uri | 41 | return uri | ||
| 39 | } | 42 | } | ||
| 40 | 43 | ||||
| @@ -59,94 +62,94 @@ func buildCrashPageURI(originalURI string) string { | |||||
| 59 | } | 62 | } | ||
| 60 | 63 | ||||
| 61 | // setupWebViewCallbacks configures standard callbacks and popup handling. | 64 | // setupWebViewCallbacks configures standard callbacks and popup handling. | ||
| 62 | - | func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 65 | + | func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.PaneID, wv port.WebView) { |
| 63 | log := logging.FromContext(ctx) | 66 | log := logging.FromContext(ctx) | ||
| 64 | 67 | ||||
| 65 | - | // Title changes | 68 | + | // Build port callbacks for standard events |
| 66 | - | wv.OnTitleChanged = func(title string) { | 69 | + | callbacks := &port.WebViewCallbacks{ |
| 67 | - | c.onTitleChanged(ctx, paneID, title) | 70 | + | OnTitleChanged: func(title string) { |
| 68 | - | } | 71 | + | c.onTitleChanged(ctx, paneID, title) |
| 72 | + | }, | |||
| 73 | + | OnLoadChanged: func(event port.LoadEvent) { | |||
| 74 | + | switch event { | |||
| 75 | + | case port.LoadStarted: | |||
| 76 | + | c.onLoadStarted(paneID) | |||
| 77 | + | case port.LoadCommitted: | |||
| 78 | + | c.onLoadCommitted(ctx, paneID, wv) | |||
| 79 | + | case port.LoadFinished: | |||
| 80 | + | c.onLoadFinished(ctx, paneID, wv) | |||
| 81 | + | } | |||
| 82 | + | }, | |||
| 83 | + | OnProgressChanged: func(progress float64) { | |||
| 84 | + | c.onProgressChanged(paneID, progress) | |||
| 85 | + | }, | |||
| 86 | + | OnURIChanged: func(uri string) { | |||
| 87 | + | c.handleURIChanged(ctx, paneID, wv, uri) | |||
| 88 | + | }, | |||
| 89 | + | OnLinkHover: func(uri string) { | |||
| 90 | + | c.onLinkHover(paneID, uri) | |||
| 91 | + | }, | |||
| 92 | + | OnWebProcessTerminated: func(reason port.WebProcessTerminationReason, reasonLabel string, uri string) { | |||
| 93 | + | originalURI := extractOriginalURIFromCrashPage(uri) | |||
| 94 | + | if !shouldRenderCrashPage(reason) { | |||
| 95 | + | log.Info(). | |||
| 96 | + | Str("pane_id", string(paneID)). | |||
| 97 | + | Str("reason", reasonLabel). | |||
| 98 | + | Str("uri", uri). | |||
| 99 | + | Msg("web process termination handled without crash page") | |||
| 100 | + | return | |||
| 101 | + | } | |||
| 69 | 102 | ||||
| 70 | - | // Favicon changes - capture generation to detect stale callbacks after pool reuse | 103 | + | crashURI := buildCrashPageURI(originalURI) |
| 71 | - | faviconGen := wv.Generation() | 104 | + | log.Warn(). |
| 72 | - | wv.OnFaviconChanged = func(favicon *gdk.Texture) { | 105 | + | Str("pane_id", string(paneID)). |
| 73 | - | if wv.Generation() != faviconGen { | 106 | + | Str("reason", reasonLabel). |
| 74 | - | return // WebView was reused, ignore stale signal | 107 | + | Str("uri", uri). |
| 75 | - | } | 108 | + | Str("crash_uri", crashURI). |
| 76 | - | c.onFaviconChanged(ctx, paneID, wv, favicon) | 109 | + | Msg("web process terminated, redirecting to crash page") |
| 110 | + | ||||
| 111 | + | if err := wv.LoadURI(ctx, crashURI); err != nil { | |||
| 112 | + | log.Error(). | |||
| 113 | + | Err(err). | |||
| 114 | + | Str("pane_id", string(paneID)). | |||
| 115 | + | Str("reason", reasonLabel). | |||
| 116 | + | Str("uri", uri). | |||
| 117 | + | Str("crash_uri", crashURI). | |||
| 118 | + | Msg("failed to load crash page after web process termination") | |||
| 119 | + | } | |||
| 120 | + | }, | |||
| 121 | + | OnPermissionRequest: func(origin string, permTypes []string, allow, deny func()) bool { | |||
| 122 | + | return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny) | |||
| 123 | + | }, | |||
| 77 | } | 124 | } | ||
| 78 | 125 | ||||
| 79 | - | // Load events | 126 | + | // Favicon changes - need webkit-specific Generation() to detect stale callbacks |
| 80 | - | wv.OnLoadChanged = func(event webkit.LoadEvent) { | 127 | + | if wkWV, ok := wv.(*webkit.WebView); ok { |
| 81 | - | switch event { | 128 | + | faviconGen := wkWV.Generation() |
| 82 | - | case webkit.LoadStarted: | 129 | + | callbacks.OnFaviconChanged = func(favicon port.Texture) { |
| 83 | - | c.onLoadStarted(paneID) | 130 | + | if wkWV.Generation() != faviconGen { |
| 84 | - | case webkit.LoadCommitted: | 131 | + | return // WebView was reused, ignore stale signal |
| 85 | - | c.onLoadCommitted(ctx, paneID, wv) | 132 | + | } |
| 86 | - | case webkit.LoadFinished: | 133 | + | if gdkTexture, ok := favicon.(*gdk.Texture); ok { |
| 87 | - | c.onLoadFinished(ctx, paneID, wv) | 134 | + | c.onFaviconChanged(ctx, paneID, wkWV, gdkTexture) |
| 135 | + | } | |||
| 88 | } | 136 | } | ||
| 89 | } | 137 | } | ||
| 90 | 138 | ||||
| 91 | - | // Progress | 139 | + | // Add popup create handler if popup handling is configured |
| 92 | - | wv.OnProgressChanged = func(progress float64) { | 140 | + | callbacks.OnCreate = c.buildPopupCreateHandler(ctx, paneID, wv) |
| 93 | - | c.onProgressChanged(paneID, progress) | |||
| 94 | - | } | |||
| 95 | - | ||||
| 96 | - | // SPA navigation and external scheme handling | |||
| 97 | - | wv.OnURIChanged = func(uri string) { | |||
| 98 | - | c.handleURIChanged(ctx, paneID, wv, uri) | |||
| 99 | - | } | |||
| 100 | 141 | ||||
| 101 | - | // Middle-click / Ctrl+click handler for opening links in new pane | 142 | + | wv.SetCallbacks(callbacks) |
| 102 | - | wv.OnLinkMiddleClick = func(uri string) bool { | |||
| 103 | - | return c.handleLinkMiddleClick(ctx, paneID, uri) | |||
| 104 | - | } | |||
| 105 | 143 | ||||
| 106 | - | // Link hover callback for status overlay | 144 | + | // Webkit-specific callbacks not in port.WebViewCallbacks (middle-click, fullscreen, audio) |
| 107 | - | wv.OnLinkHover = func(uri string) { | 145 | + | if wkWV, ok := wv.(*webkit.WebView); ok { |
| 108 | - | c.onLinkHover(paneID, uri) | 146 | + | wkWV.OnLinkMiddleClick = func(uri string) bool { |
| 109 | - | } | 147 | + | return c.handleLinkMiddleClick(ctx, paneID, uri) |
| 110 | - | ||||
| 111 | - | wv.OnWebProcessTerminated = func(reason webkitlib.WebProcessTerminationReason, reasonLabel string, uri string) { | |||
| 112 | - | originalURI := extractOriginalURIFromCrashPage(uri) | |||
| 113 | - | if !shouldRenderCrashPage(reason) { | |||
| 114 | - | log.Info(). | |||
| 115 | - | Str("pane_id", string(paneID)). | |||
| 116 | - | Str("reason", reasonLabel). | |||
| 117 | - | Str("uri", uri). | |||
| 118 | - | Msg("web process termination handled without crash page") | |||
| 119 | - | return | |||
| 120 | - | } | |||
| 121 | - | ||||
| 122 | - | crashURI := buildCrashPageURI(originalURI) | |||
| 123 | - | log.Warn(). | |||
| 124 | - | Str("pane_id", string(paneID)). | |||
| 125 | - | Str("reason", reasonLabel). | |||
| 126 | - | Str("uri", uri). | |||
| 127 | - | Str("crash_uri", crashURI). | |||
| 128 | - | Msg("web process terminated, redirecting to crash page") | |||
| 129 | - | ||||
| 130 | - | if err := wv.LoadURI(ctx, crashURI); err != nil { | |||
| 131 | - | log.Error(). | |||
| 132 | - | Err(err). | |||
| 133 | - | Str("pane_id", string(paneID)). | |||
| 134 | - | Str("reason", reasonLabel). | |||
| 135 | - | Str("uri", uri). | |||
| 136 | - | Str("crash_uri", crashURI). | |||
| 137 | - | Msg("failed to load crash page after web process termination") | |||
| 138 | } | 148 | } | ||
| 139 | } | 149 | } | ||
| 140 | 150 | ||||
| 141 | - | // Permission request handler | |||
| 142 | - | wv.OnPermissionRequest = func(origin string, permTypes []string, allow, deny func()) bool { | |||
| 143 | - | return c.handlePermissionRequest(ctx, origin, permTypes, allow, deny) | |||
| 144 | - | } | |||
| 145 | // Fullscreen handlers for idle inhibition | 151 | // Fullscreen handlers for idle inhibition | ||
| 146 | c.setupIdleInhibitionHandlers(ctx, paneID, wv) | 152 | c.setupIdleInhibitionHandlers(ctx, paneID, wv) | ||
| 147 | - | ||||
| 148 | - | // Popup handling for nested popups | |||
| 149 | - | c.setupPopupHandling(ctx, paneID, wv) | |||
| 150 | } | 153 | } | ||
| 151 | 154 | ||||
| 152 | // handlePermissionRequest processes media permission requests from WebKit. | 155 | // handlePermissionRequest processes media permission requests from WebKit. | ||
| @@ -246,15 +249,21 @@ func filterWebRTCPermissionTypes(types []entity.PermissionType) []entity.Permiss | |||||
| 246 | // - The webview enters fullscreen mode (e.g., fullscreen video) | 249 | // - The webview enters fullscreen mode (e.g., fullscreen video) | ||
| 247 | // - The webview is playing audio (e.g., video/music playback) | 250 | // - The webview is playing audio (e.g., video/music playback) | ||
| 248 | // The inhibitor uses refcounting, so both can be active simultaneously. | 251 | // The inhibitor uses refcounting, so both can be active simultaneously. | ||
| 249 | - | func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 252 | + | func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv port.WebView) { |
| 250 | log := logging.FromContext(ctx) | 253 | log := logging.FromContext(ctx) | ||
| 251 | 254 | ||||
| 252 | if wv == nil { | 255 | if wv == nil { | ||
| 253 | return | 256 | return | ||
| 254 | } | 257 | } | ||
| 255 | 258 | ||||
| 259 | + | // Type-assert to access webkit-specific fullscreen/audio callbacks | |||
| 260 | + | wkWV, ok := wv.(*webkit.WebView) | |||
| 261 | + | if !ok { | |||
| 262 | + | return | |||
| 263 | + | } | |||
| 264 | + | ||||
| 256 | // Fullscreen handling | 265 | // Fullscreen handling | ||
| 257 | - | wv.OnEnterFullscreen = func() bool { | 266 | + | wkWV.OnEnterFullscreen = func() bool { |
| 258 | if c.idleInhibitor != nil { | 267 | if c.idleInhibitor != nil { | ||
| 259 | if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil { | 268 | if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil { | ||
| 260 | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle") | 269 | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle") | ||
| @@ -266,7 +275,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en | |||||
| 266 | return false // Allow fullscreen | 275 | return false // Allow fullscreen | ||
| 267 | } | 276 | } | ||
| 268 | 277 | ||||
| 269 | - | wv.OnLeaveFullscreen = func() bool { | 278 | + | wkWV.OnLeaveFullscreen = func() bool { |
| 270 | if c.idleInhibitor != nil { | 279 | if c.idleInhibitor != nil { | ||
| 271 | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | 280 | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | ||
| 272 | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle") | 281 | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle") | ||
| @@ -279,7 +288,7 @@ func (c *Coordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID en | |||||
| 279 | } | 288 | } | ||
| 280 | 289 | ||||
| 281 | // Audio playback handling | 290 | // Audio playback handling | ||
| 282 | - | wv.OnAudioStateChanged = func(playing bool) { | 291 | + | wkWV.OnAudioStateChanged = func(playing bool) { |
| 283 | if c.idleInhibitor == nil { | 292 | if c.idleInhibitor == nil { | ||
| 284 | return | 293 | return | ||
| 285 | } | 294 | } | ||
M internal/ui/coordinator/content/content_active_override_test.go +9 -8
| @@ -6,19 +6,20 @@ import ( | |||
| 6 | 6 | ||
| 7 | 7 | "github.com/stretchr/testify/assert" | |
| 8 | 8 | ||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |
| 9 | 11 | "github.com/bnema/dumber/internal/domain/entity" | |
| 10 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 11 | 12 | "github.com/bnema/dumber/internal/ui/component" | |
| 12 | 13 | ) | |
| 13 | 14 | ||
| 14 | 15 | func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | |
| 15 | 16 | t.Parallel() | |
| 16 | 17 | ||
| 17 | - | mainWV := &webkit.WebView{} | |
| 18 | - | floatingWV := &webkit.WebView{} | |
| 18 | + | mainWV := mocks.NewMockWebView(t) | |
| 19 | + | floatingWV := mocks.NewMockWebView(t) | |
| 19 | 20 | ||
| 20 | 21 | c := &Coordinator{ | |
| 21 | - | webViews: map[entity.PaneID]*webkit.WebView{ | |
| 22 | + | webViews: map[entity.PaneID]port.WebView{ | |
| 22 | 23 | "main-pane": mainWV, | |
| 23 | 24 | "floating-pane": floatingWV, | |
| 24 | 25 | }, | |
| @@ -33,10 +34,10 @@ func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | |||
| 33 | 34 | func TestCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) { | |
| 34 | 35 | t.Parallel() | |
| 35 | 36 | ||
| 36 | - | mainWV := &webkit.WebView{} | |
| 37 | + | mainWV := mocks.NewMockWebView(t) | |
| 37 | 38 | ||
| 38 | 39 | c := &Coordinator{ | |
| 39 | - | webViews: map[entity.PaneID]*webkit.WebView{ | |
| 40 | + | webViews: map[entity.PaneID]port.WebView{ | |
| 40 | 41 | "main-pane": mainWV, | |
| 41 | 42 | }, | |
| 42 | 43 | } | |
| @@ -52,12 +53,12 @@ func TestCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing. | |||
| 52 | 53 | t.Parallel() | |
| 53 | 54 | ||
| 54 | 55 | ctx := context.Background() | |
| 55 | - | mainWV := &webkit.WebView{} | |
| 56 | + | mainWV := mocks.NewMockWebView(t) | |
| 56 | 57 | mainPane := entity.NewPane(entity.PaneID("main-pane")) | |
| 57 | 58 | ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane) | |
| 58 | 59 | ||
| 59 | 60 | c := &Coordinator{ | |
| 60 | - | webViews: map[entity.PaneID]*webkit.WebView{ | |
| 61 | + | webViews: map[entity.PaneID]port.WebView{ | |
| 61 | 62 | mainPane.ID: mainWV, | |
| 62 | 63 | }, | |
| 63 | 64 | getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { | |
| @@ -6,19 +6,20 @@ import ( | |||||
| 6 | 6 | ||||
| 7 | "github.com/stretchr/testify/assert" | 7 | "github.com/stretchr/testify/assert" | ||
| 8 | 8 | ||||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |||
| 9 | "github.com/bnema/dumber/internal/domain/entity" | 11 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 10 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |||
| 11 | "github.com/bnema/dumber/internal/ui/component" | 12 | "github.com/bnema/dumber/internal/ui/component" | ||
| 12 | ) | 13 | ) | ||
| 13 | 14 | ||||
| 14 | func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | 15 | func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | ||
| 15 | t.Parallel() | 16 | t.Parallel() | ||
| 16 | 17 | ||||
| 17 | - | mainWV := &webkit.WebView{} | 18 | + | mainWV := mocks.NewMockWebView(t) |
| 18 | - | floatingWV := &webkit.WebView{} | 19 | + | floatingWV := mocks.NewMockWebView(t) |
| 19 | 20 | ||||
| 20 | c := &Coordinator{ | 21 | c := &Coordinator{ | ||
| 21 | - | webViews: map[entity.PaneID]*webkit.WebView{ | 22 | + | webViews: map[entity.PaneID]port.WebView{ |
| 22 | "main-pane": mainWV, | 23 | "main-pane": mainWV, | ||
| 23 | "floating-pane": floatingWV, | 24 | "floating-pane": floatingWV, | ||
| 24 | }, | 25 | }, | ||
| @@ -33,10 +34,10 @@ func TestCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | |||||
| 33 | func TestCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) { | 34 | func TestCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) { | ||
| 34 | t.Parallel() | 35 | t.Parallel() | ||
| 35 | 36 | ||||
| 36 | - | mainWV := &webkit.WebView{} | 37 | + | mainWV := mocks.NewMockWebView(t) |
| 37 | 38 | ||||
| 38 | c := &Coordinator{ | 39 | c := &Coordinator{ | ||
| 39 | - | webViews: map[entity.PaneID]*webkit.WebView{ | 40 | + | webViews: map[entity.PaneID]port.WebView{ |
| 40 | "main-pane": mainWV, | 41 | "main-pane": mainWV, | ||
| 41 | }, | 42 | }, | ||
| 42 | } | 43 | } | ||
| @@ -52,12 +53,12 @@ func TestCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing. | |||||
| 52 | t.Parallel() | 53 | t.Parallel() | ||
| 53 | 54 | ||||
| 54 | ctx := context.Background() | 55 | ctx := context.Background() | ||
| 55 | - | mainWV := &webkit.WebView{} | 56 | + | mainWV := mocks.NewMockWebView(t) |
| 56 | mainPane := entity.NewPane(entity.PaneID("main-pane")) | 57 | mainPane := entity.NewPane(entity.PaneID("main-pane")) | ||
| 57 | ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane) | 58 | ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane) | ||
| 58 | 59 | ||||
| 59 | c := &Coordinator{ | 60 | c := &Coordinator{ | ||
| 60 | - | webViews: map[entity.PaneID]*webkit.WebView{ | 61 | + | webViews: map[entity.PaneID]port.WebView{ |
| 61 | mainPane.ID: mainWV, | 62 | mainPane.ID: mainWV, | ||
| 62 | }, | 63 | }, | ||
| 63 | getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { | 64 | getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { | ||
M internal/ui/coordinator/content/content_reveal_test.go +5 -5
| @@ -7,8 +7,8 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | "github.com/stretchr/testify/assert" | |
| 9 | 9 | ||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | 11 | "github.com/bnema/dumber/internal/domain/entity" | |
| 11 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | 14 | // TestPendingReveal tests the pending reveal state management. | |
| @@ -49,7 +49,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) { | |||
| 49 | 49 | callbackCalled := false | |
| 50 | 50 | c := &Coordinator{ | |
| 51 | 51 | pendingReveal: make(map[entity.PaneID]bool), | |
| 52 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 52 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 53 | 53 | onWebViewShown: func(paneID entity.PaneID) { | |
| 54 | 54 | callbackCalled = true | |
| 55 | 55 | }, | |
| @@ -65,7 +65,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) { | |||
| 65 | 65 | func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) { | |
| 66 | 66 | c := &Coordinator{ | |
| 67 | 67 | pendingReveal: make(map[entity.PaneID]bool), | |
| 68 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 68 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 69 | 69 | } | |
| 70 | 70 | paneID := entity.PaneID("pane-1") | |
| 71 | 71 | ||
| @@ -84,7 +84,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) { | |||
| 84 | 84 | revealCount := 0 | |
| 85 | 85 | c := &Coordinator{ | |
| 86 | 86 | pendingReveal: make(map[entity.PaneID]bool), | |
| 87 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 87 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 88 | 88 | onWebViewShown: func(paneID entity.PaneID) { | |
| 89 | 89 | revealCount++ | |
| 90 | 90 | }, | |
| @@ -111,7 +111,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) { | |||
| 111 | 111 | func TestPendingReveal_ConcurrentAccess(t *testing.T) { | |
| 112 | 112 | c := &Coordinator{ | |
| 113 | 113 | pendingReveal: make(map[entity.PaneID]bool), | |
| 114 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 114 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | 117 | var wg sync.WaitGroup | |
| @@ -7,8 +7,8 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | "github.com/stretchr/testify/assert" | 8 | "github.com/stretchr/testify/assert" | ||
| 9 | 9 | ||||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | "github.com/bnema/dumber/internal/domain/entity" | 11 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 11 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | // TestPendingReveal tests the pending reveal state management. | 14 | // TestPendingReveal tests the pending reveal state management. | ||
| @@ -49,7 +49,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) { | |||||
| 49 | callbackCalled := false | 49 | callbackCalled := false | ||
| 50 | c := &Coordinator{ | 50 | c := &Coordinator{ | ||
| 51 | pendingReveal: make(map[entity.PaneID]bool), | 51 | pendingReveal: make(map[entity.PaneID]bool), | ||
| 52 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | 52 | + | webViews: make(map[entity.PaneID]port.WebView), |
| 53 | onWebViewShown: func(paneID entity.PaneID) { | 53 | onWebViewShown: func(paneID entity.PaneID) { | ||
| 54 | callbackCalled = true | 54 | callbackCalled = true | ||
| 55 | }, | 55 | }, | ||
| @@ -65,7 +65,7 @@ func TestRevealIfPending_NotPending_DoesNothing(t *testing.T) { | |||||
| 65 | func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) { | 65 | func TestRevealIfPending_ClearsStateOnReveal(t *testing.T) { | ||
| 66 | c := &Coordinator{ | 66 | c := &Coordinator{ | ||
| 67 | pendingReveal: make(map[entity.PaneID]bool), | 67 | pendingReveal: make(map[entity.PaneID]bool), | ||
| 68 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | 68 | + | webViews: make(map[entity.PaneID]port.WebView), |
| 69 | } | 69 | } | ||
| 70 | paneID := entity.PaneID("pane-1") | 70 | paneID := entity.PaneID("pane-1") | ||
| 71 | 71 | ||||
| @@ -84,7 +84,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) { | |||||
| 84 | revealCount := 0 | 84 | revealCount := 0 | ||
| 85 | c := &Coordinator{ | 85 | c := &Coordinator{ | ||
| 86 | pendingReveal: make(map[entity.PaneID]bool), | 86 | pendingReveal: make(map[entity.PaneID]bool), | ||
| 87 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | 87 | + | webViews: make(map[entity.PaneID]port.WebView), |
| 88 | onWebViewShown: func(paneID entity.PaneID) { | 88 | onWebViewShown: func(paneID entity.PaneID) { | ||
| 89 | revealCount++ | 89 | revealCount++ | ||
| 90 | }, | 90 | }, | ||
| @@ -111,7 +111,7 @@ func TestRevealIfPending_OnlyRevealsOnce(t *testing.T) { | |||||
| 111 | func TestPendingReveal_ConcurrentAccess(t *testing.T) { | 111 | func TestPendingReveal_ConcurrentAccess(t *testing.T) { | ||
| 112 | c := &Coordinator{ | 112 | c := &Coordinator{ | ||
| 113 | pendingReveal: make(map[entity.PaneID]bool), | 113 | pendingReveal: make(map[entity.PaneID]bool), | ||
| 114 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | 114 | + | webViews: make(map[entity.PaneID]port.WebView), |
| 115 | } | 115 | } | ||
| 116 | 116 | ||||
| 117 | var wg sync.WaitGroup | 117 | var wg sync.WaitGroup | ||
M internal/ui/coordinator/content/coordinator.go +12 -12
| @@ -19,14 +19,14 @@ import ( | |||
| 19 | 19 | ||
| 20 | 20 | // Coordinator manages WebView lifecycle, title tracking, and content attachment. | |
| 21 | 21 | type Coordinator struct { | |
| 22 | - | pool *webkit.WebViewPool | |
| 22 | + | pool port.WebViewPool | |
| 23 | 23 | widgetFactory layout.WidgetFactory | |
| 24 | 24 | faviconAdapter *adapter.FaviconAdapter | |
| 25 | 25 | zoomUC *usecase.ManageZoomUseCase | |
| 26 | 26 | permissionUC *usecase.HandlePermissionUseCase | |
| 27 | 27 | injector *webkit.ContentInjector | |
| 28 | 28 | ||
| 29 | - | webViews map[entity.PaneID]*webkit.WebView | |
| 29 | + | webViews map[entity.PaneID]port.WebView | |
| 30 | 30 | webViewsMu sync.RWMutex | |
| 31 | 31 | ||
| 32 | 32 | activePaneOverride entity.PaneID | |
| @@ -79,7 +79,7 @@ type Coordinator struct { | |||
| 79 | 79 | gestureActionHandler input.ActionHandler | |
| 80 | 80 | ||
| 81 | 81 | // Popup handling | |
| 82 | - | factory *webkit.WebViewFactory | |
| 82 | + | factory port.WebViewFactory | |
| 83 | 83 | popupConfig *config.PopupBehaviorConfig | |
| 84 | 84 | pendingPopups map[port.WebViewID]*PendingPopup | |
| 85 | 85 | popupOAuth map[port.WebViewID]*popupOAuthState | |
| @@ -102,7 +102,7 @@ type Coordinator struct { | |||
| 102 | 102 | onFullscreenChanged func(entering bool) | |
| 103 | 103 | ||
| 104 | 104 | // Callback when WebView gains focus (for accent picker text input targeting) | |
| 105 | - | onWebViewFocused func(paneID entity.PaneID, wv *webkit.WebView) | |
| 105 | + | onWebViewFocused func(paneID entity.PaneID, wv port.WebView) | |
| 106 | 106 | ||
| 107 | 107 | // Callback for first load_started event (triggers deferred initialization) | |
| 108 | 108 | onFirstLoadStarted func() | |
| @@ -134,7 +134,7 @@ const ( | |||
| 134 | 134 | // NewCoordinator creates a new Coordinator. | |
| 135 | 135 | func NewCoordinator( | |
| 136 | 136 | ctx context.Context, | |
| 137 | - | pool *webkit.WebViewPool, | |
| 137 | + | pool port.WebViewPool, | |
| 138 | 138 | widgetFactory layout.WidgetFactory, | |
| 139 | 139 | faviconAdapter *adapter.FaviconAdapter, | |
| 140 | 140 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView), | |
| @@ -150,7 +150,7 @@ func NewCoordinator( | |||
| 150 | 150 | faviconAdapter: faviconAdapter, | |
| 151 | 151 | zoomUC: zoomUC, | |
| 152 | 152 | permissionUC: permissionUC, | |
| 153 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 153 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 154 | 154 | paneTitles: make(map[entity.PaneID]string), | |
| 155 | 155 | navOrigins: make(map[entity.PaneID]string), | |
| 156 | 156 | pendingReveal: make(map[entity.PaneID]bool), | |
| @@ -214,7 +214,7 @@ func (c *Coordinator) SetOnFullscreenChanged(fn func(entering bool)) { | |||
| 214 | 214 | } | |
| 215 | 215 | ||
| 216 | 216 | // SetOnWebViewFocused sets the callback for when a WebView gains focus. | |
| 217 | - | func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv *webkit.WebView)) { | |
| 217 | + | func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv port.WebView)) { | |
| 218 | 218 | c.onWebViewFocused = fn | |
| 219 | 219 | } | |
| 220 | 220 | ||
| @@ -270,19 +270,19 @@ func (c *Coordinator) webViewCount() int { | |||
| 270 | 270 | return len(c.webViews) | |
| 271 | 271 | } | |
| 272 | 272 | ||
| 273 | - | func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) *webkit.WebView { | |
| 273 | + | func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView { | |
| 274 | 274 | c.webViewsMu.RLock() | |
| 275 | 275 | defer c.webViewsMu.RUnlock() | |
| 276 | 276 | return c.webViews[paneID] | |
| 277 | 277 | } | |
| 278 | 278 | ||
| 279 | - | func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv *webkit.WebView) { | |
| 279 | + | func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv port.WebView) { | |
| 280 | 280 | c.webViewsMu.Lock() | |
| 281 | 281 | c.webViews[paneID] = wv | |
| 282 | 282 | c.webViewsMu.Unlock() | |
| 283 | 283 | } | |
| 284 | 284 | ||
| 285 | - | func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView { | |
| 285 | + | func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) port.WebView { | |
| 286 | 286 | c.webViewsMu.Lock() | |
| 287 | 287 | defer c.webViewsMu.Unlock() | |
| 288 | 288 | wv := c.webViews[paneID] | |
| @@ -290,10 +290,10 @@ func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView | |||
| 290 | 290 | return wv | |
| 291 | 291 | } | |
| 292 | 292 | ||
| 293 | - | func (c *Coordinator) snapshotWebViews() map[entity.PaneID]*webkit.WebView { | |
| 293 | + | func (c *Coordinator) snapshotWebViews() map[entity.PaneID]port.WebView { | |
| 294 | 294 | c.webViewsMu.RLock() | |
| 295 | 295 | defer c.webViewsMu.RUnlock() | |
| 296 | - | snapshot := make(map[entity.PaneID]*webkit.WebView, len(c.webViews)) | |
| 296 | + | snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews)) | |
| 297 | 297 | for paneID, wv := range c.webViews { | |
| 298 | 298 | snapshot[paneID] = wv | |
| 299 | 299 | } | |
| @@ -19,14 +19,14 @@ import ( | |||||
| 19 | 19 | ||||
| 20 | // Coordinator manages WebView lifecycle, title tracking, and content attachment. | 20 | // Coordinator manages WebView lifecycle, title tracking, and content attachment. | ||
| 21 | type Coordinator struct { | 21 | type Coordinator struct { | ||
| 22 | - | pool *webkit.WebViewPool | 22 | + | pool port.WebViewPool |
| 23 | widgetFactory layout.WidgetFactory | 23 | widgetFactory layout.WidgetFactory | ||
| 24 | faviconAdapter *adapter.FaviconAdapter | 24 | faviconAdapter *adapter.FaviconAdapter | ||
| 25 | zoomUC *usecase.ManageZoomUseCase | 25 | zoomUC *usecase.ManageZoomUseCase | ||
| 26 | permissionUC *usecase.HandlePermissionUseCase | 26 | permissionUC *usecase.HandlePermissionUseCase | ||
| 27 | injector *webkit.ContentInjector | 27 | injector *webkit.ContentInjector | ||
| 28 | 28 | ||||
| 29 | - | webViews map[entity.PaneID]*webkit.WebView | 29 | + | webViews map[entity.PaneID]port.WebView |
| 30 | webViewsMu sync.RWMutex | 30 | webViewsMu sync.RWMutex | ||
| 31 | 31 | ||||
| 32 | activePaneOverride entity.PaneID | 32 | activePaneOverride entity.PaneID | ||
| @@ -79,7 +79,7 @@ type Coordinator struct { | |||||
| 79 | gestureActionHandler input.ActionHandler | 79 | gestureActionHandler input.ActionHandler | ||
| 80 | 80 | ||||
| 81 | // Popup handling | 81 | // Popup handling | ||
| 82 | - | factory *webkit.WebViewFactory | 82 | + | factory port.WebViewFactory |
| 83 | popupConfig *config.PopupBehaviorConfig | 83 | popupConfig *config.PopupBehaviorConfig | ||
| 84 | pendingPopups map[port.WebViewID]*PendingPopup | 84 | pendingPopups map[port.WebViewID]*PendingPopup | ||
| 85 | popupOAuth map[port.WebViewID]*popupOAuthState | 85 | popupOAuth map[port.WebViewID]*popupOAuthState | ||
| @@ -102,7 +102,7 @@ type Coordinator struct { | |||||
| 102 | onFullscreenChanged func(entering bool) | 102 | onFullscreenChanged func(entering bool) | ||
| 103 | 103 | ||||
| 104 | // Callback when WebView gains focus (for accent picker text input targeting) | 104 | // Callback when WebView gains focus (for accent picker text input targeting) | ||
| 105 | - | onWebViewFocused func(paneID entity.PaneID, wv *webkit.WebView) | 105 | + | onWebViewFocused func(paneID entity.PaneID, wv port.WebView) |
| 106 | 106 | ||||
| 107 | // Callback for first load_started event (triggers deferred initialization) | 107 | // Callback for first load_started event (triggers deferred initialization) | ||
| 108 | onFirstLoadStarted func() | 108 | onFirstLoadStarted func() | ||
| @@ -134,7 +134,7 @@ const ( | |||||
| 134 | // NewCoordinator creates a new Coordinator. | 134 | // NewCoordinator creates a new Coordinator. | ||
| 135 | func NewCoordinator( | 135 | func NewCoordinator( | ||
| 136 | ctx context.Context, | 136 | ctx context.Context, | ||
| 137 | - | pool *webkit.WebViewPool, | 137 | + | pool port.WebViewPool, |
| 138 | widgetFactory layout.WidgetFactory, | 138 | widgetFactory layout.WidgetFactory, | ||
| 139 | faviconAdapter *adapter.FaviconAdapter, | 139 | faviconAdapter *adapter.FaviconAdapter, | ||
| 140 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView), | 140 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView), | ||
| @@ -150,7 +150,7 @@ func NewCoordinator( | |||||
| 150 | faviconAdapter: faviconAdapter, | 150 | faviconAdapter: faviconAdapter, | ||
| 151 | zoomUC: zoomUC, | 151 | zoomUC: zoomUC, | ||
| 152 | permissionUC: permissionUC, | 152 | permissionUC: permissionUC, | ||
| 153 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | 153 | + | webViews: make(map[entity.PaneID]port.WebView), |
| 154 | paneTitles: make(map[entity.PaneID]string), | 154 | paneTitles: make(map[entity.PaneID]string), | ||
| 155 | navOrigins: make(map[entity.PaneID]string), | 155 | navOrigins: make(map[entity.PaneID]string), | ||
| 156 | pendingReveal: make(map[entity.PaneID]bool), | 156 | pendingReveal: make(map[entity.PaneID]bool), | ||
| @@ -214,7 +214,7 @@ func (c *Coordinator) SetOnFullscreenChanged(fn func(entering bool)) { | |||||
| 214 | } | 214 | } | ||
| 215 | 215 | ||||
| 216 | // SetOnWebViewFocused sets the callback for when a WebView gains focus. | 216 | // SetOnWebViewFocused sets the callback for when a WebView gains focus. | ||
| 217 | - | func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv *webkit.WebView)) { | 217 | + | func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv port.WebView)) { |
| 218 | c.onWebViewFocused = fn | 218 | c.onWebViewFocused = fn | ||
| 219 | } | 219 | } | ||
| 220 | 220 | ||||
| @@ -270,19 +270,19 @@ func (c *Coordinator) webViewCount() int { | |||||
| 270 | return len(c.webViews) | 270 | return len(c.webViews) | ||
| 271 | } | 271 | } | ||
| 272 | 272 | ||||
| 273 | - | func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) *webkit.WebView { | 273 | + | func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView { |
| 274 | c.webViewsMu.RLock() | 274 | c.webViewsMu.RLock() | ||
| 275 | defer c.webViewsMu.RUnlock() | 275 | defer c.webViewsMu.RUnlock() | ||
| 276 | return c.webViews[paneID] | 276 | return c.webViews[paneID] | ||
| 277 | } | 277 | } | ||
| 278 | 278 | ||||
| 279 | - | func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv *webkit.WebView) { | 279 | + | func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv port.WebView) { |
| 280 | c.webViewsMu.Lock() | 280 | c.webViewsMu.Lock() | ||
| 281 | c.webViews[paneID] = wv | 281 | c.webViews[paneID] = wv | ||
| 282 | c.webViewsMu.Unlock() | 282 | c.webViewsMu.Unlock() | ||
| 283 | } | 283 | } | ||
| 284 | 284 | ||||
| 285 | - | func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView { | 285 | + | func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) port.WebView { |
| 286 | c.webViewsMu.Lock() | 286 | c.webViewsMu.Lock() | ||
| 287 | defer c.webViewsMu.Unlock() | 287 | defer c.webViewsMu.Unlock() | ||
| 288 | wv := c.webViews[paneID] | 288 | wv := c.webViews[paneID] | ||
| @@ -290,10 +290,10 @@ func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) *webkit.WebView | |||||
| 290 | return wv | 290 | return wv | ||
| 291 | } | 291 | } | ||
| 292 | 292 | ||||
| 293 | - | func (c *Coordinator) snapshotWebViews() map[entity.PaneID]*webkit.WebView { | 293 | + | func (c *Coordinator) snapshotWebViews() map[entity.PaneID]port.WebView { |
| 294 | c.webViewsMu.RLock() | 294 | c.webViewsMu.RLock() | ||
| 295 | defer c.webViewsMu.RUnlock() | 295 | defer c.webViewsMu.RUnlock() | ||
| 296 | - | snapshot := make(map[entity.PaneID]*webkit.WebView, len(c.webViews)) | 296 | + | snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews)) |
| 297 | for paneID, wv := range c.webViews { | 297 | for paneID, wv := range c.webViews { | ||
| 298 | snapshot[paneID] = wv | 298 | snapshot[paneID] = wv | ||
| 299 | } | 299 | } | ||
M internal/ui/coordinator/content/lifecycle.go +18 -8
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 9 | 10 | "github.com/bnema/dumber/internal/logging" | |
| @@ -13,7 +14,7 @@ import ( | |||
| 13 | 14 | ) | |
| 14 | 15 | ||
| 15 | 16 | // EnsureWebView acquires or reuses a WebView for the given pane. | |
| 16 | - | func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) { | |
| 17 | + | func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) { | |
| 17 | 18 | log := logging.FromContext(ctx) | |
| 18 | 19 | ||
| 19 | 20 | if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() { | |
| @@ -81,7 +82,7 @@ func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID) | |||
| 81 | 82 | c.navOriginMu.Unlock() | |
| 82 | 83 | ||
| 83 | 84 | if c.pool != nil { | |
| 84 | - | c.pool.Release(ctx, wv) | |
| 85 | + | c.pool.Release(wv) | |
| 85 | 86 | } else { | |
| 86 | 87 | wv.Destroy() | |
| 87 | 88 | } | |
| @@ -129,7 +130,7 @@ func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspac | |||
| 129 | 130 | ||
| 130 | 131 | // WrapWidget converts a WebView to a layout.Widget for embedding. | |
| 131 | 132 | // It also attaches gesture handlers for mouse button navigation. | |
| 132 | - | func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout.Widget { | |
| 133 | + | func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget { | |
| 133 | 134 | log := logging.FromContext(ctx) | |
| 134 | 135 | ||
| 135 | 136 | if wv == nil || c.widgetFactory == nil { | |
| @@ -137,7 +138,14 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout | |||
| 137 | 138 | return nil | |
| 138 | 139 | } | |
| 139 | 140 | ||
| 140 | - | gtkView := wv.Widget() | |
| 141 | + | // Type-assert to access webkit-specific Widget() for GTK embedding | |
| 142 | + | wkWV, ok := wv.(*webkit.WebView) | |
| 143 | + | if !ok { | |
| 144 | + | log.Debug().Msg("webview does not support widget embedding") | |
| 145 | + | return nil | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | gtkView := wkWV.Widget() | |
| 141 | 149 | if gtkView == nil { | |
| 142 | 150 | return nil | |
| 143 | 151 | } | |
| @@ -148,7 +156,9 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout | |||
| 148 | 156 | if widget != nil { | |
| 149 | 157 | gestureHandler := input.NewGestureHandler(ctx) | |
| 150 | 158 | // Pass WebView directly to preserve user gesture context (like Epiphany) | |
| 151 | - | gestureHandler.SetNavigator(wv) | |
| 159 | + | if nav, ok := wv.(input.DirectNavigator); ok { | |
| 160 | + | gestureHandler.SetNavigator(nav) | |
| 161 | + | } | |
| 152 | 162 | // Keep callback as fallback | |
| 153 | 163 | if c.gestureActionHandler != nil { | |
| 154 | 164 | gestureHandler.SetOnAction(c.gestureActionHandler) | |
| @@ -161,7 +171,7 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout | |||
| 161 | 171 | } | |
| 162 | 172 | ||
| 163 | 173 | // ActiveWebView returns the WebView for the active pane. | |
| 164 | - | func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | |
| 174 | + | func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView { | |
| 165 | 175 | log := logging.FromContext(ctx) | |
| 166 | 176 | ||
| 167 | 177 | if paneID, ok := c.activePaneOverrideID(); ok { | |
| @@ -192,13 +202,13 @@ func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | |||
| 192 | 202 | } | |
| 193 | 203 | ||
| 194 | 204 | // GetWebView returns the WebView for a specific pane. | |
| 195 | - | func (c *Coordinator) GetWebView(paneID entity.PaneID) *webkit.WebView { | |
| 205 | + | func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView { | |
| 196 | 206 | return c.getWebViewLocked(paneID) | |
| 197 | 207 | } | |
| 198 | 208 | ||
| 199 | 209 | // RegisterPopupWebView registers a popup WebView that was created externally. | |
| 200 | 210 | // This is used when popup tabs are created and the WebView needs to be tracked. | |
| 201 | - | func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv *webkit.WebView) { | |
| 211 | + | func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) { | |
| 202 | 212 | if wv != nil && paneID != "" { | |
| 203 | 213 | c.setWebViewLocked(paneID, wv) | |
| 204 | 214 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 8 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 9 | "github.com/bnema/dumber/internal/logging" | 10 | "github.com/bnema/dumber/internal/logging" | ||
| @@ -13,7 +14,7 @@ import ( | |||||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| 15 | // EnsureWebView acquires or reuses a WebView for the given pane. | 16 | // EnsureWebView acquires or reuses a WebView for the given pane. | ||
| 16 | - | func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) { | 17 | + | func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) { |
| 17 | log := logging.FromContext(ctx) | 18 | log := logging.FromContext(ctx) | ||
| 18 | 19 | ||||
| 19 | if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() { | 20 | if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() { | ||
| @@ -81,7 +82,7 @@ func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID) | |||||
| 81 | c.navOriginMu.Unlock() | 82 | c.navOriginMu.Unlock() | ||
| 82 | 83 | ||||
| 83 | if c.pool != nil { | 84 | if c.pool != nil { | ||
| 84 | - | c.pool.Release(ctx, wv) | 85 | + | c.pool.Release(wv) |
| 85 | } else { | 86 | } else { | ||
| 86 | wv.Destroy() | 87 | wv.Destroy() | ||
| 87 | } | 88 | } | ||
| @@ -129,7 +130,7 @@ func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspac | |||||
| 129 | 130 | ||||
| 130 | // WrapWidget converts a WebView to a layout.Widget for embedding. | 131 | // WrapWidget converts a WebView to a layout.Widget for embedding. | ||
| 131 | // It also attaches gesture handlers for mouse button navigation. | 132 | // It also attaches gesture handlers for mouse button navigation. | ||
| 132 | - | func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout.Widget { | 133 | + | func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget { |
| 133 | log := logging.FromContext(ctx) | 134 | log := logging.FromContext(ctx) | ||
| 134 | 135 | ||||
| 135 | if wv == nil || c.widgetFactory == nil { | 136 | if wv == nil || c.widgetFactory == nil { | ||
| @@ -137,7 +138,14 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout | |||||
| 137 | return nil | 138 | return nil | ||
| 138 | } | 139 | } | ||
| 139 | 140 | ||||
| 140 | - | gtkView := wv.Widget() | 141 | + | // Type-assert to access webkit-specific Widget() for GTK embedding |
| 142 | + | wkWV, ok := wv.(*webkit.WebView) | |||
| 143 | + | if !ok { | |||
| 144 | + | log.Debug().Msg("webview does not support widget embedding") | |||
| 145 | + | return nil | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | gtkView := wkWV.Widget() | |||
| 141 | if gtkView == nil { | 149 | if gtkView == nil { | ||
| 142 | return nil | 150 | return nil | ||
| 143 | } | 151 | } | ||
| @@ -148,7 +156,9 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout | |||||
| 148 | if widget != nil { | 156 | if widget != nil { | ||
| 149 | gestureHandler := input.NewGestureHandler(ctx) | 157 | gestureHandler := input.NewGestureHandler(ctx) | ||
| 150 | // Pass WebView directly to preserve user gesture context (like Epiphany) | 158 | // Pass WebView directly to preserve user gesture context (like Epiphany) | ||
| 151 | - | gestureHandler.SetNavigator(wv) | 159 | + | if nav, ok := wv.(input.DirectNavigator); ok { |
| 160 | + | gestureHandler.SetNavigator(nav) | |||
| 161 | + | } | |||
| 152 | // Keep callback as fallback | 162 | // Keep callback as fallback | ||
| 153 | if c.gestureActionHandler != nil { | 163 | if c.gestureActionHandler != nil { | ||
| 154 | gestureHandler.SetOnAction(c.gestureActionHandler) | 164 | gestureHandler.SetOnAction(c.gestureActionHandler) | ||
| @@ -161,7 +171,7 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv *webkit.WebView) layout | |||||
| 161 | } | 171 | } | ||
| 162 | 172 | ||||
| 163 | // ActiveWebView returns the WebView for the active pane. | 173 | // ActiveWebView returns the WebView for the active pane. | ||
| 164 | - | func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | 174 | + | func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView { |
| 165 | log := logging.FromContext(ctx) | 175 | log := logging.FromContext(ctx) | ||
| 166 | 176 | ||||
| 167 | if paneID, ok := c.activePaneOverrideID(); ok { | 177 | if paneID, ok := c.activePaneOverrideID(); ok { | ||
| @@ -192,13 +202,13 @@ func (c *Coordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | |||||
| 192 | } | 202 | } | ||
| 193 | 203 | ||||
| 194 | // GetWebView returns the WebView for a specific pane. | 204 | // GetWebView returns the WebView for a specific pane. | ||
| 195 | - | func (c *Coordinator) GetWebView(paneID entity.PaneID) *webkit.WebView { | 205 | + | func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView { |
| 196 | return c.getWebViewLocked(paneID) | 206 | return c.getWebViewLocked(paneID) | ||
| 197 | } | 207 | } | ||
| 198 | 208 | ||||
| 199 | // RegisterPopupWebView registers a popup WebView that was created externally. | 209 | // RegisterPopupWebView registers a popup WebView that was created externally. | ||
| 200 | // This is used when popup tabs are created and the WebView needs to be tracked. | 210 | // This is used when popup tabs are created and the WebView needs to be tracked. | ||
| 201 | - | func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv *webkit.WebView) { | 211 | + | func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) { |
| 202 | if wv != nil && paneID != "" { | 212 | if wv != nil && paneID != "" { | ||
| 203 | c.setWebViewLocked(paneID, wv) | 213 | c.setWebViewLocked(paneID, wv) | ||
| 204 | } | 214 | } | ||
M internal/ui/coordinator/content/navigation.go +32 -25
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "strings" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 8 | "github.com/bnema/dumber/internal/application/usecase" | |
| 8 | 9 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | 10 | urlutil "github.com/bnema/dumber/internal/domain/url" | |
| @@ -16,7 +17,7 @@ import ( | |||
| 16 | 17 | // WebKit may reset zoom during document transitions, so we reapply after LoadCommitted. | |
| 17 | 18 | // History is recorded here because the URI is guaranteed to be correct after commit. | |
| 18 | 19 | // Also shows the WebView widget (it's hidden during creation to avoid white flash). | |
| 19 | - | func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 20 | + | func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView) { | |
| 20 | 21 | log := logging.FromContext(ctx) | |
| 21 | 22 | logging.Trace().Mark("load_committed") | |
| 22 | 23 | ||
| @@ -26,20 +27,23 @@ func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, | |||
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | 29 | // Set appropriate background color based on page type to prevent dark background bleeding. | |
| 29 | - | switch { | |
| 30 | - | case strings.HasPrefix(uri, "dumb://"): | |
| 31 | - | // Internal pages: apply themed background | |
| 32 | - | theme, ok := c.getCurrentTheme() | |
| 33 | - | if ok && theme.prefersDark { | |
| 34 | - | wv.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA) | |
| 35 | - | } else { | |
| 36 | - | wv.ResetBackgroundToDefault() | |
| 30 | + | // Type-assert to access webkit-specific background color methods. | |
| 31 | + | if wkWV, ok := wv.(*webkit.WebView); ok { | |
| 32 | + | switch { | |
| 33 | + | case strings.HasPrefix(uri, "dumb://"): | |
| 34 | + | // Internal pages: apply themed background | |
| 35 | + | theme, ok := c.getCurrentTheme() | |
| 36 | + | if ok && theme.prefersDark { | |
| 37 | + | wkWV.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA) | |
| 38 | + | } else { | |
| 39 | + | wkWV.ResetBackgroundToDefault() | |
| 40 | + | } | |
| 41 | + | case strings.HasPrefix(uri, "about:"): | |
| 42 | + | // Keep pool background (no action) | |
| 43 | + | default: | |
| 44 | + | // External pages: white background | |
| 45 | + | wkWV.ResetBackgroundToDefault() | |
| 37 | 46 | } | |
| 38 | - | case strings.HasPrefix(uri, "about:"): | |
| 39 | - | // Keep pool background (no action) | |
| 40 | - | default: | |
| 41 | - | // External pages: white background | |
| 42 | - | wv.ResetBackgroundToDefault() | |
| 43 | 47 | } | |
| 44 | 48 | ||
| 45 | 49 | // Show the WebView now that content is being painted | |
| @@ -125,7 +129,7 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) { | |||
| 125 | 129 | } | |
| 126 | 130 | } | |
| 127 | 131 | ||
| 128 | - | func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv *webkit.WebView) bool { | |
| 132 | + | func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv port.WebView) bool { | |
| 129 | 133 | if wv == nil || wv.IsDestroyed() { | |
| 130 | 134 | return false | |
| 131 | 135 | } | |
| @@ -190,7 +194,7 @@ func (c *Coordinator) onLoadStarted(paneID entity.PaneID) { | |||
| 190 | 194 | } | |
| 191 | 195 | ||
| 192 | 196 | // onLoadFinished hides the progress bar when page loading completes. | |
| 193 | - | func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 197 | + | func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv port.WebView) { | |
| 194 | 198 | _, wsView := c.getActiveWS() | |
| 195 | 199 | if wsView == nil { | |
| 196 | 200 | return | |
| @@ -255,14 +259,17 @@ func (c *Coordinator) revealIfPending(ctx context.Context, paneID entity.PaneID, | |||
| 255 | 259 | return | |
| 256 | 260 | } | |
| 257 | 261 | ||
| 258 | - | if inner := wv.Widget(); inner != nil { | |
| 259 | - | inner.SetVisible(true) | |
| 260 | - | logging.FromContext(ctx). | |
| 261 | - | Debug(). | |
| 262 | - | Str("pane_id", string(paneID)). | |
| 263 | - | Str("uri", uri). | |
| 264 | - | Str("reason", reason). | |
| 265 | - | Msg("webview revealed") | |
| 262 | + | // Type-assert to access webkit-specific Widget() for visibility control | |
| 263 | + | if wkWV, ok := wv.(*webkit.WebView); ok { | |
| 264 | + | if inner := wkWV.Widget(); inner != nil { | |
| 265 | + | inner.SetVisible(true) | |
| 266 | + | logging.FromContext(ctx). | |
| 267 | + | Debug(). | |
| 268 | + | Str("pane_id", string(paneID)). | |
| 269 | + | Str("uri", uri). | |
| 270 | + | Str("reason", reason). | |
| 271 | + | Msg("webview revealed") | |
| 272 | + | } | |
| 266 | 273 | } | |
| 267 | 274 | ||
| 268 | 275 | // Mark first_paint and finish startup trace | |
| @@ -295,7 +302,7 @@ func (c *Coordinator) onLinkHover(paneID entity.PaneID, uri string) { | |||
| 295 | 302 | ||
| 296 | 303 | // handleURIChanged handles URI changes from WebKit, including external scheme detection | |
| 297 | 304 | // and SPA navigation tracking. | |
| 298 | - | func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView, uri string) { | |
| 305 | + | func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv port.WebView, uri string) { | |
| 299 | 306 | if uri == "" { | |
| 300 | 307 | return | |
| 301 | 308 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "strings" | 5 | "strings" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | "github.com/bnema/dumber/internal/application/usecase" | 8 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 9 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | urlutil "github.com/bnema/dumber/internal/domain/url" | 10 | urlutil "github.com/bnema/dumber/internal/domain/url" | ||
| @@ -16,7 +17,7 @@ import ( | |||||
| 16 | // WebKit may reset zoom during document transitions, so we reapply after LoadCommitted. | 17 | // WebKit may reset zoom during document transitions, so we reapply after LoadCommitted. | ||
| 17 | // History is recorded here because the URI is guaranteed to be correct after commit. | 18 | // History is recorded here because the URI is guaranteed to be correct after commit. | ||
| 18 | // Also shows the WebView widget (it's hidden during creation to avoid white flash). | 19 | // Also shows the WebView widget (it's hidden during creation to avoid white flash). | ||
| 19 | - | func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 20 | + | func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView) { |
| 20 | log := logging.FromContext(ctx) | 21 | log := logging.FromContext(ctx) | ||
| 21 | logging.Trace().Mark("load_committed") | 22 | logging.Trace().Mark("load_committed") | ||
| 22 | 23 | ||||
| @@ -26,20 +27,23 @@ func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, | |||||
| 26 | } | 27 | } | ||
| 27 | 28 | ||||
| 28 | // Set appropriate background color based on page type to prevent dark background bleeding. | 29 | // Set appropriate background color based on page type to prevent dark background bleeding. | ||
| 29 | - | switch { | 30 | + | // Type-assert to access webkit-specific background color methods. |
| 30 | - | case strings.HasPrefix(uri, "dumb://"): | 31 | + | if wkWV, ok := wv.(*webkit.WebView); ok { |
| 31 | - | // Internal pages: apply themed background | 32 | + | switch { |
| 32 | - | theme, ok := c.getCurrentTheme() | 33 | + | case strings.HasPrefix(uri, "dumb://"): |
| 33 | - | if ok && theme.prefersDark { | 34 | + | // Internal pages: apply themed background |
| 34 | - | wv.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA) | 35 | + | theme, ok := c.getCurrentTheme() |
| 35 | - | } else { | 36 | + | if ok && theme.prefersDark { |
| 36 | - | wv.ResetBackgroundToDefault() | 37 | + | wkWV.SetBackgroundColor(darkBgR, darkBgG, darkBgB, darkBgA) |
| 38 | + | } else { | |||
| 39 | + | wkWV.ResetBackgroundToDefault() | |||
| 40 | + | } | |||
| 41 | + | case strings.HasPrefix(uri, "about:"): | |||
| 42 | + | // Keep pool background (no action) | |||
| 43 | + | default: | |||
| 44 | + | // External pages: white background | |||
| 45 | + | wkWV.ResetBackgroundToDefault() | |||
| 37 | } | 46 | } | ||
| 38 | - | case strings.HasPrefix(uri, "about:"): | |||
| 39 | - | // Keep pool background (no action) | |||
| 40 | - | default: | |||
| 41 | - | // External pages: white background | |||
| 42 | - | wv.ResetBackgroundToDefault() | |||
| 43 | } | 47 | } | ||
| 44 | 48 | ||||
| 45 | // Show the WebView now that content is being painted | 49 | // Show the WebView now that content is being painted | ||
| @@ -125,7 +129,7 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) { | |||||
| 125 | } | 129 | } | ||
| 126 | } | 130 | } | ||
| 127 | 131 | ||||
| 128 | - | func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv *webkit.WebView) bool { | 132 | + | func (c *Coordinator) shouldSkipAboutBlankAppearance(paneID entity.PaneID, wv port.WebView) bool { |
| 129 | if wv == nil || wv.IsDestroyed() { | 133 | if wv == nil || wv.IsDestroyed() { | ||
| 130 | return false | 134 | return false | ||
| 131 | } | 135 | } | ||
| @@ -190,7 +194,7 @@ func (c *Coordinator) onLoadStarted(paneID entity.PaneID) { | |||||
| 190 | } | 194 | } | ||
| 191 | 195 | ||||
| 192 | // onLoadFinished hides the progress bar when page loading completes. | 196 | // onLoadFinished hides the progress bar when page loading completes. | ||
| 193 | - | func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 197 | + | func (c *Coordinator) onLoadFinished(ctx context.Context, paneID entity.PaneID, wv port.WebView) { |
| 194 | _, wsView := c.getActiveWS() | 198 | _, wsView := c.getActiveWS() | ||
| 195 | if wsView == nil { | 199 | if wsView == nil { | ||
| 196 | return | 200 | return | ||
| @@ -255,14 +259,17 @@ func (c *Coordinator) revealIfPending(ctx context.Context, paneID entity.PaneID, | |||||
| 255 | return | 259 | return | ||
| 256 | } | 260 | } | ||
| 257 | 261 | ||||
| 258 | - | if inner := wv.Widget(); inner != nil { | 262 | + | // Type-assert to access webkit-specific Widget() for visibility control |
| 259 | - | inner.SetVisible(true) | 263 | + | if wkWV, ok := wv.(*webkit.WebView); ok { |
| 260 | - | logging.FromContext(ctx). | 264 | + | if inner := wkWV.Widget(); inner != nil { |
| 261 | - | Debug(). | 265 | + | inner.SetVisible(true) |
| 262 | - | Str("pane_id", string(paneID)). | 266 | + | logging.FromContext(ctx). |
| 263 | - | Str("uri", uri). | 267 | + | Debug(). |
| 264 | - | Str("reason", reason). | 268 | + | Str("pane_id", string(paneID)). |
| 265 | - | Msg("webview revealed") | 269 | + | Str("uri", uri). |
| 270 | + | Str("reason", reason). | |||
| 271 | + | Msg("webview revealed") | |||
| 272 | + | } | |||
| 266 | } | 273 | } | ||
| 267 | 274 | ||||
| 268 | // Mark first_paint and finish startup trace | 275 | // Mark first_paint and finish startup trace | ||
| @@ -295,7 +302,7 @@ func (c *Coordinator) onLinkHover(paneID entity.PaneID, uri string) { | |||||
| 295 | 302 | ||||
| 296 | // handleURIChanged handles URI changes from WebKit, including external scheme detection | 303 | // handleURIChanged handles URI changes from WebKit, including external scheme detection | ||
| 297 | // and SPA navigation tracking. | 304 | // and SPA navigation tracking. | ||
| 298 | - | func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView, uri string) { | 305 | + | func (c *Coordinator) handleURIChanged(ctx context.Context, paneID entity.PaneID, wv port.WebView, uri string) { |
| 299 | if uri == "" { | 306 | if uri == "" { | ||
| 300 | return | 307 | return | ||
| 301 | } | 308 | } | ||
M internal/ui/coordinator/content/oauth.go +21 -9
| @@ -208,10 +208,17 @@ func (c *Coordinator) setupOAuthAutoClose( | |||
| 208 | 208 | ctx context.Context, | |
| 209 | 209 | paneID entity.PaneID, | |
| 210 | 210 | popupID port.WebViewID, | |
| 211 | - | wv *webkit.WebView, | |
| 211 | + | wv port.WebView, | |
| 212 | 212 | ) { | |
| 213 | 213 | log := logging.FromContext(ctx) | |
| 214 | 214 | ||
| 215 | + | // Type-assert to access webkit-specific direct field callbacks and Close() | |
| 216 | + | wkWV, ok := wv.(*webkit.WebView) | |
| 217 | + | if !ok { | |
| 218 | + | log.Debug().Str("pane_id", string(paneID)).Msg("oauth auto-close: webview does not support webkit callbacks") | |
| 219 | + | return | |
| 220 | + | } | |
| 221 | + | ||
| 215 | 222 | // Safety timeout - only triggers if popup gets stuck (provider should close via window.close) | |
| 216 | 223 | var safetyTimer *time.Timer | |
| 217 | 224 | var safetyTimerMu sync.Mutex | |
| @@ -232,7 +239,7 @@ func (c *Coordinator) setupOAuthAutoClose( | |||
| 232 | 239 | uri := wv.URI() | |
| 233 | 240 | if shouldForceCloseOnSafetyTimeout(uri) { | |
| 234 | 241 | log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup") | |
| 235 | - | wv.Close() | |
| 242 | + | wkWV.Close() | |
| 236 | 243 | return false | |
| 237 | 244 | } | |
| 238 | 245 | log.Debug(). | |
| @@ -268,7 +275,7 @@ func (c *Coordinator) setupOAuthAutoClose( | |||
| 268 | 275 | time.AfterFunc(oauthCloseDelay, func() { | |
| 269 | 276 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 270 | 277 | if wv != nil && !wv.IsDestroyed() { | |
| 271 | - | wv.Close() | |
| 278 | + | wkWV.Close() | |
| 272 | 279 | } | |
| 273 | 280 | return false | |
| 274 | 281 | }) | |
| @@ -281,26 +288,31 @@ func (c *Coordinator) setupOAuthAutoClose( | |||
| 281 | 288 | startSafetyTimer() | |
| 282 | 289 | ||
| 283 | 290 | // Wrap OnURIChanged to check for OAuth callbacks. | |
| 284 | - | wv.OnURIChanged = composeOnURIChanged(wv.OnURIChanged, func(uri string) { | |
| 291 | + | wkWV.OnURIChanged = composeOnURIChanged(wkWV.OnURIChanged, func(uri string) { | |
| 285 | 292 | if ShouldAutoClose(uri) { | |
| 286 | 293 | requestOAuthClose(uri, "uri_changed") | |
| 287 | 294 | } | |
| 288 | 295 | }) | |
| 289 | 296 | ||
| 290 | 297 | // Monitor load events for URL-based detection. | |
| 291 | - | wv.OnLoadChanged = composeOnLoadChanged(wv.OnLoadChanged, func(event webkit.LoadEvent) { | |
| 292 | - | if event == webkit.LoadCommitted { | |
| 298 | + | // Wrap webkit.LoadEvent to port.LoadEvent at the boundary. | |
| 299 | + | existingOnLoad := wkWV.OnLoadChanged | |
| 300 | + | wkWV.OnLoadChanged = func(event webkit.LoadEvent) { | |
| 301 | + | if existingOnLoad != nil { | |
| 302 | + | existingOnLoad(event) | |
| 303 | + | } | |
| 304 | + | if port.LoadEvent(event) == port.LoadCommitted { | |
| 293 | 305 | uri := wv.URI() | |
| 294 | 306 | if ShouldAutoClose(uri) { | |
| 295 | 307 | requestOAuthClose(uri, "load_committed") | |
| 296 | 308 | } | |
| 297 | 309 | } | |
| 298 | - | }) | |
| 310 | + | } | |
| 299 | 311 | ||
| 300 | 312 | // Cancel safety timer on any close path. | |
| 301 | - | wv.OnClose = composeOnClose(func() { | |
| 313 | + | wkWV.OnClose = composeOnClose(func() { | |
| 302 | 314 | cancelSafetyTimer() | |
| 303 | - | }, wv.OnClose) | |
| 315 | + | }, wkWV.OnClose) | |
| 304 | 316 | } | |
| 305 | 317 | ||
| 306 | 318 | func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) { | |
| @@ -208,10 +208,17 @@ func (c *Coordinator) setupOAuthAutoClose( | |||||
| 208 | ctx context.Context, | 208 | ctx context.Context, | ||
| 209 | paneID entity.PaneID, | 209 | paneID entity.PaneID, | ||
| 210 | popupID port.WebViewID, | 210 | popupID port.WebViewID, | ||
| 211 | - | wv *webkit.WebView, | 211 | + | wv port.WebView, |
| 212 | ) { | 212 | ) { | ||
| 213 | log := logging.FromContext(ctx) | 213 | log := logging.FromContext(ctx) | ||
| 214 | 214 | ||||
| 215 | + | // Type-assert to access webkit-specific direct field callbacks and Close() | |||
| 216 | + | wkWV, ok := wv.(*webkit.WebView) | |||
| 217 | + | if !ok { | |||
| 218 | + | log.Debug().Str("pane_id", string(paneID)).Msg("oauth auto-close: webview does not support webkit callbacks") | |||
| 219 | + | return | |||
| 220 | + | } | |||
| 221 | + | ||||
| 215 | // Safety timeout - only triggers if popup gets stuck (provider should close via window.close) | 222 | // Safety timeout - only triggers if popup gets stuck (provider should close via window.close) | ||
| 216 | var safetyTimer *time.Timer | 223 | var safetyTimer *time.Timer | ||
| 217 | var safetyTimerMu sync.Mutex | 224 | var safetyTimerMu sync.Mutex | ||
| @@ -232,7 +239,7 @@ func (c *Coordinator) setupOAuthAutoClose( | |||||
| 232 | uri := wv.URI() | 239 | uri := wv.URI() | ||
| 233 | if shouldForceCloseOnSafetyTimeout(uri) { | 240 | if shouldForceCloseOnSafetyTimeout(uri) { | ||
| 234 | log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup") | 241 | log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup") | ||
| 235 | - | wv.Close() | 242 | + | wkWV.Close() |
| 236 | return false | 243 | return false | ||
| 237 | } | 244 | } | ||
| 238 | log.Debug(). | 245 | log.Debug(). | ||
| @@ -268,7 +275,7 @@ func (c *Coordinator) setupOAuthAutoClose( | |||||
| 268 | time.AfterFunc(oauthCloseDelay, func() { | 275 | time.AfterFunc(oauthCloseDelay, func() { | ||
| 269 | cb := glib.SourceFunc(func(_ uintptr) bool { | 276 | cb := glib.SourceFunc(func(_ uintptr) bool { | ||
| 270 | if wv != nil && !wv.IsDestroyed() { | 277 | if wv != nil && !wv.IsDestroyed() { | ||
| 271 | - | wv.Close() | 278 | + | wkWV.Close() |
| 272 | } | 279 | } | ||
| 273 | return false | 280 | return false | ||
| 274 | }) | 281 | }) | ||
| @@ -281,26 +288,31 @@ func (c *Coordinator) setupOAuthAutoClose( | |||||
| 281 | startSafetyTimer() | 288 | startSafetyTimer() | ||
| 282 | 289 | ||||
| 283 | // Wrap OnURIChanged to check for OAuth callbacks. | 290 | // Wrap OnURIChanged to check for OAuth callbacks. | ||
| 284 | - | wv.OnURIChanged = composeOnURIChanged(wv.OnURIChanged, func(uri string) { | 291 | + | wkWV.OnURIChanged = composeOnURIChanged(wkWV.OnURIChanged, func(uri string) { |
| 285 | if ShouldAutoClose(uri) { | 292 | if ShouldAutoClose(uri) { | ||
| 286 | requestOAuthClose(uri, "uri_changed") | 293 | requestOAuthClose(uri, "uri_changed") | ||
| 287 | } | 294 | } | ||
| 288 | }) | 295 | }) | ||
| 289 | 296 | ||||
| 290 | // Monitor load events for URL-based detection. | 297 | // Monitor load events for URL-based detection. | ||
| 291 | - | wv.OnLoadChanged = composeOnLoadChanged(wv.OnLoadChanged, func(event webkit.LoadEvent) { | 298 | + | // Wrap webkit.LoadEvent to port.LoadEvent at the boundary. |
| 292 | - | if event == webkit.LoadCommitted { | 299 | + | existingOnLoad := wkWV.OnLoadChanged |
| 300 | + | wkWV.OnLoadChanged = func(event webkit.LoadEvent) { | |||
| 301 | + | if existingOnLoad != nil { | |||
| 302 | + | existingOnLoad(event) | |||
| 303 | + | } | |||
| 304 | + | if port.LoadEvent(event) == port.LoadCommitted { | |||
| 293 | uri := wv.URI() | 305 | uri := wv.URI() | ||
| 294 | if ShouldAutoClose(uri) { | 306 | if ShouldAutoClose(uri) { | ||
| 295 | requestOAuthClose(uri, "load_committed") | 307 | requestOAuthClose(uri, "load_committed") | ||
| 296 | } | 308 | } | ||
| 297 | } | 309 | } | ||
| 298 | - | }) | 310 | + | } |
| 299 | 311 | ||||
| 300 | // Cancel safety timer on any close path. | 312 | // Cancel safety timer on any close path. | ||
| 301 | - | wv.OnClose = composeOnClose(func() { | 313 | + | wkWV.OnClose = composeOnClose(func() { |
| 302 | cancelSafetyTimer() | 314 | cancelSafetyTimer() | ||
| 303 | - | }, wv.OnClose) | 315 | + | }, wkWV.OnClose) |
| 304 | } | 316 | } | ||
| 305 | 317 | ||||
| 306 | func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) { | 318 | func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) { | ||
M internal/ui/coordinator/content/oauth_test.go +2 -3
| @@ -9,7 +9,6 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/dumber/internal/application/port" | |
| 11 | 11 | "github.com/bnema/dumber/internal/domain/entity" | |
| 12 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 13 | 12 | ) | |
| 14 | 13 | ||
| 15 | 14 | func TestComposeOnClose_Order(t *testing.T) { | |
| @@ -53,7 +52,7 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentRefresh(t *testing.T) { | |||
| 53 | 52 | popupID := port.WebViewID(101) | |
| 54 | 53 | ||
| 55 | 54 | c := &Coordinator{ | |
| 56 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 55 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 57 | 56 | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | |
| 58 | 57 | popupRefresh: make(map[entity.PaneID]*time.Timer), | |
| 59 | 58 | } | |
| @@ -82,7 +81,7 @@ func TestHandlePopupOAuthClose_ErrorDoesNotScheduleParentRefresh(t *testing.T) { | |||
| 82 | 81 | popupID := port.WebViewID(102) | |
| 83 | 82 | ||
| 84 | 83 | c := &Coordinator{ | |
| 85 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 84 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 86 | 85 | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | |
| 87 | 86 | popupRefresh: make(map[entity.PaneID]*time.Timer), | |
| 88 | 87 | } | |
| @@ -9,7 +9,6 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | "github.com/bnema/dumber/internal/application/port" | 10 | "github.com/bnema/dumber/internal/application/port" | ||
| 11 | "github.com/bnema/dumber/internal/domain/entity" | 11 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 12 | - | "github.com/bnema/dumber/internal/infrastructure/webkit" | |||
| 13 | ) | 12 | ) | ||
| 14 | 13 | ||||
| 15 | func TestComposeOnClose_Order(t *testing.T) { | 14 | func TestComposeOnClose_Order(t *testing.T) { | ||
| @@ -53,7 +52,7 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentRefresh(t *testing.T) { | |||||
| 53 | popupID := port.WebViewID(101) | 52 | popupID := port.WebViewID(101) | ||
| 54 | 53 | ||||
| 55 | c := &Coordinator{ | 54 | c := &Coordinator{ | ||
| 56 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | 55 | + | webViews: make(map[entity.PaneID]port.WebView), |
| 57 | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | 56 | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | ||
| 58 | popupRefresh: make(map[entity.PaneID]*time.Timer), | 57 | popupRefresh: make(map[entity.PaneID]*time.Timer), | ||
| 59 | } | 58 | } | ||
| @@ -82,7 +81,7 @@ func TestHandlePopupOAuthClose_ErrorDoesNotScheduleParentRefresh(t *testing.T) { | |||||
| 82 | popupID := port.WebViewID(102) | 81 | popupID := port.WebViewID(102) | ||
| 83 | 82 | ||||
| 84 | c := &Coordinator{ | 83 | c := &Coordinator{ | ||
| 85 | - | webViews: make(map[entity.PaneID]*webkit.WebView), | 84 | + | webViews: make(map[entity.PaneID]port.WebView), |
| 86 | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | 85 | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | ||
| 87 | popupRefresh: make(map[entity.PaneID]*time.Timer), | 86 | popupRefresh: make(map[entity.PaneID]*time.Timer), | ||
| 88 | } | 87 | } | ||
M internal/ui/coordinator/content/popup.go +23 -21
| @@ -132,7 +132,7 @@ func GetBehavior(popupType PopupType, cfg *config.PopupBehaviorConfig) config.Po | |||
| 132 | 132 | ||
| 133 | 133 | // SetPopupConfig configures popup handling. | |
| 134 | 134 | func (c *Coordinator) SetPopupConfig( | |
| 135 | - | factory *webkit.WebViewFactory, | |
| 135 | + | factory port.WebViewFactory, | |
| 136 | 136 | popupConfig *config.PopupBehaviorConfig, | |
| 137 | 137 | generateID func() string, | |
| 138 | 138 | ) { | |
| @@ -151,21 +151,19 @@ func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity. | |||
| 151 | 151 | c.onClosePane = fn | |
| 152 | 152 | } | |
| 153 | 153 | ||
| 154 | - | // setupPopupHandling wires the popup create signal for a WebView. | |
| 155 | - | // This should be called after acquiring a WebView for a pane. | |
| 156 | - | func (c *Coordinator) setupPopupHandling(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 157 | - | log := logging.FromContext(ctx) | |
| 158 | - | ||
| 154 | + | // buildPopupCreateHandler returns the OnCreate callback for a WebView. | |
| 155 | + | // Returns nil if popup handling is not configured. | |
| 156 | + | func (c *Coordinator) buildPopupCreateHandler(ctx context.Context, paneID entity.PaneID, wv port.WebView) func(port.PopupRequest) port.WebView { | |
| 159 | 157 | if wv == nil { | |
| 160 | - | return | |
| 158 | + | return nil | |
| 161 | 159 | } | |
| 162 | 160 | ||
| 163 | - | // Wire the OnCreate callback for popup requests | |
| 164 | - | wv.OnCreate = func(req webkit.PopupRequest) *webkit.WebView { | |
| 161 | + | log := logging.FromContext(ctx) | |
| 162 | + | log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview") | |
| 163 | + | ||
| 164 | + | return func(req port.PopupRequest) port.WebView { | |
| 165 | 165 | return c.handlePopupCreate(ctx, paneID, wv, req) | |
| 166 | 166 | } | |
| 167 | - | ||
| 168 | - | log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview") | |
| 169 | 167 | } | |
| 170 | 168 | ||
| 171 | 169 | // createPopupPane creates a new pane entity for a popup window. | |
| @@ -199,9 +197,9 @@ func (c *Coordinator) createPopupPane( | |||
| 199 | 197 | func (c *Coordinator) handlePopupCreate( | |
| 200 | 198 | ctx context.Context, | |
| 201 | 199 | parentPaneID entity.PaneID, | |
| 202 | - | parentWV *webkit.WebView, | |
| 203 | - | req webkit.PopupRequest, | |
| 204 | - | ) *webkit.WebView { | |
| 200 | + | parentWV port.WebView, | |
| 201 | + | req port.PopupRequest, | |
| 202 | + | ) port.WebView { | |
| 205 | 203 | log := logging.FromContext(ctx) | |
| 206 | 204 | ||
| 207 | 205 | log.Debug(). | |
| @@ -306,13 +304,15 @@ func (c *Coordinator) handlePopupCreate( | |||
| 306 | 304 | c.pendingPopups[popupID] = pending | |
| 307 | 305 | c.popupMu.Unlock() | |
| 308 | 306 | ||
| 309 | - | // Wire ready-to-show (now just for visibility) and close signals | |
| 310 | - | popupWV.OnReadyToShow = func() { | |
| 311 | - | c.handlePopupReadyToShow(ctx, popupID) | |
| 307 | + | // Wire ready-to-show and close signals via webkit-specific fields | |
| 308 | + | if wkWV, ok := popupWV.(*webkit.WebView); ok { | |
| 309 | + | wkWV.OnReadyToShow = func() { | |
| 310 | + | c.handlePopupReadyToShow(ctx, popupID) | |
| 311 | + | } | |
| 312 | + | wkWV.OnClose = composeOnClose(wkWV.OnClose, func() { | |
| 313 | + | c.handlePopupClose(ctx, popupID) | |
| 314 | + | }) | |
| 312 | 315 | } | |
| 313 | - | popupWV.OnClose = composeOnClose(popupWV.OnClose, func() { | |
| 314 | - | c.handlePopupClose(ctx, popupID) | |
| 315 | - | }) | |
| 316 | 316 | ||
| 317 | 317 | log.Info(). | |
| 318 | 318 | Uint64("popup_id", uint64(popupID)). | |
| @@ -350,7 +350,9 @@ func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.W | |||
| 350 | 350 | ||
| 351 | 351 | // Make the WebView visible now that it's ready | |
| 352 | 352 | if pending.WebView != nil { | |
| 353 | - | pending.WebView.Show() | |
| 353 | + | if wkWV, ok := pending.WebView.(*webkit.WebView); ok { | |
| 354 | + | wkWV.Show() | |
| 355 | + | } | |
| 354 | 356 | } | |
| 355 | 357 | ||
| 356 | 358 | log.Info(). | |
| @@ -132,7 +132,7 @@ func GetBehavior(popupType PopupType, cfg *config.PopupBehaviorConfig) config.Po | |||||
| 132 | 132 | ||||
| 133 | // SetPopupConfig configures popup handling. | 133 | // SetPopupConfig configures popup handling. | ||
| 134 | func (c *Coordinator) SetPopupConfig( | 134 | func (c *Coordinator) SetPopupConfig( | ||
| 135 | - | factory *webkit.WebViewFactory, | 135 | + | factory port.WebViewFactory, |
| 136 | popupConfig *config.PopupBehaviorConfig, | 136 | popupConfig *config.PopupBehaviorConfig, | ||
| 137 | generateID func() string, | 137 | generateID func() string, | ||
| 138 | ) { | 138 | ) { | ||
| @@ -151,21 +151,19 @@ func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity. | |||||
| 151 | c.onClosePane = fn | 151 | c.onClosePane = fn | ||
| 152 | } | 152 | } | ||
| 153 | 153 | ||||
| 154 | - | // setupPopupHandling wires the popup create signal for a WebView. | 154 | + | // buildPopupCreateHandler returns the OnCreate callback for a WebView. |
| 155 | - | // This should be called after acquiring a WebView for a pane. | 155 | + | // Returns nil if popup handling is not configured. |
| 156 | - | func (c *Coordinator) setupPopupHandling(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 156 | + | func (c *Coordinator) buildPopupCreateHandler(ctx context.Context, paneID entity.PaneID, wv port.WebView) func(port.PopupRequest) port.WebView { |
| 157 | - | log := logging.FromContext(ctx) | |||
| 158 | - | ||||
| 159 | if wv == nil { | 157 | if wv == nil { | ||
| 160 | - | return | 158 | + | return nil |
| 161 | } | 159 | } | ||
| 162 | 160 | ||||
| 163 | - | // Wire the OnCreate callback for popup requests | 161 | + | log := logging.FromContext(ctx) |
| 164 | - | wv.OnCreate = func(req webkit.PopupRequest) *webkit.WebView { | 162 | + | log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview") |
| 163 | + | ||||
| 164 | + | return func(req port.PopupRequest) port.WebView { | |||
| 165 | return c.handlePopupCreate(ctx, paneID, wv, req) | 165 | return c.handlePopupCreate(ctx, paneID, wv, req) | ||
| 166 | } | 166 | } | ||
| 167 | - | ||||
| 168 | - | log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview") | |||
| 169 | } | 167 | } | ||
| 170 | 168 | ||||
| 171 | // createPopupPane creates a new pane entity for a popup window. | 169 | // createPopupPane creates a new pane entity for a popup window. | ||
| @@ -199,9 +197,9 @@ func (c *Coordinator) createPopupPane( | |||||
| 199 | func (c *Coordinator) handlePopupCreate( | 197 | func (c *Coordinator) handlePopupCreate( | ||
| 200 | ctx context.Context, | 198 | ctx context.Context, | ||
| 201 | parentPaneID entity.PaneID, | 199 | parentPaneID entity.PaneID, | ||
| 202 | - | parentWV *webkit.WebView, | 200 | + | parentWV port.WebView, |
| 203 | - | req webkit.PopupRequest, | 201 | + | req port.PopupRequest, |
| 204 | - | ) *webkit.WebView { | 202 | + | ) port.WebView { |
| 205 | log := logging.FromContext(ctx) | 203 | log := logging.FromContext(ctx) | ||
| 206 | 204 | ||||
| 207 | log.Debug(). | 205 | log.Debug(). | ||
| @@ -306,13 +304,15 @@ func (c *Coordinator) handlePopupCreate( | |||||
| 306 | c.pendingPopups[popupID] = pending | 304 | c.pendingPopups[popupID] = pending | ||
| 307 | c.popupMu.Unlock() | 305 | c.popupMu.Unlock() | ||
| 308 | 306 | ||||
| 309 | - | // Wire ready-to-show (now just for visibility) and close signals | 307 | + | // Wire ready-to-show and close signals via webkit-specific fields |
| 310 | - | popupWV.OnReadyToShow = func() { | 308 | + | if wkWV, ok := popupWV.(*webkit.WebView); ok { |
| 311 | - | c.handlePopupReadyToShow(ctx, popupID) | 309 | + | wkWV.OnReadyToShow = func() { |
| 310 | + | c.handlePopupReadyToShow(ctx, popupID) | |||
| 311 | + | } | |||
| 312 | + | wkWV.OnClose = composeOnClose(wkWV.OnClose, func() { | |||
| 313 | + | c.handlePopupClose(ctx, popupID) | |||
| 314 | + | }) | |||
| 312 | } | 315 | } | ||
| 313 | - | popupWV.OnClose = composeOnClose(popupWV.OnClose, func() { | |||
| 314 | - | c.handlePopupClose(ctx, popupID) | |||
| 315 | - | }) | |||
| 316 | 316 | ||||
| 317 | log.Info(). | 317 | log.Info(). | ||
| 318 | Uint64("popup_id", uint64(popupID)). | 318 | Uint64("popup_id", uint64(popupID)). | ||
| @@ -350,7 +350,9 @@ func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.W | |||||
| 350 | 350 | ||||
| 351 | // Make the WebView visible now that it's ready | 351 | // Make the WebView visible now that it's ready | ||
| 352 | if pending.WebView != nil { | 352 | if pending.WebView != nil { | ||
| 353 | - | pending.WebView.Show() | 353 | + | if wkWV, ok := pending.WebView.(*webkit.WebView); ok { |
| 354 | + | wkWV.Show() | |||
| 355 | + | } | |||
| 354 | } | 356 | } | ||
| 355 | 357 | ||||
| 356 | log.Info(). | 358 | log.Info(). | ||
M internal/ui/coordinator/content/title.go +12 -5
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "strings" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 9 | 10 | "github.com/bnema/dumber/internal/logging" | |
| @@ -115,7 +116,7 @@ func (c *Coordinator) syncStackedTitle(ctx context.Context, paneID entity.PaneID | |||
| 115 | 116 | } | |
| 116 | 117 | ||
| 117 | 118 | // onFaviconChanged updates favicon tracking when a WebView's favicon changes. | |
| 118 | - | func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV *webkit.WebView, favicon *gdk.Texture) { | |
| 119 | + | func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV port.WebView, favicon *gdk.Texture) { | |
| 119 | 120 | log := logging.FromContext(ctx) | |
| 120 | 121 | ||
| 121 | 122 | // Verify this WebView is still bound to the expected pane | |
| @@ -189,7 +190,7 @@ func (c *Coordinator) updateStackedFaviconForPane(ctx context.Context, paneID en | |||
| 189 | 190 | // First checks if WebKit already has a favicon for the page (common for subpath URLs). | |
| 190 | 191 | // Falls back to the full GetOrFetch pipeline (cache + WebKit DB + DuckDuckGo API). | |
| 191 | 192 | // This closes the gap where title bars relied solely on the notify::favicon signal. | |
| 192 | - | func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 193 | + | func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv port.WebView) { | |
| 193 | 194 | if c.faviconAdapter == nil || wv == nil { | |
| 194 | 195 | return | |
| 195 | 196 | } | |
| @@ -208,8 +209,14 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity | |||
| 208 | 209 | return | |
| 209 | 210 | } | |
| 210 | 211 | ||
| 212 | + | // Type-assert to access webkit-specific Favicon() and Generation() | |
| 213 | + | wkWV, ok := wv.(*webkit.WebView) | |
| 214 | + | if !ok { | |
| 215 | + | return | |
| 216 | + | } | |
| 217 | + | ||
| 211 | 218 | // Check if WebKit already has a favicon for this page | |
| 212 | - | if icon := wv.Favicon(); icon != nil { | |
| 219 | + | if icon := wkWV.Favicon(); icon != nil { | |
| 213 | 220 | log.Debug().Str("pane_id", string(paneID)).Str("uri", uri).Msg("using existing WebKit favicon for committed page") | |
| 214 | 221 | // Store and update through the normal path | |
| 215 | 222 | c.navOriginMu.RLock() | |
| @@ -222,7 +229,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity | |||
| 222 | 229 | ||
| 223 | 230 | // Fall back to GetOrFetch (checks service cache, WebKit DB, then DuckDuckGo API) | |
| 224 | 231 | // Capture current generation to guard against stale callbacks | |
| 225 | - | gen := wv.Generation() | |
| 232 | + | gen := wkWV.Generation() | |
| 226 | 233 | capturedURI := uri | |
| 227 | 234 | c.faviconAdapter.GetOrFetch(ctx, uri, func(texture *gdk.Texture) { | |
| 228 | 235 | // Skip nil results — a nil means "couldn't resolve", not "no favicon". | |
| @@ -232,7 +239,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity | |||
| 232 | 239 | return | |
| 233 | 240 | } | |
| 234 | 241 | // Verify WebView is still bound to pane and hasn't been reused | |
| 235 | - | if wv.Generation() != gen { | |
| 242 | + | if wkWV.Generation() != gen { | |
| 236 | 243 | return | |
| 237 | 244 | } | |
| 238 | 245 | currentWV := c.getWebViewLocked(paneID) | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "strings" | 5 | "strings" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 8 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 9 | "github.com/bnema/dumber/internal/logging" | 10 | "github.com/bnema/dumber/internal/logging" | ||
| @@ -115,7 +116,7 @@ func (c *Coordinator) syncStackedTitle(ctx context.Context, paneID entity.PaneID | |||||
| 115 | } | 116 | } | ||
| 116 | 117 | ||||
| 117 | // onFaviconChanged updates favicon tracking when a WebView's favicon changes. | 118 | // onFaviconChanged updates favicon tracking when a WebView's favicon changes. | ||
| 118 | - | func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV *webkit.WebView, favicon *gdk.Texture) { | 119 | + | func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV port.WebView, favicon *gdk.Texture) { |
| 119 | log := logging.FromContext(ctx) | 120 | log := logging.FromContext(ctx) | ||
| 120 | 121 | ||||
| 121 | // Verify this WebView is still bound to the expected pane | 122 | // Verify this WebView is still bound to the expected pane | ||
| @@ -189,7 +190,7 @@ func (c *Coordinator) updateStackedFaviconForPane(ctx context.Context, paneID en | |||||
| 189 | // First checks if WebKit already has a favicon for the page (common for subpath URLs). | 190 | // First checks if WebKit already has a favicon for the page (common for subpath URLs). | ||
| 190 | // Falls back to the full GetOrFetch pipeline (cache + WebKit DB + DuckDuckGo API). | 191 | // Falls back to the full GetOrFetch pipeline (cache + WebKit DB + DuckDuckGo API). | ||
| 191 | // This closes the gap where title bars relied solely on the notify::favicon signal. | 192 | // This closes the gap where title bars relied solely on the notify::favicon signal. | ||
| 192 | - | func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 193 | + | func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv port.WebView) { |
| 193 | if c.faviconAdapter == nil || wv == nil { | 194 | if c.faviconAdapter == nil || wv == nil { | ||
| 194 | return | 195 | return | ||
| 195 | } | 196 | } | ||
| @@ -208,8 +209,14 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity | |||||
| 208 | return | 209 | return | ||
| 209 | } | 210 | } | ||
| 210 | 211 | ||||
| 212 | + | // Type-assert to access webkit-specific Favicon() and Generation() | |||
| 213 | + | wkWV, ok := wv.(*webkit.WebView) | |||
| 214 | + | if !ok { | |||
| 215 | + | return | |||
| 216 | + | } | |||
| 217 | + | ||||
| 211 | // Check if WebKit already has a favicon for this page | 218 | // Check if WebKit already has a favicon for this page | ||
| 212 | - | if icon := wv.Favicon(); icon != nil { | 219 | + | if icon := wkWV.Favicon(); icon != nil { |
| 213 | log.Debug().Str("pane_id", string(paneID)).Str("uri", uri).Msg("using existing WebKit favicon for committed page") | 220 | log.Debug().Str("pane_id", string(paneID)).Str("uri", uri).Msg("using existing WebKit favicon for committed page") | ||
| 214 | // Store and update through the normal path | 221 | // Store and update through the normal path | ||
| 215 | c.navOriginMu.RLock() | 222 | c.navOriginMu.RLock() | ||
| @@ -222,7 +229,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity | |||||
| 222 | 229 | ||||
| 223 | // Fall back to GetOrFetch (checks service cache, WebKit DB, then DuckDuckGo API) | 230 | // Fall back to GetOrFetch (checks service cache, WebKit DB, then DuckDuckGo API) | ||
| 224 | // Capture current generation to guard against stale callbacks | 231 | // Capture current generation to guard against stale callbacks | ||
| 225 | - | gen := wv.Generation() | 232 | + | gen := wkWV.Generation() |
| 226 | capturedURI := uri | 233 | capturedURI := uri | ||
| 227 | c.faviconAdapter.GetOrFetch(ctx, uri, func(texture *gdk.Texture) { | 234 | c.faviconAdapter.GetOrFetch(ctx, uri, func(texture *gdk.Texture) { | ||
| 228 | // Skip nil results — a nil means "couldn't resolve", not "no favicon". | 235 | // Skip nil results — a nil means "couldn't resolve", not "no favicon". | ||
| @@ -232,7 +239,7 @@ func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity | |||||
| 232 | return | 239 | return | ||
| 233 | } | 240 | } | ||
| 234 | // Verify WebView is still bound to pane and hasn't been reused | 241 | // Verify WebView is still bound to pane and hasn't been reused | ||
| 235 | - | if wv.Generation() != gen { | 242 | + | if wkWV.Generation() != gen { |
| 236 | return | 243 | return | ||
| 237 | } | 244 | } | ||
| 238 | currentWV := c.getWebViewLocked(paneID) | 245 | currentWV := c.getWebViewLocked(paneID) | ||