Speed up CEF repaint after live resize
- commit
- 6b42329cec503b516eabb61da383216986cea504
- author
- brice · · 2026-05-11 07:01 UTC
- parent
- 013796f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +145 -35
M internal/infrastructure/cef/factory.go +3 -17
| @@ -177,27 +177,13 @@ func (f *WebViewFactory) configureInitialBrowserCreation( | |||
| 177 | 177 | return | |
| 178 | 178 | } | |
| 179 | 179 | ||
| 180 | - | wv.mu.RLock() | |
| 181 | - | host := wv.host | |
| 182 | - | wv.mu.RUnlock() | |
| 183 | - | if host == nil { | |
| 184 | - | logging.FromContext(ctx).Debug(). | |
| 185 | - | Uint64("webview_id", uint64(wv.id)). | |
| 186 | - | Int32("resize_width", w). | |
| 187 | - | Int32("resize_height", h). | |
| 188 | - | Bool("host_nil", true). | |
| 189 | - | Bool("browser_ready", false). | |
| 190 | - | Msg("cef: resize observed before browser host existed") | |
| 191 | - | return | |
| 192 | - | } | |
| 193 | - | notifyBrowserResize(host) | |
| 180 | + | synced := wv.syncResizeViewportOnGTK(ctx, "gtk-size-observer") | |
| 194 | 181 | logging.FromContext(ctx).Debug(). | |
| 195 | 182 | Uint64("webview_id", uint64(wv.id)). | |
| 196 | 183 | Int32("resize_width", w). | |
| 197 | 184 | Int32("resize_height", h). | |
| 198 | - | Bool("host_nil", false). | |
| 199 | - | Bool("browser_ready", true). | |
| 200 | - | Msg("cef: browser host WasResized invoked") | |
| 185 | + | Bool("browser_ready", synced). | |
| 186 | + | Msg("cef: viewport sync handled after GTK size change") | |
| 201 | 187 | }) | |
| 202 | 188 | }) | |
| 203 | 189 | } | |
| @@ -177,27 +177,13 @@ func (f *WebViewFactory) configureInitialBrowserCreation( | |||||
| 177 | return | 177 | return | ||
| 178 | } | 178 | } | ||
| 179 | 179 | ||||
| 180 | - | wv.mu.RLock() | 180 | + | synced := wv.syncResizeViewportOnGTK(ctx, "gtk-size-observer") |
| 181 | - | host := wv.host | |||
| 182 | - | wv.mu.RUnlock() | |||
| 183 | - | if host == nil { | |||
| 184 | - | logging.FromContext(ctx).Debug(). | |||
| 185 | - | Uint64("webview_id", uint64(wv.id)). | |||
| 186 | - | Int32("resize_width", w). | |||
| 187 | - | Int32("resize_height", h). | |||
| 188 | - | Bool("host_nil", true). | |||
| 189 | - | Bool("browser_ready", false). | |||
| 190 | - | Msg("cef: resize observed before browser host existed") | |||
| 191 | - | return | |||
| 192 | - | } | |||
| 193 | - | notifyBrowserResize(host) | |||
| 194 | logging.FromContext(ctx).Debug(). | 181 | logging.FromContext(ctx).Debug(). | ||
| 195 | Uint64("webview_id", uint64(wv.id)). | 182 | Uint64("webview_id", uint64(wv.id)). | ||
| 196 | Int32("resize_width", w). | 183 | Int32("resize_width", w). | ||
| 197 | Int32("resize_height", h). | 184 | Int32("resize_height", h). | ||
| 198 | - | Bool("host_nil", false). | 185 | + | Bool("browser_ready", synced). |
| 199 | - | Bool("browser_ready", true). | 186 | + | Msg("cef: viewport sync handled after GTK size change") |
| 200 | - | Msg("cef: browser host WasResized invoked") | |||
| 201 | }) | 187 | }) | ||
| 202 | }) | 188 | }) | ||
| 203 | } | 189 | } | ||
M internal/infrastructure/cef/webview.go +12 -10
| @@ -90,13 +90,17 @@ type WebView struct { | |||
| 90 | 90 | previousProfileSnapshot cef2gtk.ProfileSnapshot | |
| 91 | 91 | latestProfileSnapshotAt time.Time | |
| 92 | 92 | ||
| 93 | - | removeSizeObserver func() | |
| 94 | - | viewportSyncMu sync.Mutex | |
| 95 | - | viewportSyncPending bool | |
| 96 | - | viewportSyncReason string | |
| 97 | - | viewportMapFunc func(gtk.Widget) | |
| 98 | - | viewportShowFunc func(gtk.Widget) | |
| 99 | - | viewportRealizeFunc func(gtk.Widget) | |
| 93 | + | removeSizeObserver func() | |
| 94 | + | viewportSyncMu sync.Mutex | |
| 95 | + | viewportSyncPending bool | |
| 96 | + | viewportSyncReason string | |
| 97 | + | viewportMapFunc func(gtk.Widget) | |
| 98 | + | viewportShowFunc func(gtk.Widget) | |
| 99 | + | viewportRealizeFunc func(gtk.Widget) | |
| 100 | + | viewportMapSignalID uint | |
| 101 | + | viewportShowSignalID uint | |
| 102 | + | viewportRealizeSignalID uint | |
| 103 | + | viewportResizePulseSeq atomic.Uint64 | |
| 100 | 104 | ||
| 101 | 105 | // beginFrameTick drives CEF external BeginFrame requests while the GTK | |
| 102 | 106 | // widget is visible. Access is guarded by mu. | |
| @@ -1084,9 +1088,7 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||
| 1084 | 1088 | wv.removeSizeObserver() | |
| 1085 | 1089 | wv.removeSizeObserver = nil | |
| 1086 | 1090 | } | |
| 1087 | - | wv.viewportMapFunc = nil | |
| 1088 | - | wv.viewportShowFunc = nil | |
| 1089 | - | wv.viewportRealizeFunc = nil | |
| 1091 | + | wv.disconnectViewportSyncHooksOnGTKThread() | |
| 1090 | 1092 | _ = wv.viewBridge.DetachInput() | |
| 1091 | 1093 | _ = wv.viewBridge.Destroy() | |
| 1092 | 1094 | wv.viewBridge = nil | |
| @@ -90,13 +90,17 @@ type WebView struct { | |||||
| 90 | previousProfileSnapshot cef2gtk.ProfileSnapshot | 90 | previousProfileSnapshot cef2gtk.ProfileSnapshot | ||
| 91 | latestProfileSnapshotAt time.Time | 91 | latestProfileSnapshotAt time.Time | ||
| 92 | 92 | ||||
| 93 | - | removeSizeObserver func() | 93 | + | removeSizeObserver func() |
| 94 | - | viewportSyncMu sync.Mutex | 94 | + | viewportSyncMu sync.Mutex |
| 95 | - | viewportSyncPending bool | 95 | + | viewportSyncPending bool |
| 96 | - | viewportSyncReason string | 96 | + | viewportSyncReason string |
| 97 | - | viewportMapFunc func(gtk.Widget) | 97 | + | viewportMapFunc func(gtk.Widget) |
| 98 | - | viewportShowFunc func(gtk.Widget) | 98 | + | viewportShowFunc func(gtk.Widget) |
| 99 | - | viewportRealizeFunc func(gtk.Widget) | 99 | + | viewportRealizeFunc func(gtk.Widget) |
| 100 | + | viewportMapSignalID uint | |||
| 101 | + | viewportShowSignalID uint | |||
| 102 | + | viewportRealizeSignalID uint | |||
| 103 | + | viewportResizePulseSeq atomic.Uint64 | |||
| 100 | 104 | ||||
| 101 | // beginFrameTick drives CEF external BeginFrame requests while the GTK | 105 | // beginFrameTick drives CEF external BeginFrame requests while the GTK | ||
| 102 | // widget is visible. Access is guarded by mu. | 106 | // widget is visible. Access is guarded by mu. | ||
| @@ -1084,9 +1088,7 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||||
| 1084 | wv.removeSizeObserver() | 1088 | wv.removeSizeObserver() | ||
| 1085 | wv.removeSizeObserver = nil | 1089 | wv.removeSizeObserver = nil | ||
| 1086 | } | 1090 | } | ||
| 1087 | - | wv.viewportMapFunc = nil | 1091 | + | wv.disconnectViewportSyncHooksOnGTKThread() |
| 1088 | - | wv.viewportShowFunc = nil | |||
| 1089 | - | wv.viewportRealizeFunc = nil | |||
| 1090 | _ = wv.viewBridge.DetachInput() | 1092 | _ = wv.viewBridge.DetachInput() | ||
| 1091 | _ = wv.viewBridge.Destroy() | 1093 | _ = wv.viewBridge.Destroy() | ||
| 1092 | wv.viewBridge = nil | 1094 | wv.viewBridge = nil | ||
M internal/infrastructure/cef/webview_viewport.go +89 -8
| @@ -4,8 +4,11 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "strings" | |
| 6 | 6 | ||
| 7 | - | "github.com/bnema/dumber/internal/logging" | |
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | "github.com/bnema/puregotk/v4/gobject" | |
| 8 | 9 | "github.com/bnema/puregotk/v4/gtk" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/dumber/internal/logging" | |
| 9 | 12 | ) | |
| 10 | 13 | ||
| 11 | 14 | type viewportSyncBrowserHost interface { | |
| @@ -66,13 +69,23 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) { | |||
| 66 | 69 | if wv == nil { | |
| 67 | 70 | return | |
| 68 | 71 | } | |
| 69 | - | reason := wv.takeViewportSyncReason() | |
| 72 | + | wv.syncViewportNowOnGTK(ctx, wv.takeViewportSyncReason()) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool { | |
| 76 | + | if wv == nil { | |
| 77 | + | return false | |
| 78 | + | } | |
| 79 | + | reason = strings.TrimSpace(reason) | |
| 80 | + | if reason == "" { | |
| 81 | + | reason = "manual" | |
| 82 | + | } | |
| 70 | 83 | if wv.destroyed.Load() || wv.viewBridge == nil { | |
| 71 | - | return | |
| 84 | + | return false | |
| 72 | 85 | } | |
| 73 | 86 | ||
| 74 | 87 | if wv.tryStartPendingBrowserCreateOnGTKThread(ctx, reason) { | |
| 75 | - | return | |
| 88 | + | return false | |
| 76 | 89 | } | |
| 77 | 90 | ||
| 78 | 91 | wv.mu.RLock() | |
| @@ -83,7 +96,7 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) { | |||
| 83 | 96 | Uint64("webview_id", uint64(wv.id)). | |
| 84 | 97 | Str("reason", reason). | |
| 85 | 98 | Msg("cef: viewport sync skipped; browser host not ready") | |
| 86 | - | return | |
| 99 | + | return false | |
| 87 | 100 | } | |
| 88 | 101 | ||
| 89 | 102 | widget := wv.viewBridge.Widget() | |
| @@ -94,6 +107,46 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) { | |||
| 94 | 107 | Bool("visible", visible). | |
| 95 | 108 | Str("reason", reason). | |
| 96 | 109 | Msg("cef: viewport sync requested") | |
| 110 | + | return true | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func (wv *WebView) syncResizeViewportOnGTK(ctx context.Context, reason string) bool { | |
| 114 | + | if !wv.syncViewportNowOnGTK(ctx, reason) { | |
| 115 | + | return false | |
| 116 | + | } | |
| 117 | + | wv.scheduleResizeRepaintPulse(ctx, reason) | |
| 118 | + | return true | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string) { | |
| 122 | + | if wv == nil || wv.destroyed.Load() { | |
| 123 | + | return | |
| 124 | + | } | |
| 125 | + | seq := wv.viewportResizePulseSeq.Add(1) | |
| 126 | + | for _, delayMs := range [...]int64{16, 48} { | |
| 127 | + | delayMs := delayMs | |
| 128 | + | task := cefNewTask(cefTaskFunc(func() { | |
| 129 | + | if wv == nil || wv.destroyed.Load() || wv.viewportResizePulseSeq.Load() != seq { | |
| 130 | + | return | |
| 131 | + | } | |
| 132 | + | wv.mu.RLock() | |
| 133 | + | host := wv.host | |
| 134 | + | wv.mu.RUnlock() | |
| 135 | + | if host == nil { | |
| 136 | + | return | |
| 137 | + | } | |
| 138 | + | host.Invalidate(purecef.PaintElementTypePetView) | |
| 139 | + | logging.FromContext(ctx).Debug(). | |
| 140 | + | Uint64("webview_id", uint64(wv.id)). | |
| 141 | + | Int64("delay_ms", delayMs). | |
| 142 | + | Str("reason", reason). | |
| 143 | + | Msg("cef: delayed resize repaint pulse") | |
| 144 | + | })) | |
| 145 | + | if task == nil { | |
| 146 | + | continue | |
| 147 | + | } | |
| 148 | + | cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs) | |
| 149 | + | } | |
| 97 | 150 | } | |
| 98 | 151 | ||
| 99 | 152 | func (wv *WebView) tryStartPendingBrowserCreateOnGTKThread(ctx context.Context, reason string) bool { | |
| @@ -179,9 +232,37 @@ func (wv *WebView) installViewportSyncHooksOnGTKThread() { | |||
| 179 | 232 | wv.SyncViewport(wv.ctx, "gtk-realize") | |
| 180 | 233 | } | |
| 181 | 234 | ||
| 182 | - | widget.ConnectMap(&wv.viewportMapFunc) | |
| 183 | - | widget.ConnectShow(&wv.viewportShowFunc) | |
| 184 | - | widget.ConnectRealize(&wv.viewportRealizeFunc) | |
| 235 | + | wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc) | |
| 236 | + | wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc) | |
| 237 | + | wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc) | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |
| 241 | + | if wv == nil || wv.viewBridge == nil { | |
| 242 | + | return | |
| 243 | + | } | |
| 244 | + | widget := wv.viewBridge.Widget() | |
| 245 | + | if widget != nil { | |
| 246 | + | widgetPtr := widget.GoPointer() | |
| 247 | + | if widgetPtr != 0 { | |
| 248 | + | obj := gobject.ObjectNewFromInternalPtr(widgetPtr) | |
| 249 | + | if wv.viewportMapSignalID != 0 { | |
| 250 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportMapSignalID) | |
| 251 | + | wv.viewportMapSignalID = 0 | |
| 252 | + | } | |
| 253 | + | if wv.viewportShowSignalID != 0 { | |
| 254 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID) | |
| 255 | + | wv.viewportShowSignalID = 0 | |
| 256 | + | } | |
| 257 | + | if wv.viewportRealizeSignalID != 0 { | |
| 258 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID) | |
| 259 | + | wv.viewportRealizeSignalID = 0 | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | } | |
| 263 | + | wv.viewportMapFunc = nil | |
| 264 | + | wv.viewportShowFunc = nil | |
| 265 | + | wv.viewportRealizeFunc = nil | |
| 185 | 266 | } | |
| 186 | 267 | ||
| 187 | 268 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | |
| @@ -4,8 +4,11 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "strings" | 5 | "strings" | ||
| 6 | 6 | ||||
| 7 | - | "github.com/bnema/dumber/internal/logging" | 7 | + | purecef "github.com/bnema/purego-cef/cef" |
| 8 | + | "github.com/bnema/puregotk/v4/gobject" | |||
| 8 | "github.com/bnema/puregotk/v4/gtk" | 9 | "github.com/bnema/puregotk/v4/gtk" | ||
| 10 | + | ||||
| 11 | + | "github.com/bnema/dumber/internal/logging" | |||
| 9 | ) | 12 | ) | ||
| 10 | 13 | ||||
| 11 | type viewportSyncBrowserHost interface { | 14 | type viewportSyncBrowserHost interface { | ||
| @@ -66,13 +69,23 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) { | |||||
| 66 | if wv == nil { | 69 | if wv == nil { | ||
| 67 | return | 70 | return | ||
| 68 | } | 71 | } | ||
| 69 | - | reason := wv.takeViewportSyncReason() | 72 | + | wv.syncViewportNowOnGTK(ctx, wv.takeViewportSyncReason()) |
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool { | |||
| 76 | + | if wv == nil { | |||
| 77 | + | return false | |||
| 78 | + | } | |||
| 79 | + | reason = strings.TrimSpace(reason) | |||
| 80 | + | if reason == "" { | |||
| 81 | + | reason = "manual" | |||
| 82 | + | } | |||
| 70 | if wv.destroyed.Load() || wv.viewBridge == nil { | 83 | if wv.destroyed.Load() || wv.viewBridge == nil { | ||
| 71 | - | return | 84 | + | return false |
| 72 | } | 85 | } | ||
| 73 | 86 | ||||
| 74 | if wv.tryStartPendingBrowserCreateOnGTKThread(ctx, reason) { | 87 | if wv.tryStartPendingBrowserCreateOnGTKThread(ctx, reason) { | ||
| 75 | - | return | 88 | + | return false |
| 76 | } | 89 | } | ||
| 77 | 90 | ||||
| 78 | wv.mu.RLock() | 91 | wv.mu.RLock() | ||
| @@ -83,7 +96,7 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) { | |||||
| 83 | Uint64("webview_id", uint64(wv.id)). | 96 | Uint64("webview_id", uint64(wv.id)). | ||
| 84 | Str("reason", reason). | 97 | Str("reason", reason). | ||
| 85 | Msg("cef: viewport sync skipped; browser host not ready") | 98 | Msg("cef: viewport sync skipped; browser host not ready") | ||
| 86 | - | return | 99 | + | return false |
| 87 | } | 100 | } | ||
| 88 | 101 | ||||
| 89 | widget := wv.viewBridge.Widget() | 102 | widget := wv.viewBridge.Widget() | ||
| @@ -94,6 +107,46 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) { | |||||
| 94 | Bool("visible", visible). | 107 | Bool("visible", visible). | ||
| 95 | Str("reason", reason). | 108 | Str("reason", reason). | ||
| 96 | Msg("cef: viewport sync requested") | 109 | Msg("cef: viewport sync requested") | ||
| 110 | + | return true | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func (wv *WebView) syncResizeViewportOnGTK(ctx context.Context, reason string) bool { | |||
| 114 | + | if !wv.syncViewportNowOnGTK(ctx, reason) { | |||
| 115 | + | return false | |||
| 116 | + | } | |||
| 117 | + | wv.scheduleResizeRepaintPulse(ctx, reason) | |||
| 118 | + | return true | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string) { | |||
| 122 | + | if wv == nil || wv.destroyed.Load() { | |||
| 123 | + | return | |||
| 124 | + | } | |||
| 125 | + | seq := wv.viewportResizePulseSeq.Add(1) | |||
| 126 | + | for _, delayMs := range [...]int64{16, 48} { | |||
| 127 | + | delayMs := delayMs | |||
| 128 | + | task := cefNewTask(cefTaskFunc(func() { | |||
| 129 | + | if wv == nil || wv.destroyed.Load() || wv.viewportResizePulseSeq.Load() != seq { | |||
| 130 | + | return | |||
| 131 | + | } | |||
| 132 | + | wv.mu.RLock() | |||
| 133 | + | host := wv.host | |||
| 134 | + | wv.mu.RUnlock() | |||
| 135 | + | if host == nil { | |||
| 136 | + | return | |||
| 137 | + | } | |||
| 138 | + | host.Invalidate(purecef.PaintElementTypePetView) | |||
| 139 | + | logging.FromContext(ctx).Debug(). | |||
| 140 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 141 | + | Int64("delay_ms", delayMs). | |||
| 142 | + | Str("reason", reason). | |||
| 143 | + | Msg("cef: delayed resize repaint pulse") | |||
| 144 | + | })) | |||
| 145 | + | if task == nil { | |||
| 146 | + | continue | |||
| 147 | + | } | |||
| 148 | + | cefPostDelayedTask(purecef.ThreadIDTidUi, task, delayMs) | |||
| 149 | + | } | |||
| 97 | } | 150 | } | ||
| 98 | 151 | ||||
| 99 | func (wv *WebView) tryStartPendingBrowserCreateOnGTKThread(ctx context.Context, reason string) bool { | 152 | func (wv *WebView) tryStartPendingBrowserCreateOnGTKThread(ctx context.Context, reason string) bool { | ||
| @@ -179,9 +232,37 @@ func (wv *WebView) installViewportSyncHooksOnGTKThread() { | |||||
| 179 | wv.SyncViewport(wv.ctx, "gtk-realize") | 232 | wv.SyncViewport(wv.ctx, "gtk-realize") | ||
| 180 | } | 233 | } | ||
| 181 | 234 | ||||
| 182 | - | widget.ConnectMap(&wv.viewportMapFunc) | 235 | + | wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc) |
| 183 | - | widget.ConnectShow(&wv.viewportShowFunc) | 236 | + | wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc) |
| 184 | - | widget.ConnectRealize(&wv.viewportRealizeFunc) | 237 | + | wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc) |
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |||
| 241 | + | if wv == nil || wv.viewBridge == nil { | |||
| 242 | + | return | |||
| 243 | + | } | |||
| 244 | + | widget := wv.viewBridge.Widget() | |||
| 245 | + | if widget != nil { | |||
| 246 | + | widgetPtr := widget.GoPointer() | |||
| 247 | + | if widgetPtr != 0 { | |||
| 248 | + | obj := gobject.ObjectNewFromInternalPtr(widgetPtr) | |||
| 249 | + | if wv.viewportMapSignalID != 0 { | |||
| 250 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportMapSignalID) | |||
| 251 | + | wv.viewportMapSignalID = 0 | |||
| 252 | + | } | |||
| 253 | + | if wv.viewportShowSignalID != 0 { | |||
| 254 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID) | |||
| 255 | + | wv.viewportShowSignalID = 0 | |||
| 256 | + | } | |||
| 257 | + | if wv.viewportRealizeSignalID != 0 { | |||
| 258 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID) | |||
| 259 | + | wv.viewportRealizeSignalID = 0 | |||
| 260 | + | } | |||
| 261 | + | } | |||
| 262 | + | } | |||
| 263 | + | wv.viewportMapFunc = nil | |||
| 264 | + | wv.viewportShowFunc = nil | |||
| 265 | + | wv.viewportRealizeFunc = nil | |||
| 185 | } | 266 | } | ||
| 186 | 267 | ||||
| 187 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | 268 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | ||
M internal/infrastructure/cef/webview_viewport_test.go +40 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package cef | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "testing" | |
| 5 | 6 | ||
| 6 | 7 | purecef "github.com/bnema/purego-cef/cef" | |
| @@ -8,6 +9,7 @@ import ( | |||
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 11 | type viewportSyncOrderHost struct { | |
| 12 | + | purecef.BrowserHost | |
| 11 | 13 | calls []string | |
| 12 | 14 | } | |
| 13 | 15 | ||
| @@ -45,3 +47,41 @@ func TestNotifyBrowserViewportSync_HiddenSkipsWasHidden(t *testing.T) { | |||
| 45 | 47 | ||
| 46 | 48 | require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls) | |
| 47 | 49 | } | |
| 50 | + | ||
| 51 | + | func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) { | |
| 52 | + | oldNewTask := cefNewTask | |
| 53 | + | oldPostDelayedTask := cefPostDelayedTask | |
| 54 | + | defer func() { | |
| 55 | + | cefNewTask = oldNewTask | |
| 56 | + | cefPostDelayedTask = oldPostDelayedTask | |
| 57 | + | }() | |
| 58 | + | ||
| 59 | + | cefNewTask = func(task purecef.Task) purecef.Task { return task } | |
| 60 | + | ||
| 61 | + | var scheduled []purecef.Task | |
| 62 | + | var delays []int64 | |
| 63 | + | cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 { | |
| 64 | + | require.Equal(t, purecef.ThreadIDTidUi, threadID) | |
| 65 | + | require.NotNil(t, task) | |
| 66 | + | scheduled = append(scheduled, task) | |
| 67 | + | delays = append(delays, delayMs) | |
| 68 | + | return 1 | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | host := &viewportSyncOrderHost{} | |
| 72 | + | wv := &WebView{ctx: context.Background(), host: host} | |
| 73 | + | ||
| 74 | + | wv.scheduleResizeRepaintPulse(context.Background(), "first") | |
| 75 | + | wv.scheduleResizeRepaintPulse(context.Background(), "second") | |
| 76 | + | ||
| 77 | + | require.Equal(t, []int64{16, 48, 16, 48}, delays) | |
| 78 | + | require.Len(t, scheduled, 4) | |
| 79 | + | ||
| 80 | + | scheduled[0].Execute() | |
| 81 | + | scheduled[1].Execute() | |
| 82 | + | require.Empty(t, host.calls) | |
| 83 | + | ||
| 84 | + | scheduled[2].Execute() | |
| 85 | + | scheduled[3].Execute() | |
| 86 | + | require.Equal(t, []string{"Invalidate", "Invalidate"}, host.calls) | |
| 87 | + | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package cef | 1 | package cef | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "testing" | 5 | "testing" | ||
| 5 | 6 | ||||
| 6 | purecef "github.com/bnema/purego-cef/cef" | 7 | purecef "github.com/bnema/purego-cef/cef" | ||
| @@ -8,6 +9,7 @@ import ( | |||||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| 10 | type viewportSyncOrderHost struct { | 11 | type viewportSyncOrderHost struct { | ||
| 12 | + | purecef.BrowserHost | |||
| 11 | calls []string | 13 | calls []string | ||
| 12 | } | 14 | } | ||
| 13 | 15 | ||||
| @@ -45,3 +47,41 @@ func TestNotifyBrowserViewportSync_HiddenSkipsWasHidden(t *testing.T) { | |||||
| 45 | 47 | ||||
| 46 | require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls) | 48 | require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls) | ||
| 47 | } | 49 | } | ||
| 50 | + | ||||
| 51 | + | func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) { | |||
| 52 | + | oldNewTask := cefNewTask | |||
| 53 | + | oldPostDelayedTask := cefPostDelayedTask | |||
| 54 | + | defer func() { | |||
| 55 | + | cefNewTask = oldNewTask | |||
| 56 | + | cefPostDelayedTask = oldPostDelayedTask | |||
| 57 | + | }() | |||
| 58 | + | ||||
| 59 | + | cefNewTask = func(task purecef.Task) purecef.Task { return task } | |||
| 60 | + | ||||
| 61 | + | var scheduled []purecef.Task | |||
| 62 | + | var delays []int64 | |||
| 63 | + | cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 { | |||
| 64 | + | require.Equal(t, purecef.ThreadIDTidUi, threadID) | |||
| 65 | + | require.NotNil(t, task) | |||
| 66 | + | scheduled = append(scheduled, task) | |||
| 67 | + | delays = append(delays, delayMs) | |||
| 68 | + | return 1 | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | host := &viewportSyncOrderHost{} | |||
| 72 | + | wv := &WebView{ctx: context.Background(), host: host} | |||
| 73 | + | ||||
| 74 | + | wv.scheduleResizeRepaintPulse(context.Background(), "first") | |||
| 75 | + | wv.scheduleResizeRepaintPulse(context.Background(), "second") | |||
| 76 | + | ||||
| 77 | + | require.Equal(t, []int64{16, 48, 16, 48}, delays) | |||
| 78 | + | require.Len(t, scheduled, 4) | |||
| 79 | + | ||||
| 80 | + | scheduled[0].Execute() | |||
| 81 | + | scheduled[1].Execute() | |||
| 82 | + | require.Empty(t, host.calls) | |||
| 83 | + | ||||
| 84 | + | scheduled[2].Execute() | |||
| 85 | + | scheduled[3].Execute() | |||
| 86 | + | require.Equal(t, []string{"Invalidate", "Invalidate"}, host.calls) | |||
| 87 | + | } | |||
M internal/ui/app.go +1 -0
| @@ -3821,6 +3821,7 @@ func (a *App) attachPopupToTab(ctx context.Context, tabID entity.TabID, pane *en | |||
| 3821 | 3821 | if widget != nil { | |
| 3822 | 3822 | if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil { | |
| 3823 | 3823 | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("pane view not found for popup") | |
| 3824 | + | return | |
| 3824 | 3825 | } | |
| 3825 | 3826 | } | |
| 3826 | 3827 | } | |
| @@ -3821,6 +3821,7 @@ func (a *App) attachPopupToTab(ctx context.Context, tabID entity.TabID, pane *en | |||||
| 3821 | if widget != nil { | 3821 | if widget != nil { | ||
| 3822 | if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil { | 3822 | if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil { | ||
| 3823 | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("pane view not found for popup") | 3823 | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("pane view not found for popup") | ||
| 3824 | + | return | |||
| 3824 | } | 3825 | } | ||
| 3825 | } | 3826 | } | ||
| 3826 | } | 3827 | } | ||