fix: support fractional scale CEF OSR
- commit
- b6c23e2b56b61f76d778b67ea744bbdc7a220f58
- author
- brice · · 2026-05-16 14:58 UTC
- committer
- brice · · 2026-05-16 15:01 UTC
- parent
- 53b9850
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
20 files · +239 -86
M go.mod +1 -1
| @@ -7,7 +7,7 @@ require ( | |||
| 7 | 7 | github.com/andybalholm/brotli v1.2.1 | |
| 8 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | |
| 9 | 9 | github.com/bnema/purego-cef v0.13.0 | |
| 10 | - | github.com/bnema/purego-cef2gtk v0.1.1 | |
| 10 | + | github.com/bnema/purego-cef2gtk v0.2.0 | |
| 11 | 11 | github.com/bnema/purego-pipewire v0.1.3 | |
| 12 | 12 | github.com/bnema/purego-sqlite v0.1.2 | |
| 13 | 13 | github.com/bnema/puregotk v0.5.2 | |
| @@ -7,7 +7,7 @@ require ( | |||||
| 7 | github.com/andybalholm/brotli v1.2.1 | 7 | github.com/andybalholm/brotli v1.2.1 | ||
| 8 | github.com/bnema/purego v0.11.0-bnema.2 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | ||
| 9 | github.com/bnema/purego-cef v0.13.0 | 9 | github.com/bnema/purego-cef v0.13.0 | ||
| 10 | - | github.com/bnema/purego-cef2gtk v0.1.1 | 10 | + | github.com/bnema/purego-cef2gtk v0.2.0 |
| 11 | github.com/bnema/purego-pipewire v0.1.3 | 11 | github.com/bnema/purego-pipewire v0.1.3 | ||
| 12 | github.com/bnema/purego-sqlite v0.1.2 | 12 | github.com/bnema/purego-sqlite v0.1.2 | ||
| 13 | github.com/bnema/puregotk v0.5.2 | 13 | github.com/bnema/puregotk v0.5.2 | ||
M go.sum +2 -2 generated file: collapsed
M internal/infrastructure/cef/cef2gtk_adapter.go +15 -0
| @@ -107,6 +107,21 @@ func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 { | |||
| 107 | 107 | return a.view.DeviceScaleFactor() | |
| 108 | 108 | } | |
| 109 | 109 | ||
| 110 | + | // OSRBackingScaleFactor returns the device backing scale used for CEF OSR | |
| 111 | + | // coordinates. It returns 1 when purego-cef2gtk is using CEF's normal logical | |
| 112 | + | // OSR contract. | |
| 113 | + | func (a *Cef2gtkAdapter) OSRBackingScaleFactor() float64 { | |
| 114 | + | if a == nil || a.destroyed.Load() { | |
| 115 | + | return 1 | |
| 116 | + | } | |
| 117 | + | a.viewMu.RLock() | |
| 118 | + | defer a.viewMu.RUnlock() | |
| 119 | + | if a.view == nil { | |
| 120 | + | return 1 | |
| 121 | + | } | |
| 122 | + | return cef2gtk.OSRBackingScaleFactorForScale(float64(a.view.DeviceScaleFactor())) | |
| 123 | + | } | |
| 124 | + | ||
| 110 | 125 | // AddSizeObserver registers a positive-size change callback. Register and | |
| 111 | 126 | // unregister from the GTK main thread; callbacks are invoked by the bridge from | |
| 112 | 127 | // GTK size notifications. | |
| @@ -107,6 +107,21 @@ func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 { | |||||
| 107 | return a.view.DeviceScaleFactor() | 107 | return a.view.DeviceScaleFactor() | ||
| 108 | } | 108 | } | ||
| 109 | 109 | ||||
| 110 | + | // OSRBackingScaleFactor returns the device backing scale used for CEF OSR | |||
| 111 | + | // coordinates. It returns 1 when purego-cef2gtk is using CEF's normal logical | |||
| 112 | + | // OSR contract. | |||
| 113 | + | func (a *Cef2gtkAdapter) OSRBackingScaleFactor() float64 { | |||
| 114 | + | if a == nil || a.destroyed.Load() { | |||
| 115 | + | return 1 | |||
| 116 | + | } | |||
| 117 | + | a.viewMu.RLock() | |||
| 118 | + | defer a.viewMu.RUnlock() | |||
| 119 | + | if a.view == nil { | |||
| 120 | + | return 1 | |||
| 121 | + | } | |||
| 122 | + | return cef2gtk.OSRBackingScaleFactorForScale(float64(a.view.DeviceScaleFactor())) | |||
| 123 | + | } | |||
| 124 | + | ||||
| 110 | // AddSizeObserver registers a positive-size change callback. Register and | 125 | // AddSizeObserver registers a positive-size change callback. Register and | ||
| 111 | // unregister from the GTK main thread; callbacks are invoked by the bridge from | 126 | // unregister from the GTK main thread; callbacks are invoked by the bridge from | ||
| 112 | // GTK size notifications. | 127 | // GTK size notifications. | ||
M internal/infrastructure/cef/context_menu.go +7 -6
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | purecef "github.com/bnema/purego-cef/cef" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/dumber/internal/application/port" | |
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/gtkutil" | |
| 11 | 12 | "github.com/bnema/dumber/internal/logging" | |
| 12 | 13 | "github.com/bnema/puregotk/v4/gtk" | |
| 13 | 14 | ) | |
| @@ -79,7 +80,7 @@ func (h *handlerSet) RunContextMenu( | |||
| 79 | 80 | return 0 | |
| 80 | 81 | } | |
| 81 | 82 | ||
| 82 | - | x, y := contextMenuAnchorPosition(params, h.wv.viewBridgeScale()) | |
| 83 | + | x, y := contextMenuAnchorPosition(params, h.wv.viewBridgeScale(), h.wv.osrBackingScaleFactor()) | |
| 83 | 84 | executor := h.contextMenuExecutor() | |
| 84 | 85 | wv := h.wv | |
| 85 | 86 | wv.runOnGTK(func() { | |
| @@ -126,14 +127,14 @@ func (h *handlerSet) contextMenuExecutor() port.ContextMenuActionExecutor { | |||
| 126 | 127 | ) | |
| 127 | 128 | } | |
| 128 | 129 | ||
| 129 | - | func contextMenuAnchorPosition(params purecef.ContextMenuParams, scale int32) (int32, int32) { | |
| 130 | + | func contextMenuAnchorPosition(params purecef.ContextMenuParams, scale, backingScale float64) (int32, int32) { | |
| 130 | 131 | if params == nil { | |
| 131 | 132 | return 0, 0 | |
| 132 | 133 | } | |
| 133 | - | if scale <= 0 { | |
| 134 | - | scale = 1 | |
| 134 | + | if backingScale <= 1 { | |
| 135 | + | return params.GetXcoord(), params.GetYcoord() | |
| 135 | 136 | } | |
| 136 | - | return params.GetXcoord() / scale, params.GetYcoord() / scale | |
| 137 | + | return gtkutil.DeviceToLogical(params.GetXcoord(), scale), gtkutil.DeviceToLogical(params.GetYcoord(), scale) | |
| 137 | 138 | } | |
| 138 | 139 | ||
| 139 | 140 | func logContextMenuPopupRequest( | |
| @@ -152,7 +153,7 @@ func logContextMenuPopupRequest( | |||
| 152 | 153 | Int32("raw_y", rawY). | |
| 153 | 154 | Int32("popup_x", x). | |
| 154 | 155 | Int32("popup_y", y). | |
| 155 | - | Int32("scale", h.wv.viewBridgeScale()). | |
| 156 | + | Float64("scale", h.wv.viewBridgeScale()). | |
| 156 | 157 | Int("anchor_width", widget.GetAllocatedWidth()). | |
| 157 | 158 | Int("anchor_height", widget.GetAllocatedHeight()). | |
| 158 | 159 | Int("parent_width", cefWidgetAllocatedWidth(parent)). | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | purecef "github.com/bnema/purego-cef/cef" | 8 | purecef "github.com/bnema/purego-cef/cef" | ||
| 9 | 9 | ||||
| 10 | "github.com/bnema/dumber/internal/application/port" | 10 | "github.com/bnema/dumber/internal/application/port" | ||
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/gtkutil" | |||
| 11 | "github.com/bnema/dumber/internal/logging" | 12 | "github.com/bnema/dumber/internal/logging" | ||
| 12 | "github.com/bnema/puregotk/v4/gtk" | 13 | "github.com/bnema/puregotk/v4/gtk" | ||
| 13 | ) | 14 | ) | ||
| @@ -79,7 +80,7 @@ func (h *handlerSet) RunContextMenu( | |||||
| 79 | return 0 | 80 | return 0 | ||
| 80 | } | 81 | } | ||
| 81 | 82 | ||||
| 82 | - | x, y := contextMenuAnchorPosition(params, h.wv.viewBridgeScale()) | 83 | + | x, y := contextMenuAnchorPosition(params, h.wv.viewBridgeScale(), h.wv.osrBackingScaleFactor()) |
| 83 | executor := h.contextMenuExecutor() | 84 | executor := h.contextMenuExecutor() | ||
| 84 | wv := h.wv | 85 | wv := h.wv | ||
| 85 | wv.runOnGTK(func() { | 86 | wv.runOnGTK(func() { | ||
| @@ -126,14 +127,14 @@ func (h *handlerSet) contextMenuExecutor() port.ContextMenuActionExecutor { | |||||
| 126 | ) | 127 | ) | ||
| 127 | } | 128 | } | ||
| 128 | 129 | ||||
| 129 | - | func contextMenuAnchorPosition(params purecef.ContextMenuParams, scale int32) (int32, int32) { | 130 | + | func contextMenuAnchorPosition(params purecef.ContextMenuParams, scale, backingScale float64) (int32, int32) { |
| 130 | if params == nil { | 131 | if params == nil { | ||
| 131 | return 0, 0 | 132 | return 0, 0 | ||
| 132 | } | 133 | } | ||
| 133 | - | if scale <= 0 { | 134 | + | if backingScale <= 1 { |
| 134 | - | scale = 1 | 135 | + | return params.GetXcoord(), params.GetYcoord() |
| 135 | } | 136 | } | ||
| 136 | - | return params.GetXcoord() / scale, params.GetYcoord() / scale | 137 | + | return gtkutil.DeviceToLogical(params.GetXcoord(), scale), gtkutil.DeviceToLogical(params.GetYcoord(), scale) |
| 137 | } | 138 | } | ||
| 138 | 139 | ||||
| 139 | func logContextMenuPopupRequest( | 140 | func logContextMenuPopupRequest( | ||
| @@ -152,7 +153,7 @@ func logContextMenuPopupRequest( | |||||
| 152 | Int32("raw_y", rawY). | 153 | Int32("raw_y", rawY). | ||
| 153 | Int32("popup_x", x). | 154 | Int32("popup_x", x). | ||
| 154 | Int32("popup_y", y). | 155 | Int32("popup_y", y). | ||
| 155 | - | Int32("scale", h.wv.viewBridgeScale()). | 156 | + | Float64("scale", h.wv.viewBridgeScale()). |
| 156 | Int("anchor_width", widget.GetAllocatedWidth()). | 157 | Int("anchor_width", widget.GetAllocatedWidth()). | ||
| 157 | Int("anchor_height", widget.GetAllocatedHeight()). | 158 | Int("anchor_height", widget.GetAllocatedHeight()). | ||
| 158 | Int("parent_width", cefWidgetAllocatedWidth(parent)). | 159 | Int("parent_width", cefWidgetAllocatedWidth(parent)). | ||
M internal/infrastructure/cef/context_menu_adapter_test.go +8 -4
| @@ -216,11 +216,15 @@ func TestContextMenuSelectionFallsBackToNativeCopySelectionWhenTextUnavailable(t | |||
| 216 | 216 | require.Zero(t, executor.executeCalls) | |
| 217 | 217 | } | |
| 218 | 218 | ||
| 219 | - | func TestContextMenuAnchorPositionScalesCEFCoordinates(t *testing.T) { | |
| 220 | - | x, y := contextMenuAnchorPosition(stubContextMenuParams{x: 320, y: 180}, 2) | |
| 219 | + | func TestContextMenuAnchorPositionScalesCEFCoordinatesForDeviceBacking(t *testing.T) { | |
| 220 | + | x, y := contextMenuAnchorPosition(stubContextMenuParams{x: 320, y: 180}, 1.25, 1) | |
| 221 | 221 | ||
| 222 | - | require.Equal(t, int32(160), x) | |
| 223 | - | require.Equal(t, int32(90), y) | |
| 222 | + | require.Equal(t, int32(320), x) | |
| 223 | + | require.Equal(t, int32(180), y) | |
| 224 | + | ||
| 225 | + | x, y = contextMenuAnchorPosition(stubContextMenuParams{x: 320, y: 180}, 1.25, 1.25) | |
| 226 | + | require.Equal(t, int32(256), x) | |
| 227 | + | require.Equal(t, int32(144), y) | |
| 224 | 228 | } | |
| 225 | 229 | ||
| 226 | 230 | func TestContextMenuRawPositionDefaultsToZeroWhenParamsNil(t *testing.T) { | |
| @@ -216,11 +216,15 @@ func TestContextMenuSelectionFallsBackToNativeCopySelectionWhenTextUnavailable(t | |||||
| 216 | require.Zero(t, executor.executeCalls) | 216 | require.Zero(t, executor.executeCalls) | ||
| 217 | } | 217 | } | ||
| 218 | 218 | ||||
| 219 | - | func TestContextMenuAnchorPositionScalesCEFCoordinates(t *testing.T) { | 219 | + | func TestContextMenuAnchorPositionScalesCEFCoordinatesForDeviceBacking(t *testing.T) { |
| 220 | - | x, y := contextMenuAnchorPosition(stubContextMenuParams{x: 320, y: 180}, 2) | 220 | + | x, y := contextMenuAnchorPosition(stubContextMenuParams{x: 320, y: 180}, 1.25, 1) |
| 221 | 221 | ||||
| 222 | - | require.Equal(t, int32(160), x) | 222 | + | require.Equal(t, int32(320), x) |
| 223 | - | require.Equal(t, int32(90), y) | 223 | + | require.Equal(t, int32(180), y) |
| 224 | + | ||||
| 225 | + | x, y = contextMenuAnchorPosition(stubContextMenuParams{x: 320, y: 180}, 1.25, 1.25) | |||
| 226 | + | require.Equal(t, int32(256), x) | |||
| 227 | + | require.Equal(t, int32(144), y) | |||
| 224 | } | 228 | } | ||
| 225 | 229 | ||||
| 226 | func TestContextMenuRawPositionDefaultsToZeroWhenParamsNil(t *testing.T) { | 230 | func TestContextMenuRawPositionDefaultsToZeroWhenParamsNil(t *testing.T) { | ||
M internal/infrastructure/cef/engine_init.go +0 -35
| @@ -208,7 +208,6 @@ func wireEngine( | |||
| 208 | 208 | currentConfigPayload func() ([]byte, error), defaultConfigPayload func() ([]byte, error), | |
| 209 | 209 | ) (*Engine, error) { | |
| 210 | 210 | eng.factory = newWebViewFactory(eng, webViewFactoryOptions{ | |
| 211 | - | scale: detectHiDPIScale(logger), | |
| 212 | 211 | windowlessFrameRate: windowlessFrameRate, | |
| 213 | 212 | audioOutputFactory: audioFactory, | |
| 214 | 213 | }) | |
| @@ -315,40 +314,6 @@ func registerEngineSchemeFactory( | |||
| 315 | 314 | Msg("cef: registered internal https handler factory") | |
| 316 | 315 | } | |
| 317 | 316 | ||
| 318 | - | // getPrimaryMonitor returns the primary GDK monitor, or nil if unavailable. | |
| 319 | - | func getPrimaryMonitor() *gdk.Monitor { | |
| 320 | - | display := gdk.DisplayGetDefault() | |
| 321 | - | if display == nil { | |
| 322 | - | return nil | |
| 323 | - | } | |
| 324 | - | monitors := display.GetMonitors() | |
| 325 | - | if monitors == nil { | |
| 326 | - | return nil | |
| 327 | - | } | |
| 328 | - | obj := monitors.GetObject(0) | |
| 329 | - | if obj == nil { | |
| 330 | - | return nil | |
| 331 | - | } | |
| 332 | - | mon := &gdk.Monitor{} | |
| 333 | - | mon.SetGoPointer(obj.GoPointer()) | |
| 334 | - | return mon | |
| 335 | - | } | |
| 336 | - | ||
| 337 | - | // detectHiDPIScale queries the primary GDK monitor for its scale factor. | |
| 338 | - | func detectHiDPIScale(logger *zerolog.Logger) int32 { | |
| 339 | - | mon := getPrimaryMonitor() | |
| 340 | - | if mon == nil { | |
| 341 | - | logger.Debug().Msg("cef: no primary monitor, using scale=1") | |
| 342 | - | return 1 | |
| 343 | - | } | |
| 344 | - | if s := mon.GetScaleFactor(); s > 0 { | |
| 345 | - | logger.Info().Int32("scale", int32(s)).Msg("cef: detected HiDPI scale from monitor") | |
| 346 | - | return int32(s) | |
| 347 | - | } | |
| 348 | - | logger.Debug().Msg("cef: monitor scale factor <= 0, using scale=1") | |
| 349 | - | return 1 | |
| 350 | - | } | |
| 351 | - | ||
| 352 | 317 | // appendIfMissing appends flag to args if it's not already present. | |
| 353 | 318 | func appendIfMissing(args []string, flag string) []string { | |
| 354 | 319 | for _, a := range args { | |
| @@ -208,7 +208,6 @@ func wireEngine( | |||||
| 208 | currentConfigPayload func() ([]byte, error), defaultConfigPayload func() ([]byte, error), | 208 | currentConfigPayload func() ([]byte, error), defaultConfigPayload func() ([]byte, error), | ||
| 209 | ) (*Engine, error) { | 209 | ) (*Engine, error) { | ||
| 210 | eng.factory = newWebViewFactory(eng, webViewFactoryOptions{ | 210 | eng.factory = newWebViewFactory(eng, webViewFactoryOptions{ | ||
| 211 | - | scale: detectHiDPIScale(logger), | |||
| 212 | windowlessFrameRate: windowlessFrameRate, | 211 | windowlessFrameRate: windowlessFrameRate, | ||
| 213 | audioOutputFactory: audioFactory, | 212 | audioOutputFactory: audioFactory, | ||
| 214 | }) | 213 | }) | ||
| @@ -315,40 +314,6 @@ func registerEngineSchemeFactory( | |||||
| 315 | Msg("cef: registered internal https handler factory") | 314 | Msg("cef: registered internal https handler factory") | ||
| 316 | } | 315 | } | ||
| 317 | 316 | ||||
| 318 | - | // getPrimaryMonitor returns the primary GDK monitor, or nil if unavailable. | |||
| 319 | - | func getPrimaryMonitor() *gdk.Monitor { | |||
| 320 | - | display := gdk.DisplayGetDefault() | |||
| 321 | - | if display == nil { | |||
| 322 | - | return nil | |||
| 323 | - | } | |||
| 324 | - | monitors := display.GetMonitors() | |||
| 325 | - | if monitors == nil { | |||
| 326 | - | return nil | |||
| 327 | - | } | |||
| 328 | - | obj := monitors.GetObject(0) | |||
| 329 | - | if obj == nil { | |||
| 330 | - | return nil | |||
| 331 | - | } | |||
| 332 | - | mon := &gdk.Monitor{} | |||
| 333 | - | mon.SetGoPointer(obj.GoPointer()) | |||
| 334 | - | return mon | |||
| 335 | - | } | |||
| 336 | - | ||||
| 337 | - | // detectHiDPIScale queries the primary GDK monitor for its scale factor. | |||
| 338 | - | func detectHiDPIScale(logger *zerolog.Logger) int32 { | |||
| 339 | - | mon := getPrimaryMonitor() | |||
| 340 | - | if mon == nil { | |||
| 341 | - | logger.Debug().Msg("cef: no primary monitor, using scale=1") | |||
| 342 | - | return 1 | |||
| 343 | - | } | |||
| 344 | - | if s := mon.GetScaleFactor(); s > 0 { | |||
| 345 | - | logger.Info().Int32("scale", int32(s)).Msg("cef: detected HiDPI scale from monitor") | |||
| 346 | - | return int32(s) | |||
| 347 | - | } | |||
| 348 | - | logger.Debug().Msg("cef: monitor scale factor <= 0, using scale=1") | |||
| 349 | - | return 1 | |||
| 350 | - | } | |||
| 351 | - | ||||
| 352 | // appendIfMissing appends flag to args if it's not already present. | 317 | // appendIfMissing appends flag to args if it's not already present. | ||
| 353 | func appendIfMissing(args []string, flag string) []string { | 318 | func appendIfMissing(args []string, flag string) []string { | ||
| 354 | for _, a := range args { | 319 | for _, a := range args { | ||
M internal/infrastructure/cef/env.go +6 -0
| @@ -13,6 +13,8 @@ const ( | |||
| 13 | 13 | cefRenderNodeEnvVar = "DUMBER_CEF_RENDER_NODE" | |
| 14 | 14 | cefRenderStallRecoveryEnvVar = "DUMBER_CEF_RENDER_STALL_RECOVERY" | |
| 15 | 15 | cefRenderStallBacktraceEnvVar = "DUMBER_CEF_RENDER_STALL_BACKTRACE" | |
| 16 | + | cefScaleProbeEnvVar = "DUMBER_CEF_SCALE_PROBE" | |
| 17 | + | cef2GTKTraceScaleEnvVar = "PUREGO_CEF2GTK_TRACE_SCALE" | |
| 16 | 18 | ) | |
| 17 | 19 | ||
| 18 | 20 | // envBoolEnabled returns true when the given environment variable is set | |
| @@ -41,3 +43,7 @@ func renderStallRecoveryEnabled() bool { | |||
| 41 | 43 | func renderStallBacktraceEnabled() bool { | |
| 42 | 44 | return envBoolEnabled(cefRenderStallBacktraceEnvVar) | |
| 43 | 45 | } | |
| 46 | + | ||
| 47 | + | func cefScaleProbeEnabled() bool { | |
| 48 | + | return envBoolEnabled(cefScaleProbeEnvVar) || os.Getenv(cef2GTKTraceScaleEnvVar) != "" | |
| 49 | + | } | |
| @@ -13,6 +13,8 @@ const ( | |||||
| 13 | cefRenderNodeEnvVar = "DUMBER_CEF_RENDER_NODE" | 13 | cefRenderNodeEnvVar = "DUMBER_CEF_RENDER_NODE" | ||
| 14 | cefRenderStallRecoveryEnvVar = "DUMBER_CEF_RENDER_STALL_RECOVERY" | 14 | cefRenderStallRecoveryEnvVar = "DUMBER_CEF_RENDER_STALL_RECOVERY" | ||
| 15 | cefRenderStallBacktraceEnvVar = "DUMBER_CEF_RENDER_STALL_BACKTRACE" | 15 | cefRenderStallBacktraceEnvVar = "DUMBER_CEF_RENDER_STALL_BACKTRACE" | ||
| 16 | + | cefScaleProbeEnvVar = "DUMBER_CEF_SCALE_PROBE" | |||
| 17 | + | cef2GTKTraceScaleEnvVar = "PUREGO_CEF2GTK_TRACE_SCALE" | |||
| 16 | ) | 18 | ) | ||
| 17 | 19 | ||||
| 18 | // envBoolEnabled returns true when the given environment variable is set | 20 | // envBoolEnabled returns true when the given environment variable is set | ||
| @@ -41,3 +43,7 @@ func renderStallRecoveryEnabled() bool { | |||||
| 41 | func renderStallBacktraceEnabled() bool { | 43 | func renderStallBacktraceEnabled() bool { | ||
| 42 | return envBoolEnabled(cefRenderStallBacktraceEnvVar) | 44 | return envBoolEnabled(cefRenderStallBacktraceEnvVar) | ||
| 43 | } | 45 | } | ||
| 46 | + | ||||
| 47 | + | func cefScaleProbeEnabled() bool { | |||
| 48 | + | return envBoolEnabled(cefScaleProbeEnvVar) || os.Getenv(cef2GTKTraceScaleEnvVar) != "" | |||
| 49 | + | } | |||
M internal/infrastructure/cef/factory.go +1 -8
| @@ -22,14 +22,12 @@ var _ port.WebViewFactory = (*WebViewFactory)(nil) | |||
| 22 | 22 | type WebViewFactory struct { | |
| 23 | 23 | engine *Engine | |
| 24 | 24 | nextID atomic.Uint64 | |
| 25 | - | scale int32 | |
| 26 | 25 | windowlessFrameRate int32 | |
| 27 | 26 | bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor | |
| 28 | 27 | audioOutputFactory port.AudioOutputFactory | |
| 29 | 28 | } | |
| 30 | 29 | ||
| 31 | 30 | type webViewFactoryOptions struct { | |
| 32 | - | scale int32 | |
| 33 | 31 | windowlessFrameRate int32 | |
| 34 | 32 | audioOutputFactory port.AudioOutputFactory | |
| 35 | 33 | } | |
| @@ -46,18 +44,13 @@ const ( | |||
| 46 | 44 | pendingBrowserCreateMaxPostRetries = 3 | |
| 47 | 45 | ) | |
| 48 | 46 | ||
| 49 | - | // newWebViewFactory returns a factory that will create WebViews using the | |
| 50 | - | // given HiDPI scale factor. | |
| 47 | + | // newWebViewFactory returns a factory that will create WebViews. | |
| 51 | 48 | func newWebViewFactory(engine *Engine, opts webViewFactoryOptions) *WebViewFactory { | |
| 52 | - | if opts.scale < 1 { | |
| 53 | - | opts.scale = 1 | |
| 54 | - | } | |
| 55 | 49 | if opts.windowlessFrameRate < 1 { | |
| 56 | 50 | opts.windowlessFrameRate = 60 | |
| 57 | 51 | } | |
| 58 | 52 | return &WebViewFactory{ | |
| 59 | 53 | engine: engine, | |
| 60 | - | scale: opts.scale, | |
| 61 | 54 | windowlessFrameRate: opts.windowlessFrameRate, | |
| 62 | 55 | audioOutputFactory: opts.audioOutputFactory, | |
| 63 | 56 | } | |
| @@ -22,14 +22,12 @@ var _ port.WebViewFactory = (*WebViewFactory)(nil) | |||||
| 22 | type WebViewFactory struct { | 22 | type WebViewFactory struct { | ||
| 23 | engine *Engine | 23 | engine *Engine | ||
| 24 | nextID atomic.Uint64 | 24 | nextID atomic.Uint64 | ||
| 25 | - | scale int32 | |||
| 26 | windowlessFrameRate int32 | 25 | windowlessFrameRate int32 | ||
| 27 | bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor | 26 | bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor | ||
| 28 | audioOutputFactory port.AudioOutputFactory | 27 | audioOutputFactory port.AudioOutputFactory | ||
| 29 | } | 28 | } | ||
| 30 | 29 | ||||
| 31 | type webViewFactoryOptions struct { | 30 | type webViewFactoryOptions struct { | ||
| 32 | - | scale int32 | |||
| 33 | windowlessFrameRate int32 | 31 | windowlessFrameRate int32 | ||
| 34 | audioOutputFactory port.AudioOutputFactory | 32 | audioOutputFactory port.AudioOutputFactory | ||
| 35 | } | 33 | } | ||
| @@ -46,18 +44,13 @@ const ( | |||||
| 46 | pendingBrowserCreateMaxPostRetries = 3 | 44 | pendingBrowserCreateMaxPostRetries = 3 | ||
| 47 | ) | 45 | ) | ||
| 48 | 46 | ||||
| 49 | - | // newWebViewFactory returns a factory that will create WebViews using the | 47 | + | // newWebViewFactory returns a factory that will create WebViews. |
| 50 | - | // given HiDPI scale factor. | |||
| 51 | func newWebViewFactory(engine *Engine, opts webViewFactoryOptions) *WebViewFactory { | 48 | func newWebViewFactory(engine *Engine, opts webViewFactoryOptions) *WebViewFactory { | ||
| 52 | - | if opts.scale < 1 { | |||
| 53 | - | opts.scale = 1 | |||
| 54 | - | } | |||
| 55 | if opts.windowlessFrameRate < 1 { | 49 | if opts.windowlessFrameRate < 1 { | ||
| 56 | opts.windowlessFrameRate = 60 | 50 | opts.windowlessFrameRate = 60 | ||
| 57 | } | 51 | } | ||
| 58 | return &WebViewFactory{ | 52 | return &WebViewFactory{ | ||
| 59 | engine: engine, | 53 | engine: engine, | ||
| 60 | - | scale: opts.scale, | |||
| 61 | windowlessFrameRate: opts.windowlessFrameRate, | 54 | windowlessFrameRate: opts.windowlessFrameRate, | ||
| 62 | audioOutputFactory: opts.audioOutputFactory, | 55 | audioOutputFactory: opts.audioOutputFactory, | ||
| 63 | } | 56 | } | ||
M internal/infrastructure/cef/process_diagnostics.go +2 -1
| @@ -94,7 +94,8 @@ func safeChromiumCmdlineFlags(cmdline string) []string { | |||
| 94 | 94 | var safe string | |
| 95 | 95 | switch name { | |
| 96 | 96 | case "--type", "--lang", "--enable-features", "--disable-features", "--ozone-platform", | |
| 97 | - | "--use-gl", "--use-angle", "--enable-logging", "--log-severity": | |
| 97 | + | "--use-gl", "--use-angle", "--enable-logging", "--log-severity", | |
| 98 | + | "--force-device-scale-factor", "--high-dpi-support", "--enable-use-zoom-for-dsf": | |
| 98 | 99 | if hasValue && value != "" { | |
| 99 | 100 | safe = name + "=" + value | |
| 100 | 101 | } else { | |
| @@ -94,7 +94,8 @@ func safeChromiumCmdlineFlags(cmdline string) []string { | |||||
| 94 | var safe string | 94 | var safe string | ||
| 95 | switch name { | 95 | switch name { | ||
| 96 | case "--type", "--lang", "--enable-features", "--disable-features", "--ozone-platform", | 96 | case "--type", "--lang", "--enable-features", "--disable-features", "--ozone-platform", | ||
| 97 | - | "--use-gl", "--use-angle", "--enable-logging", "--log-severity": | 97 | + | "--use-gl", "--use-angle", "--enable-logging", "--log-severity", |
| 98 | + | "--force-device-scale-factor", "--high-dpi-support", "--enable-use-zoom-for-dsf": | |||
| 98 | if hasValue && value != "" { | 99 | if hasValue && value != "" { | ||
| 99 | safe = name + "=" + value | 100 | safe = name + "=" + value | ||
| 100 | } else { | 101 | } else { | ||
M internal/infrastructure/cef/render_stack_env.go +16 -13
| @@ -12,19 +12,20 @@ const ( | |||
| 12 | 12 | dumberRenderStackEnvVar = "DUMBER_RENDER_STACK" | |
| 13 | 13 | dumberRenderStackAllowSplitEnvVar = "DUMBER_RENDER_STACK_ALLOW_SPLIT" | |
| 14 | 14 | ||
| 15 | - | cefEngineType = "cef" | |
| 16 | - | renderStackAuto = "auto" | |
| 17 | - | renderStackVulkanDMABUF = "vulkan-dmabuf" | |
| 18 | - | renderStackLegacyGL = "legacy-gl" | |
| 19 | - | cef2gtkBackendEnvVar = "PUREGO_CEF2GTK_BACKEND" | |
| 20 | - | cef2gtkAngleBackendVar = "PUREGO_CEF2GTK_ANGLE_BACKEND" | |
| 21 | - | gskRendererEnvVar = "GSK_RENDERER" | |
| 22 | - | cef2gtkBackendGDKDMABUF = "gdk-dmabuf" | |
| 23 | - | cef2gtkBackendGLArea = "glarea" | |
| 24 | - | cef2gtkAngleVulkan = "vulkan" | |
| 25 | - | cef2gtkAngleGLEGL = "gl-egl" | |
| 26 | - | gskRendererVulkan = "vulkan" | |
| 27 | - | gskRendererOpenGL = "opengl" | |
| 15 | + | cefEngineType = "cef" | |
| 16 | + | renderStackAuto = "auto" | |
| 17 | + | renderStackVulkanDMABUF = "vulkan-dmabuf" | |
| 18 | + | renderStackLegacyGL = "legacy-gl" | |
| 19 | + | cef2gtkBackendEnvVar = "PUREGO_CEF2GTK_BACKEND" | |
| 20 | + | cef2gtkAngleBackendVar = "PUREGO_CEF2GTK_ANGLE_BACKEND" | |
| 21 | + | cef2gtkOSRBackingScaleEnvVar = "PUREGO_CEF2GTK_OSR_BACKING_SCALE" | |
| 22 | + | gskRendererEnvVar = "GSK_RENDERER" | |
| 23 | + | cef2gtkBackendGDKDMABUF = "gdk-dmabuf" | |
| 24 | + | cef2gtkBackendGLArea = "glarea" | |
| 25 | + | cef2gtkAngleVulkan = "vulkan" | |
| 26 | + | cef2gtkAngleGLEGL = "gl-egl" | |
| 27 | + | gskRendererVulkan = "vulkan" | |
| 28 | + | gskRendererOpenGL = "opengl" | |
| 28 | 29 | ) | |
| 29 | 30 | ||
| 30 | 31 | // ApplyDefaultRenderStackEnvironment configures the process environment for | |
| @@ -54,6 +55,7 @@ func applyDefaultRenderStackEnvironment(logger port.Logger) string { | |||
| 54 | 55 | setRenderStackEnv(logger, gskRendererEnvVar, gskRendererVulkan) | |
| 55 | 56 | setRenderStackEnv(logger, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF) | |
| 56 | 57 | setRenderStackEnv(logger, cef2gtkAngleBackendVar, cef2gtkAngleVulkan) | |
| 58 | + | setEnvDefault(logger, cef2gtkOSRBackingScaleEnvVar, "auto") | |
| 57 | 59 | } | |
| 58 | 60 | logRenderStackEnvironment(logger, stack) | |
| 59 | 61 | return stack | |
| @@ -65,6 +67,7 @@ func logRenderStackEnvironment(logger port.Logger, stack string) { | |||
| 65 | 67 | port.Field("gsk_renderer", os.Getenv(gskRendererEnvVar)), | |
| 66 | 68 | port.Field("cef2gtk_backend", os.Getenv(cef2gtkBackendEnvVar)), | |
| 67 | 69 | port.Field("cef2gtk_angle_backend", os.Getenv(cef2gtkAngleBackendVar)), | |
| 70 | + | port.Field("cef2gtk_osr_backing_scale", os.Getenv(cef2gtkOSRBackingScaleEnvVar)), | |
| 68 | 71 | port.Field("split_stack_allowed", envBoolEnabled(dumberRenderStackAllowSplitEnvVar)), | |
| 69 | 72 | ) | |
| 70 | 73 | } | |
| @@ -12,19 +12,20 @@ const ( | |||||
| 12 | dumberRenderStackEnvVar = "DUMBER_RENDER_STACK" | 12 | dumberRenderStackEnvVar = "DUMBER_RENDER_STACK" | ||
| 13 | dumberRenderStackAllowSplitEnvVar = "DUMBER_RENDER_STACK_ALLOW_SPLIT" | 13 | dumberRenderStackAllowSplitEnvVar = "DUMBER_RENDER_STACK_ALLOW_SPLIT" | ||
| 14 | 14 | ||||
| 15 | - | cefEngineType = "cef" | 15 | + | cefEngineType = "cef" |
| 16 | - | renderStackAuto = "auto" | 16 | + | renderStackAuto = "auto" |
| 17 | - | renderStackVulkanDMABUF = "vulkan-dmabuf" | 17 | + | renderStackVulkanDMABUF = "vulkan-dmabuf" |
| 18 | - | renderStackLegacyGL = "legacy-gl" | 18 | + | renderStackLegacyGL = "legacy-gl" |
| 19 | - | cef2gtkBackendEnvVar = "PUREGO_CEF2GTK_BACKEND" | 19 | + | cef2gtkBackendEnvVar = "PUREGO_CEF2GTK_BACKEND" |
| 20 | - | cef2gtkAngleBackendVar = "PUREGO_CEF2GTK_ANGLE_BACKEND" | 20 | + | cef2gtkAngleBackendVar = "PUREGO_CEF2GTK_ANGLE_BACKEND" |
| 21 | - | gskRendererEnvVar = "GSK_RENDERER" | 21 | + | cef2gtkOSRBackingScaleEnvVar = "PUREGO_CEF2GTK_OSR_BACKING_SCALE" |
| 22 | - | cef2gtkBackendGDKDMABUF = "gdk-dmabuf" | 22 | + | gskRendererEnvVar = "GSK_RENDERER" |
| 23 | - | cef2gtkBackendGLArea = "glarea" | 23 | + | cef2gtkBackendGDKDMABUF = "gdk-dmabuf" |
| 24 | - | cef2gtkAngleVulkan = "vulkan" | 24 | + | cef2gtkBackendGLArea = "glarea" |
| 25 | - | cef2gtkAngleGLEGL = "gl-egl" | 25 | + | cef2gtkAngleVulkan = "vulkan" |
| 26 | - | gskRendererVulkan = "vulkan" | 26 | + | cef2gtkAngleGLEGL = "gl-egl" |
| 27 | - | gskRendererOpenGL = "opengl" | 27 | + | gskRendererVulkan = "vulkan" |
| 28 | + | gskRendererOpenGL = "opengl" | |||
| 28 | ) | 29 | ) | ||
| 29 | 30 | ||||
| 30 | // ApplyDefaultRenderStackEnvironment configures the process environment for | 31 | // ApplyDefaultRenderStackEnvironment configures the process environment for | ||
| @@ -54,6 +55,7 @@ func applyDefaultRenderStackEnvironment(logger port.Logger) string { | |||||
| 54 | setRenderStackEnv(logger, gskRendererEnvVar, gskRendererVulkan) | 55 | setRenderStackEnv(logger, gskRendererEnvVar, gskRendererVulkan) | ||
| 55 | setRenderStackEnv(logger, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF) | 56 | setRenderStackEnv(logger, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF) | ||
| 56 | setRenderStackEnv(logger, cef2gtkAngleBackendVar, cef2gtkAngleVulkan) | 57 | setRenderStackEnv(logger, cef2gtkAngleBackendVar, cef2gtkAngleVulkan) | ||
| 58 | + | setEnvDefault(logger, cef2gtkOSRBackingScaleEnvVar, "auto") | |||
| 57 | } | 59 | } | ||
| 58 | logRenderStackEnvironment(logger, stack) | 60 | logRenderStackEnvironment(logger, stack) | ||
| 59 | return stack | 61 | return stack | ||
| @@ -65,6 +67,7 @@ func logRenderStackEnvironment(logger port.Logger, stack string) { | |||||
| 65 | port.Field("gsk_renderer", os.Getenv(gskRendererEnvVar)), | 67 | port.Field("gsk_renderer", os.Getenv(gskRendererEnvVar)), | ||
| 66 | port.Field("cef2gtk_backend", os.Getenv(cef2gtkBackendEnvVar)), | 68 | port.Field("cef2gtk_backend", os.Getenv(cef2gtkBackendEnvVar)), | ||
| 67 | port.Field("cef2gtk_angle_backend", os.Getenv(cef2gtkAngleBackendVar)), | 69 | port.Field("cef2gtk_angle_backend", os.Getenv(cef2gtkAngleBackendVar)), | ||
| 70 | + | port.Field("cef2gtk_osr_backing_scale", os.Getenv(cef2gtkOSRBackingScaleEnvVar)), | |||
| 68 | port.Field("split_stack_allowed", envBoolEnabled(dumberRenderStackAllowSplitEnvVar)), | 71 | port.Field("split_stack_allowed", envBoolEnabled(dumberRenderStackAllowSplitEnvVar)), | ||
| 69 | ) | 72 | ) | ||
| 70 | } | 73 | } | ||
M internal/infrastructure/cef/render_stack_env_test.go +16 -0
| @@ -13,6 +13,7 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t | |||
| 13 | 13 | t.Setenv("GSK_RENDERER", "") | |
| 14 | 14 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | |
| 15 | 15 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "") | |
| 16 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "") | |
| 16 | 17 | ||
| 17 | 18 | got := applyDefaultRenderStackEnvironment(nil) | |
| 18 | 19 | ||
| @@ -28,6 +29,9 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t | |||
| 28 | 29 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { | |
| 29 | 30 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want vulkan", got) | |
| 30 | 31 | } | |
| 32 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "auto" { | |
| 33 | + | t.Fatalf("%s = %q, want auto", cef2gtkOSRBackingScaleEnvVar, got) | |
| 34 | + | } | |
| 31 | 35 | } | |
| 32 | 36 | ||
| 33 | 37 | func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults(t *testing.T) { | |
| @@ -36,6 +40,7 @@ func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults | |||
| 36 | 40 | t.Setenv("GSK_RENDERER", "ngl") | |
| 37 | 41 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea") | |
| 38 | 42 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl") | |
| 43 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "") | |
| 39 | 44 | ||
| 40 | 45 | got := applyDefaultRenderStackEnvironment(nil) | |
| 41 | 46 | ||
| @@ -51,6 +56,9 @@ func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults | |||
| 51 | 56 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { | |
| 52 | 57 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want coherent vulkan default", got) | |
| 53 | 58 | } | |
| 59 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "auto" { | |
| 60 | + | t.Fatalf("%s = %q, want auto", cef2gtkOSRBackingScaleEnvVar, got) | |
| 61 | + | } | |
| 54 | 62 | } | |
| 55 | 63 | ||
| 56 | 64 | func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelOverrides(t *testing.T) { | |
| @@ -59,6 +67,7 @@ func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelO | |||
| 59 | 67 | t.Setenv("GSK_RENDERER", "ngl") | |
| 60 | 68 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea") | |
| 61 | 69 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl") | |
| 70 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "off") | |
| 62 | 71 | ||
| 63 | 72 | got := applyDefaultRenderStackEnvironment(nil) | |
| 64 | 73 | ||
| @@ -74,6 +83,9 @@ func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelO | |||
| 74 | 83 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | |
| 75 | 84 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want explicit gl-egl", got) | |
| 76 | 85 | } | |
| 86 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "off" { | |
| 87 | + | t.Fatalf("%s = %q, want explicit off", cef2gtkOSRBackingScaleEnvVar, got) | |
| 88 | + | } | |
| 77 | 89 | } | |
| 78 | 90 | ||
| 79 | 91 | func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | |
| @@ -81,6 +93,7 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | |||
| 81 | 93 | t.Setenv("GSK_RENDERER", "") | |
| 82 | 94 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | |
| 83 | 95 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "") | |
| 96 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "") | |
| 84 | 97 | ||
| 85 | 98 | got := applyDefaultRenderStackEnvironment(nil) | |
| 86 | 99 | ||
| @@ -93,6 +106,9 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | |||
| 93 | 106 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | |
| 94 | 107 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | |
| 95 | 108 | } | |
| 109 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "" { | |
| 110 | + | t.Fatalf("%s = %q, want unset for legacy-gl", cef2gtkOSRBackingScaleEnvVar, got) | |
| 111 | + | } | |
| 96 | 112 | } | |
| 97 | 113 | ||
| 98 | 114 | func TestNormalizeRenderStackUnknownFallsBackToGDKDMABUF(t *testing.T) { | |
| @@ -13,6 +13,7 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t | |||||
| 13 | t.Setenv("GSK_RENDERER", "") | 13 | t.Setenv("GSK_RENDERER", "") | ||
| 14 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | 14 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | ||
| 15 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "") | 15 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "") | ||
| 16 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "") | |||
| 16 | 17 | ||||
| 17 | got := applyDefaultRenderStackEnvironment(nil) | 18 | got := applyDefaultRenderStackEnvironment(nil) | ||
| 18 | 19 | ||||
| @@ -28,6 +29,9 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t | |||||
| 28 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { | 29 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { | ||
| 29 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want vulkan", got) | 30 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want vulkan", got) | ||
| 30 | } | 31 | } | ||
| 32 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "auto" { | |||
| 33 | + | t.Fatalf("%s = %q, want auto", cef2gtkOSRBackingScaleEnvVar, got) | |||
| 34 | + | } | |||
| 31 | } | 35 | } | ||
| 32 | 36 | ||||
| 33 | func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults(t *testing.T) { | 37 | func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults(t *testing.T) { | ||
| @@ -36,6 +40,7 @@ func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults | |||||
| 36 | t.Setenv("GSK_RENDERER", "ngl") | 40 | t.Setenv("GSK_RENDERER", "ngl") | ||
| 37 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea") | 41 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea") | ||
| 38 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl") | 42 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl") | ||
| 43 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "") | |||
| 39 | 44 | ||||
| 40 | got := applyDefaultRenderStackEnvironment(nil) | 45 | got := applyDefaultRenderStackEnvironment(nil) | ||
| 41 | 46 | ||||
| @@ -51,6 +56,9 @@ func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults | |||||
| 51 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { | 56 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { | ||
| 52 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want coherent vulkan default", got) | 57 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want coherent vulkan default", got) | ||
| 53 | } | 58 | } | ||
| 59 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "auto" { | |||
| 60 | + | t.Fatalf("%s = %q, want auto", cef2gtkOSRBackingScaleEnvVar, got) | |||
| 61 | + | } | |||
| 54 | } | 62 | } | ||
| 55 | 63 | ||||
| 56 | func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelOverrides(t *testing.T) { | 64 | func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelOverrides(t *testing.T) { | ||
| @@ -59,6 +67,7 @@ func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelO | |||||
| 59 | t.Setenv("GSK_RENDERER", "ngl") | 67 | t.Setenv("GSK_RENDERER", "ngl") | ||
| 60 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea") | 68 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea") | ||
| 61 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl") | 69 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl") | ||
| 70 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "off") | |||
| 62 | 71 | ||||
| 63 | got := applyDefaultRenderStackEnvironment(nil) | 72 | got := applyDefaultRenderStackEnvironment(nil) | ||
| 64 | 73 | ||||
| @@ -74,6 +83,9 @@ func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelO | |||||
| 74 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | 83 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | ||
| 75 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want explicit gl-egl", got) | 84 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want explicit gl-egl", got) | ||
| 76 | } | 85 | } | ||
| 86 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "off" { | |||
| 87 | + | t.Fatalf("%s = %q, want explicit off", cef2gtkOSRBackingScaleEnvVar, got) | |||
| 88 | + | } | |||
| 77 | } | 89 | } | ||
| 78 | 90 | ||||
| 79 | func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | 91 | func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | ||
| @@ -81,6 +93,7 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | |||||
| 81 | t.Setenv("GSK_RENDERER", "") | 93 | t.Setenv("GSK_RENDERER", "") | ||
| 82 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | 94 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | ||
| 83 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "") | 95 | t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "") | ||
| 96 | + | t.Setenv(cef2gtkOSRBackingScaleEnvVar, "") | |||
| 84 | 97 | ||||
| 85 | got := applyDefaultRenderStackEnvironment(nil) | 98 | got := applyDefaultRenderStackEnvironment(nil) | ||
| 86 | 99 | ||||
| @@ -93,6 +106,9 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | |||||
| 93 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | 106 | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | ||
| 94 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | 107 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | ||
| 95 | } | 108 | } | ||
| 109 | + | if got := os.Getenv(cef2gtkOSRBackingScaleEnvVar); got != "" { | |||
| 110 | + | t.Fatalf("%s = %q, want unset for legacy-gl", cef2gtkOSRBackingScaleEnvVar, got) | |||
| 111 | + | } | |||
| 96 | } | 112 | } | ||
| 97 | 113 | ||||
| 98 | func TestNormalizeRenderStackUnknownFallsBackToGDKDMABUF(t *testing.T) { | 114 | func TestNormalizeRenderStackUnknownFallsBackToGDKDMABUF(t *testing.T) { | ||
A internal/infrastructure/cef/scale_probe.go +34 -0
| @@ -0,0 +1,34 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | const cefScaleProbeScript = `(function(){ | |
| 4 | + | try { | |
| 5 | + | var vv = window.visualViewport; | |
| 6 | + | var probe = document.createElement('div'); | |
| 7 | + | probe.style.cssText = 'position:fixed;left:0;top:0;width:100px;height:100px;visibility:hidden;pointer-events:none;z-index:-1'; | |
| 8 | + | document.documentElement.appendChild(probe); | |
| 9 | + | var rect = probe.getBoundingClientRect(); | |
| 10 | + | probe.remove(); | |
| 11 | + | var data = { | |
| 12 | + | dpr: window.devicePixelRatio, | |
| 13 | + | inner: [window.innerWidth, window.innerHeight], | |
| 14 | + | outer: [window.outerWidth, window.outerHeight], | |
| 15 | + | client: [document.documentElement.clientWidth, document.documentElement.clientHeight], | |
| 16 | + | screen: [window.screen.width, window.screen.height], | |
| 17 | + | avail: [window.screen.availWidth, window.screen.availHeight], | |
| 18 | + | visualViewport: vv ? { | |
| 19 | + | width: vv.width, | |
| 20 | + | height: vv.height, | |
| 21 | + | scale: vv.scale, | |
| 22 | + | offsetLeft: vv.offsetLeft, | |
| 23 | + | offsetTop: vv.offsetTop, | |
| 24 | + | pageLeft: vv.pageLeft, | |
| 25 | + | pageTop: vv.pageTop | |
| 26 | + | } : null, | |
| 27 | + | css100Rect: {width: rect.width, height: rect.height, left: rect.left, top: rect.top}, | |
| 28 | + | scroll: [window.scrollX, window.scrollY] | |
| 29 | + | }; | |
| 30 | + | console.info('[SCALE-PROBE] ' + JSON.stringify(data)); | |
| 31 | + | } catch (e) { | |
| 32 | + | console.warn('[SCALE-PROBE] error ' + (e && e.message ? e.message : String(e))); | |
| 33 | + | } | |
| 34 | + | })();` | |
| @@ -0,0 +1,34 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | const cefScaleProbeScript = `(function(){ | |||
| 4 | + | try { | |||
| 5 | + | var vv = window.visualViewport; | |||
| 6 | + | var probe = document.createElement('div'); | |||
| 7 | + | probe.style.cssText = 'position:fixed;left:0;top:0;width:100px;height:100px;visibility:hidden;pointer-events:none;z-index:-1'; | |||
| 8 | + | document.documentElement.appendChild(probe); | |||
| 9 | + | var rect = probe.getBoundingClientRect(); | |||
| 10 | + | probe.remove(); | |||
| 11 | + | var data = { | |||
| 12 | + | dpr: window.devicePixelRatio, | |||
| 13 | + | inner: [window.innerWidth, window.innerHeight], | |||
| 14 | + | outer: [window.outerWidth, window.outerHeight], | |||
| 15 | + | client: [document.documentElement.clientWidth, document.documentElement.clientHeight], | |||
| 16 | + | screen: [window.screen.width, window.screen.height], | |||
| 17 | + | avail: [window.screen.availWidth, window.screen.availHeight], | |||
| 18 | + | visualViewport: vv ? { | |||
| 19 | + | width: vv.width, | |||
| 20 | + | height: vv.height, | |||
| 21 | + | scale: vv.scale, | |||
| 22 | + | offsetLeft: vv.offsetLeft, | |||
| 23 | + | offsetTop: vv.offsetTop, | |||
| 24 | + | pageLeft: vv.pageLeft, | |||
| 25 | + | pageTop: vv.pageTop | |||
| 26 | + | } : null, | |||
| 27 | + | css100Rect: {width: rect.width, height: rect.height, left: rect.left, top: rect.top}, | |||
| 28 | + | scroll: [window.scrollX, window.scrollY] | |||
| 29 | + | }; | |||
| 30 | + | console.info('[SCALE-PROBE] ' + JSON.stringify(data)); | |||
| 31 | + | } catch (e) { | |||
| 32 | + | console.warn('[SCALE-PROBE] error ' + (e && e.message ? e.message : String(e))); | |||
| 33 | + | } | |||
| 34 | + | })();` | |||
M internal/infrastructure/cef/webview.go +45 -6
| @@ -20,6 +20,7 @@ import ( | |||
| 20 | 20 | ||
| 21 | 21 | "github.com/bnema/dumber/internal/application/dto" | |
| 22 | 22 | "github.com/bnema/dumber/internal/application/port" | |
| 23 | + | "github.com/bnema/dumber/internal/infrastructure/gtkutil" | |
| 23 | 24 | "github.com/bnema/dumber/internal/logging" | |
| 24 | 25 | ) | |
| 25 | 26 | ||
| @@ -443,6 +444,14 @@ func cefZoomFromFactor(factor float64) float64 { | |||
| 443 | 444 | return math.Log(factor) / math.Log(chromiumZoomBase) | |
| 444 | 445 | } | |
| 445 | 446 | ||
| 447 | + | func cefZoomFromPageAndBackingFactor(pageZoom, backingScale float64) float64 { | |
| 448 | + | return cefZoomFromFactor(pageZoom / gtkutil.NormalizeScale(backingScale)) | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | func pageZoomFromCEFAndBackingLevel(level, backingScale float64) float64 { | |
| 452 | + | return factorFromCEFZoom(level) * gtkutil.NormalizeScale(backingScale) | |
| 453 | + | } | |
| 454 | + | ||
| 446 | 455 | // factorFromCEFZoom converts a Chromium/CEF logarithmic zoom level back to a | |
| 447 | 456 | // linear zoom factor. | |
| 448 | 457 | func factorFromCEFZoom(level float64) float64 { | |
| @@ -468,10 +477,12 @@ func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error { | |||
| 468 | 477 | if host == nil { | |
| 469 | 478 | return errNoBrowser | |
| 470 | 479 | } | |
| 471 | - | cefLevel := cefZoomFromFactor(factor) | |
| 480 | + | backingScale := wv.osrBackingScaleFactor() | |
| 481 | + | cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale) | |
| 472 | 482 | logging.FromContext(wv.ctx).Debug(). | |
| 473 | 483 | Float64("factor", factor). | |
| 474 | 484 | Float64("cef_level", cefLevel). | |
| 485 | + | Float64("osr_backing_scale", backingScale). | |
| 475 | 486 | Msg("cef: SetZoomLevel") | |
| 476 | 487 | host.SetZoomLevel(cefLevel) | |
| 477 | 488 | wv.zoomFactor.Store(factor) | |
| @@ -1376,15 +1387,18 @@ func (wv *WebView) selectedTextSnapshot() string { | |||
| 1376 | 1387 | return wv.selectedText | |
| 1377 | 1388 | } | |
| 1378 | 1389 | ||
| 1379 | - | func (wv *WebView) viewBridgeScale() int32 { | |
| 1390 | + | func (wv *WebView) viewBridgeScale() float64 { | |
| 1380 | 1391 | if wv == nil || wv.viewBridge == nil { | |
| 1381 | 1392 | return 1 | |
| 1382 | 1393 | } | |
| 1383 | - | scale := int32(math.Round(float64(wv.viewBridge.DeviceScaleFactor()))) | |
| 1384 | - | if scale < 1 { | |
| 1394 | + | return gtkutil.NormalizeScale(float64(wv.viewBridge.DeviceScaleFactor())) | |
| 1395 | + | } | |
| 1396 | + | ||
| 1397 | + | func (wv *WebView) osrBackingScaleFactor() float64 { | |
| 1398 | + | if wv == nil || wv.viewBridge == nil { | |
| 1385 | 1399 | return 1 | |
| 1386 | 1400 | } | |
| 1387 | - | return scale | |
| 1401 | + | return gtkutil.NormalizeScale(wv.viewBridge.OSRBackingScaleFactor()) | |
| 1388 | 1402 | } | |
| 1389 | 1403 | ||
| 1390 | 1404 | func (wv *WebView) handleMiddleClickFromBridge() bool { | |
| @@ -1637,18 +1651,43 @@ func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) { | |||
| 1637 | 1651 | if host == nil { | |
| 1638 | 1652 | return | |
| 1639 | 1653 | } | |
| 1654 | + | backingScale := wv.osrBackingScaleFactor() | |
| 1640 | 1655 | actualLevel := host.GetZoomLevel() | |
| 1641 | 1656 | logging.FromContext(wv.ctx).Debug(). | |
| 1642 | 1657 | Int64("delay_ms", delayMs). | |
| 1643 | 1658 | Float64("expected_factor", expectedFactor). | |
| 1644 | 1659 | Float64("expected_cef_level", expectedLevel). | |
| 1645 | - | Float64("actual_factor", factorFromCEFZoom(actualLevel)). | |
| 1660 | + | Float64("actual_factor", pageZoomFromCEFAndBackingLevel(actualLevel, backingScale)). | |
| 1661 | + | Float64("actual_cef_factor", factorFromCEFZoom(actualLevel)). | |
| 1646 | 1662 | Float64("actual_cef_level", actualLevel). | |
| 1663 | + | Float64("osr_backing_scale", backingScale). | |
| 1647 | 1664 | Msg("cef: zoom level readback") | |
| 1648 | 1665 | })), delayMs) | |
| 1649 | 1666 | } | |
| 1650 | 1667 | } | |
| 1651 | 1668 | ||
| 1669 | + | func (wv *WebView) reapplyCurrentZoomForBackingScale(reason string) { | |
| 1670 | + | if wv == nil || wv.destroyed.Load() { | |
| 1671 | + | return | |
| 1672 | + | } | |
| 1673 | + | wv.mu.RLock() | |
| 1674 | + | host := wv.host | |
| 1675 | + | wv.mu.RUnlock() | |
| 1676 | + | if host == nil { | |
| 1677 | + | return | |
| 1678 | + | } | |
| 1679 | + | factor := wv.GetZoomLevel() | |
| 1680 | + | backingScale := wv.osrBackingScaleFactor() | |
| 1681 | + | cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale) | |
| 1682 | + | host.SetZoomLevel(cefLevel) | |
| 1683 | + | logging.FromContext(wv.ctx).Debug(). | |
| 1684 | + | Str("reason", reason). | |
| 1685 | + | Float64("factor", factor). | |
| 1686 | + | Float64("cef_level", cefLevel). | |
| 1687 | + | Float64("osr_backing_scale", backingScale). | |
| 1688 | + | Msg("cef: reapplied zoom for OSR backing scale") | |
| 1689 | + | } | |
| 1690 | + | ||
| 1652 | 1691 | func (wv *WebView) scheduleStartBeginFrameLoop() { | |
| 1653 | 1692 | if !externalBeginFrameEnabled() { | |
| 1654 | 1693 | return | |
| @@ -20,6 +20,7 @@ import ( | |||||
| 20 | 20 | ||||
| 21 | "github.com/bnema/dumber/internal/application/dto" | 21 | "github.com/bnema/dumber/internal/application/dto" | ||
| 22 | "github.com/bnema/dumber/internal/application/port" | 22 | "github.com/bnema/dumber/internal/application/port" | ||
| 23 | + | "github.com/bnema/dumber/internal/infrastructure/gtkutil" | |||
| 23 | "github.com/bnema/dumber/internal/logging" | 24 | "github.com/bnema/dumber/internal/logging" | ||
| 24 | ) | 25 | ) | ||
| 25 | 26 | ||||
| @@ -443,6 +444,14 @@ func cefZoomFromFactor(factor float64) float64 { | |||||
| 443 | return math.Log(factor) / math.Log(chromiumZoomBase) | 444 | return math.Log(factor) / math.Log(chromiumZoomBase) | ||
| 444 | } | 445 | } | ||
| 445 | 446 | ||||
| 447 | + | func cefZoomFromPageAndBackingFactor(pageZoom, backingScale float64) float64 { | |||
| 448 | + | return cefZoomFromFactor(pageZoom / gtkutil.NormalizeScale(backingScale)) | |||
| 449 | + | } | |||
| 450 | + | ||||
| 451 | + | func pageZoomFromCEFAndBackingLevel(level, backingScale float64) float64 { | |||
| 452 | + | return factorFromCEFZoom(level) * gtkutil.NormalizeScale(backingScale) | |||
| 453 | + | } | |||
| 454 | + | ||||
| 446 | // factorFromCEFZoom converts a Chromium/CEF logarithmic zoom level back to a | 455 | // factorFromCEFZoom converts a Chromium/CEF logarithmic zoom level back to a | ||
| 447 | // linear zoom factor. | 456 | // linear zoom factor. | ||
| 448 | func factorFromCEFZoom(level float64) float64 { | 457 | func factorFromCEFZoom(level float64) float64 { | ||
| @@ -468,10 +477,12 @@ func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error { | |||||
| 468 | if host == nil { | 477 | if host == nil { | ||
| 469 | return errNoBrowser | 478 | return errNoBrowser | ||
| 470 | } | 479 | } | ||
| 471 | - | cefLevel := cefZoomFromFactor(factor) | 480 | + | backingScale := wv.osrBackingScaleFactor() |
| 481 | + | cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale) | |||
| 472 | logging.FromContext(wv.ctx).Debug(). | 482 | logging.FromContext(wv.ctx).Debug(). | ||
| 473 | Float64("factor", factor). | 483 | Float64("factor", factor). | ||
| 474 | Float64("cef_level", cefLevel). | 484 | Float64("cef_level", cefLevel). | ||
| 485 | + | Float64("osr_backing_scale", backingScale). | |||
| 475 | Msg("cef: SetZoomLevel") | 486 | Msg("cef: SetZoomLevel") | ||
| 476 | host.SetZoomLevel(cefLevel) | 487 | host.SetZoomLevel(cefLevel) | ||
| 477 | wv.zoomFactor.Store(factor) | 488 | wv.zoomFactor.Store(factor) | ||
| @@ -1376,15 +1387,18 @@ func (wv *WebView) selectedTextSnapshot() string { | |||||
| 1376 | return wv.selectedText | 1387 | return wv.selectedText | ||
| 1377 | } | 1388 | } | ||
| 1378 | 1389 | ||||
| 1379 | - | func (wv *WebView) viewBridgeScale() int32 { | 1390 | + | func (wv *WebView) viewBridgeScale() float64 { |
| 1380 | if wv == nil || wv.viewBridge == nil { | 1391 | if wv == nil || wv.viewBridge == nil { | ||
| 1381 | return 1 | 1392 | return 1 | ||
| 1382 | } | 1393 | } | ||
| 1383 | - | scale := int32(math.Round(float64(wv.viewBridge.DeviceScaleFactor()))) | 1394 | + | return gtkutil.NormalizeScale(float64(wv.viewBridge.DeviceScaleFactor())) |
| 1384 | - | if scale < 1 { | 1395 | + | } |
| 1396 | + | ||||
| 1397 | + | func (wv *WebView) osrBackingScaleFactor() float64 { | |||
| 1398 | + | if wv == nil || wv.viewBridge == nil { | |||
| 1385 | return 1 | 1399 | return 1 | ||
| 1386 | } | 1400 | } | ||
| 1387 | - | return scale | 1401 | + | return gtkutil.NormalizeScale(wv.viewBridge.OSRBackingScaleFactor()) |
| 1388 | } | 1402 | } | ||
| 1389 | 1403 | ||||
| 1390 | func (wv *WebView) handleMiddleClickFromBridge() bool { | 1404 | func (wv *WebView) handleMiddleClickFromBridge() bool { | ||
| @@ -1637,18 +1651,43 @@ func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) { | |||||
| 1637 | if host == nil { | 1651 | if host == nil { | ||
| 1638 | return | 1652 | return | ||
| 1639 | } | 1653 | } | ||
| 1654 | + | backingScale := wv.osrBackingScaleFactor() | |||
| 1640 | actualLevel := host.GetZoomLevel() | 1655 | actualLevel := host.GetZoomLevel() | ||
| 1641 | logging.FromContext(wv.ctx).Debug(). | 1656 | logging.FromContext(wv.ctx).Debug(). | ||
| 1642 | Int64("delay_ms", delayMs). | 1657 | Int64("delay_ms", delayMs). | ||
| 1643 | Float64("expected_factor", expectedFactor). | 1658 | Float64("expected_factor", expectedFactor). | ||
| 1644 | Float64("expected_cef_level", expectedLevel). | 1659 | Float64("expected_cef_level", expectedLevel). | ||
| 1645 | - | Float64("actual_factor", factorFromCEFZoom(actualLevel)). | 1660 | + | Float64("actual_factor", pageZoomFromCEFAndBackingLevel(actualLevel, backingScale)). |
| 1661 | + | Float64("actual_cef_factor", factorFromCEFZoom(actualLevel)). | |||
| 1646 | Float64("actual_cef_level", actualLevel). | 1662 | Float64("actual_cef_level", actualLevel). | ||
| 1663 | + | Float64("osr_backing_scale", backingScale). | |||
| 1647 | Msg("cef: zoom level readback") | 1664 | Msg("cef: zoom level readback") | ||
| 1648 | })), delayMs) | 1665 | })), delayMs) | ||
| 1649 | } | 1666 | } | ||
| 1650 | } | 1667 | } | ||
| 1651 | 1668 | ||||
| 1669 | + | func (wv *WebView) reapplyCurrentZoomForBackingScale(reason string) { | |||
| 1670 | + | if wv == nil || wv.destroyed.Load() { | |||
| 1671 | + | return | |||
| 1672 | + | } | |||
| 1673 | + | wv.mu.RLock() | |||
| 1674 | + | host := wv.host | |||
| 1675 | + | wv.mu.RUnlock() | |||
| 1676 | + | if host == nil { | |||
| 1677 | + | return | |||
| 1678 | + | } | |||
| 1679 | + | factor := wv.GetZoomLevel() | |||
| 1680 | + | backingScale := wv.osrBackingScaleFactor() | |||
| 1681 | + | cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale) | |||
| 1682 | + | host.SetZoomLevel(cefLevel) | |||
| 1683 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 1684 | + | Str("reason", reason). | |||
| 1685 | + | Float64("factor", factor). | |||
| 1686 | + | Float64("cef_level", cefLevel). | |||
| 1687 | + | Float64("osr_backing_scale", backingScale). | |||
| 1688 | + | Msg("cef: reapplied zoom for OSR backing scale") | |||
| 1689 | + | } | |||
| 1690 | + | ||||
| 1652 | func (wv *WebView) scheduleStartBeginFrameLoop() { | 1691 | func (wv *WebView) scheduleStartBeginFrameLoop() { | ||
| 1653 | if !externalBeginFrameEnabled() { | 1692 | if !externalBeginFrameEnabled() { | ||
| 1654 | return | 1693 | return | ||
M internal/infrastructure/cef/webview_audio_test.go +0 -2
| @@ -26,7 +26,6 @@ func TestEngine_WiresAudioOutputFactory(t *testing.T) { | |||
| 26 | 26 | ||
| 27 | 27 | // Verify webViewFactoryOptions can hold the audio factory | |
| 28 | 28 | opts := webViewFactoryOptions{ | |
| 29 | - | scale: 1, | |
| 30 | 29 | windowlessFrameRate: 60, | |
| 31 | 30 | audioOutputFactory: audioFactory, | |
| 32 | 31 | } | |
| @@ -44,7 +43,6 @@ func TestEngine_WiresAudioOutputFactory(t *testing.T) { | |||
| 44 | 43 | func TestWebViewFactory_AudioFactoryIsOptional(t *testing.T) { | |
| 45 | 44 | // Arrange | |
| 46 | 45 | opts := webViewFactoryOptions{ | |
| 47 | - | scale: 1, | |
| 48 | 46 | windowlessFrameRate: 60, | |
| 49 | 47 | audioOutputFactory: nil, | |
| 50 | 48 | } | |
| @@ -26,7 +26,6 @@ func TestEngine_WiresAudioOutputFactory(t *testing.T) { | |||||
| 26 | 26 | ||||
| 27 | // Verify webViewFactoryOptions can hold the audio factory | 27 | // Verify webViewFactoryOptions can hold the audio factory | ||
| 28 | opts := webViewFactoryOptions{ | 28 | opts := webViewFactoryOptions{ | ||
| 29 | - | scale: 1, | |||
| 30 | windowlessFrameRate: 60, | 29 | windowlessFrameRate: 60, | ||
| 31 | audioOutputFactory: audioFactory, | 30 | audioOutputFactory: audioFactory, | ||
| 32 | } | 31 | } | ||
| @@ -44,7 +43,6 @@ func TestEngine_WiresAudioOutputFactory(t *testing.T) { | |||||
| 44 | func TestWebViewFactory_AudioFactoryIsOptional(t *testing.T) { | 43 | func TestWebViewFactory_AudioFactoryIsOptional(t *testing.T) { | ||
| 45 | // Arrange | 44 | // Arrange | ||
| 46 | opts := webViewFactoryOptions{ | 45 | opts := webViewFactoryOptions{ | ||
| 47 | - | scale: 1, | |||
| 48 | windowlessFrameRate: 60, | 46 | windowlessFrameRate: 60, | ||
| 49 | audioOutputFactory: nil, | 47 | audioOutputFactory: nil, | ||
| 50 | } | 48 | } | ||
M internal/infrastructure/cef/webview_handlers.go +17 -4
| @@ -16,8 +16,9 @@ import ( | |||
| 16 | 16 | ||
| 17 | 17 | // Console message markers used by injected JavaScript for log filtering. | |
| 18 | 18 | const ( | |
| 19 | - | consoleMarkerVideoDiag = "[VIDEO-DIAG]" | |
| 20 | - | consoleMarkerAutoCopy = "[AUTO-COPY]" | |
| 19 | + | consoleMarkerVideoDiag = "[VIDEO-DIAG]" | |
| 20 | + | consoleMarkerAutoCopy = "[AUTO-COPY]" | |
| 21 | + | consoleMarkerScaleProbe = "[SCALE-PROBE]" | |
| 21 | 22 | ) | |
| 22 | 23 | ||
| 23 | 24 | // handlerSet implements all CEF handler interfaces and dispatches events to the | |
| @@ -226,7 +227,8 @@ func (h *handlerSet) OnConsoleMessage( | |||
| 226 | 227 | ) int32 { | |
| 227 | 228 | if h.wv != nil && h.wv.ctx != nil && | |
| 228 | 229 | (strings.Contains(message, consoleMarkerVideoDiag) || | |
| 229 | - | strings.Contains(message, consoleMarkerAutoCopy)) { | |
| 230 | + | strings.Contains(message, consoleMarkerAutoCopy) || | |
| 231 | + | strings.Contains(message, consoleMarkerScaleProbe)) { | |
| 230 | 232 | log := logging.FromContext(h.wv.ctx).With(). | |
| 231 | 233 | Str("component", "cef-console"). | |
| 232 | 234 | Str("source", source). | |
| @@ -399,6 +401,14 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||
| 399 | 401 | // Inject scripts and styles after page load. | |
| 400 | 402 | // Must run on GTK thread — OnLoadEnd fires on the CEF IO thread, | |
| 401 | 403 | // and JavaScript injection requires the main thread. | |
| 404 | + | if cefScaleProbeEnabled() && httpStatusCode >= 0 && !strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") { | |
| 405 | + | // Do not retain/use CEF callback-scoped frame wrappers across the GTK idle hop. | |
| 406 | + | // Re-read the current main frame through WebView.RunJavaScript instead. | |
| 407 | + | h.wv.runOnGTK(func() { | |
| 408 | + | logging.FromContext(h.wv.ctx).Debug().Msg("cef: injecting scale probe") | |
| 409 | + | h.wv.RunJavaScript(context.Background(), cefScaleProbeScript) | |
| 410 | + | }) | |
| 411 | + | } | |
| 402 | 412 | if h.wv.engine != nil && h.wv.engine.contentInj != nil { | |
| 403 | 413 | h.wv.runOnGTK(func() { | |
| 404 | 414 | h.wv.engine.contentInj.onLoadEnd(h.wv) | |
| @@ -630,7 +640,7 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||
| 630 | 640 | return | |
| 631 | 641 | } | |
| 632 | 642 | if err := wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{ | |
| 633 | - | Scale: wv.viewBridgeScale(), | |
| 643 | + | Scale: 0, | |
| 634 | 644 | OnMiddleClick: func(_, _ float64) bool { | |
| 635 | 645 | return wv.handleMiddleClickFromBridge() | |
| 636 | 646 | }, | |
| @@ -656,6 +666,9 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||
| 656 | 666 | // Mark browser as visible — CEF OSR starts in hidden state and suppresses | |
| 657 | 667 | // painting/caret updates until explicitly told the browser is shown. | |
| 658 | 668 | host.WasHidden(0) | |
| 669 | + | if h.wv != nil { | |
| 670 | + | h.wv.SyncViewport(h.wv.ctx, "after-created") | |
| 671 | + | } | |
| 659 | 672 | return state | |
| 660 | 673 | } | |
| 661 | 674 | ||
| @@ -16,8 +16,9 @@ import ( | |||||
| 16 | 16 | ||||
| 17 | // Console message markers used by injected JavaScript for log filtering. | 17 | // Console message markers used by injected JavaScript for log filtering. | ||
| 18 | const ( | 18 | const ( | ||
| 19 | - | consoleMarkerVideoDiag = "[VIDEO-DIAG]" | 19 | + | consoleMarkerVideoDiag = "[VIDEO-DIAG]" |
| 20 | - | consoleMarkerAutoCopy = "[AUTO-COPY]" | 20 | + | consoleMarkerAutoCopy = "[AUTO-COPY]" |
| 21 | + | consoleMarkerScaleProbe = "[SCALE-PROBE]" | |||
| 21 | ) | 22 | ) | ||
| 22 | 23 | ||||
| 23 | // handlerSet implements all CEF handler interfaces and dispatches events to the | 24 | // handlerSet implements all CEF handler interfaces and dispatches events to the | ||
| @@ -226,7 +227,8 @@ func (h *handlerSet) OnConsoleMessage( | |||||
| 226 | ) int32 { | 227 | ) int32 { | ||
| 227 | if h.wv != nil && h.wv.ctx != nil && | 228 | if h.wv != nil && h.wv.ctx != nil && | ||
| 228 | (strings.Contains(message, consoleMarkerVideoDiag) || | 229 | (strings.Contains(message, consoleMarkerVideoDiag) || | ||
| 229 | - | strings.Contains(message, consoleMarkerAutoCopy)) { | 230 | + | strings.Contains(message, consoleMarkerAutoCopy) || |
| 231 | + | strings.Contains(message, consoleMarkerScaleProbe)) { | |||
| 230 | log := logging.FromContext(h.wv.ctx).With(). | 232 | log := logging.FromContext(h.wv.ctx).With(). | ||
| 231 | Str("component", "cef-console"). | 233 | Str("component", "cef-console"). | ||
| 232 | Str("source", source). | 234 | Str("source", source). | ||
| @@ -399,6 +401,14 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||||
| 399 | // Inject scripts and styles after page load. | 401 | // Inject scripts and styles after page load. | ||
| 400 | // Must run on GTK thread — OnLoadEnd fires on the CEF IO thread, | 402 | // Must run on GTK thread — OnLoadEnd fires on the CEF IO thread, | ||
| 401 | // and JavaScript injection requires the main thread. | 403 | // and JavaScript injection requires the main thread. | ||
| 404 | + | if cefScaleProbeEnabled() && httpStatusCode >= 0 && !strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") { | |||
| 405 | + | // Do not retain/use CEF callback-scoped frame wrappers across the GTK idle hop. | |||
| 406 | + | // Re-read the current main frame through WebView.RunJavaScript instead. | |||
| 407 | + | h.wv.runOnGTK(func() { | |||
| 408 | + | logging.FromContext(h.wv.ctx).Debug().Msg("cef: injecting scale probe") | |||
| 409 | + | h.wv.RunJavaScript(context.Background(), cefScaleProbeScript) | |||
| 410 | + | }) | |||
| 411 | + | } | |||
| 402 | if h.wv.engine != nil && h.wv.engine.contentInj != nil { | 412 | if h.wv.engine != nil && h.wv.engine.contentInj != nil { | ||
| 403 | h.wv.runOnGTK(func() { | 413 | h.wv.runOnGTK(func() { | ||
| 404 | h.wv.engine.contentInj.onLoadEnd(h.wv) | 414 | h.wv.engine.contentInj.onLoadEnd(h.wv) | ||
| @@ -630,7 +640,7 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||||
| 630 | return | 640 | return | ||
| 631 | } | 641 | } | ||
| 632 | if err := wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{ | 642 | if err := wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{ | ||
| 633 | - | Scale: wv.viewBridgeScale(), | 643 | + | Scale: 0, |
| 634 | OnMiddleClick: func(_, _ float64) bool { | 644 | OnMiddleClick: func(_, _ float64) bool { | ||
| 635 | return wv.handleMiddleClickFromBridge() | 645 | return wv.handleMiddleClickFromBridge() | ||
| 636 | }, | 646 | }, | ||
| @@ -656,6 +666,9 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||||
| 656 | // Mark browser as visible — CEF OSR starts in hidden state and suppresses | 666 | // Mark browser as visible — CEF OSR starts in hidden state and suppresses | ||
| 657 | // painting/caret updates until explicitly told the browser is shown. | 667 | // painting/caret updates until explicitly told the browser is shown. | ||
| 658 | host.WasHidden(0) | 668 | host.WasHidden(0) | ||
| 669 | + | if h.wv != nil { | |||
| 670 | + | h.wv.SyncViewport(h.wv.ctx, "after-created") | |||
| 671 | + | } | |||
| 659 | return state | 672 | return state | ||
| 660 | } | 673 | } | ||
| 661 | 674 | ||||
M internal/infrastructure/cef/webview_viewport.go +8 -1
| @@ -101,7 +101,7 @@ func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool | |||
| 101 | 101 | ||
| 102 | 102 | widget := wv.viewBridge.Widget() | |
| 103 | 103 | visible := widget != nil && widget.IsVisible() | |
| 104 | - | notifyBrowserViewportSync(host, visible) | |
| 104 | + | wv.notifyViewportSyncOnCEFUIThread(host, visible) | |
| 105 | 105 | logging.FromContext(ctx).Debug(). | |
| 106 | 106 | Uint64("webview_id", uint64(wv.id)). | |
| 107 | 107 | Bool("visible", visible). | |
| @@ -264,6 +264,13 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |||
| 264 | 264 | wv.viewportRealizeFunc = nil | |
| 265 | 265 | } | |
| 266 | 266 | ||
| 267 | + | func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost, visible bool) { | |
| 268 | + | notifyBrowserViewportSync(host, visible) | |
| 269 | + | if wv != nil { | |
| 270 | + | wv.reapplyCurrentZoomForBackingScale("viewport-sync") | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | ||
| 267 | 274 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | |
| 268 | 275 | if host == nil { | |
| 269 | 276 | return | |
| @@ -101,7 +101,7 @@ func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool | |||||
| 101 | 101 | ||||
| 102 | widget := wv.viewBridge.Widget() | 102 | widget := wv.viewBridge.Widget() | ||
| 103 | visible := widget != nil && widget.IsVisible() | 103 | visible := widget != nil && widget.IsVisible() | ||
| 104 | - | notifyBrowserViewportSync(host, visible) | 104 | + | wv.notifyViewportSyncOnCEFUIThread(host, visible) |
| 105 | logging.FromContext(ctx).Debug(). | 105 | logging.FromContext(ctx).Debug(). | ||
| 106 | Uint64("webview_id", uint64(wv.id)). | 106 | Uint64("webview_id", uint64(wv.id)). | ||
| 107 | Bool("visible", visible). | 107 | Bool("visible", visible). | ||
| @@ -264,6 +264,13 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |||||
| 264 | wv.viewportRealizeFunc = nil | 264 | wv.viewportRealizeFunc = nil | ||
| 265 | } | 265 | } | ||
| 266 | 266 | ||||
| 267 | + | func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost, visible bool) { | |||
| 268 | + | notifyBrowserViewportSync(host, visible) | |||
| 269 | + | if wv != nil { | |||
| 270 | + | wv.reapplyCurrentZoomForBackingScale("viewport-sync") | |||
| 271 | + | } | |||
| 272 | + | } | |||
| 273 | + | ||||
| 267 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | 274 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | ||
| 268 | if host == nil { | 275 | if host == nil { | ||
| 269 | return | 276 | return | ||
M internal/infrastructure/cef/webview_zoom_test.go +10 -0
| @@ -17,3 +17,13 @@ func TestZoomConversionsRoundTrip(t *testing.T) { | |||
| 17 | 17 | }) | |
| 18 | 18 | } | |
| 19 | 19 | } | |
| 20 | + | ||
| 21 | + | func TestZoomConversionsCompensateOSRBackingScale(t *testing.T) { | |
| 22 | + | level := cefZoomFromPageAndBackingFactor(1.0, 1.2) | |
| 23 | + | assert.InDelta(t, 1.0/1.2, factorFromCEFZoom(level), 1e-9) | |
| 24 | + | assert.InDelta(t, 1.0, pageZoomFromCEFAndBackingLevel(level, 1.2), 1e-9) | |
| 25 | + | ||
| 26 | + | level = cefZoomFromPageAndBackingFactor(1.75, 1.2) | |
| 27 | + | assert.InDelta(t, 1.75/1.2, factorFromCEFZoom(level), 1e-9) | |
| 28 | + | assert.InDelta(t, 1.75, pageZoomFromCEFAndBackingLevel(level, 1.2), 1e-9) | |
| 29 | + | } | |
| @@ -17,3 +17,13 @@ func TestZoomConversionsRoundTrip(t *testing.T) { | |||||
| 17 | }) | 17 | }) | ||
| 18 | } | 18 | } | ||
| 19 | } | 19 | } | ||
| 20 | + | ||||
| 21 | + | func TestZoomConversionsCompensateOSRBackingScale(t *testing.T) { | |||
| 22 | + | level := cefZoomFromPageAndBackingFactor(1.0, 1.2) | |||
| 23 | + | assert.InDelta(t, 1.0/1.2, factorFromCEFZoom(level), 1e-9) | |||
| 24 | + | assert.InDelta(t, 1.0, pageZoomFromCEFAndBackingLevel(level, 1.2), 1e-9) | |||
| 25 | + | ||||
| 26 | + | level = cefZoomFromPageAndBackingFactor(1.75, 1.2) | |||
| 27 | + | assert.InDelta(t, 1.75/1.2, factorFromCEFZoom(level), 1e-9) | |||
| 28 | + | assert.InDelta(t, 1.75, pageZoomFromCEFAndBackingLevel(level, 1.2), 1e-9) | |||
| 29 | + | } | |||
A internal/infrastructure/gtkutil/scale.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | package gtkutil | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // NormalizeScale clamps invalid GTK/GDK scale values to 1. | |
| 10 | + | func NormalizeScale(scale float64) float64 { | |
| 11 | + | if math.IsNaN(scale) || math.IsInf(scale, 0) || scale <= 0 { | |
| 12 | + | return 1 | |
| 13 | + | } | |
| 14 | + | return scale | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // WidgetFromNativePointer wraps a native GTK widget pointer. | |
| 18 | + | func WidgetFromNativePointer(ptr uintptr) *gtk.Widget { | |
| 19 | + | if ptr == 0 { | |
| 20 | + | return nil | |
| 21 | + | } | |
| 22 | + | return gtk.WidgetNewFromInternalPtr(ptr) | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // DeviceToLogical converts a device-pixel coordinate into a logical coordinate. | |
| 26 | + | func DeviceToLogical(value int32, scale float64) int32 { | |
| 27 | + | return int32(math.Floor(float64(value) / NormalizeScale(scale))) | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | package gtkutil | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "math" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/puregotk/v4/gtk" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // NormalizeScale clamps invalid GTK/GDK scale values to 1. | |||
| 10 | + | func NormalizeScale(scale float64) float64 { | |||
| 11 | + | if math.IsNaN(scale) || math.IsInf(scale, 0) || scale <= 0 { | |||
| 12 | + | return 1 | |||
| 13 | + | } | |||
| 14 | + | return scale | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // WidgetFromNativePointer wraps a native GTK widget pointer. | |||
| 18 | + | func WidgetFromNativePointer(ptr uintptr) *gtk.Widget { | |||
| 19 | + | if ptr == 0 { | |||
| 20 | + | return nil | |||
| 21 | + | } | |||
| 22 | + | return gtk.WidgetNewFromInternalPtr(ptr) | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // DeviceToLogical converts a device-pixel coordinate into a logical coordinate. | |||
| 26 | + | func DeviceToLogical(value int32, scale float64) int32 { | |||
| 27 | + | return int32(math.Floor(float64(value) / NormalizeScale(scale))) | |||
| 28 | + | } | |||
A internal/infrastructure/gtkutil/scale_test.go +18 -0
| @@ -0,0 +1,18 @@ | |||
| 1 | + | package gtkutil | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | func TestNormalizeScale(t *testing.T) { | |
| 6 | + | if got := NormalizeScale(0); got != 1 { | |
| 7 | + | t.Fatalf("NormalizeScale(0) = %v, want 1", got) | |
| 8 | + | } | |
| 9 | + | if got := NormalizeScale(1.2); got != 1.2 { | |
| 10 | + | t.Fatalf("NormalizeScale(1.2) = %v, want 1.2", got) | |
| 11 | + | } | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | func TestDeviceLogicalConversions(t *testing.T) { | |
| 15 | + | if got := DeviceToLogical(320, 1.25); got != 256 { | |
| 16 | + | t.Fatalf("DeviceToLogical(320, 1.25) = %d, want 256", got) | |
| 17 | + | } | |
| 18 | + | } | |
| @@ -0,0 +1,18 @@ | |||||
| 1 | + | package gtkutil | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | func TestNormalizeScale(t *testing.T) { | |||
| 6 | + | if got := NormalizeScale(0); got != 1 { | |||
| 7 | + | t.Fatalf("NormalizeScale(0) = %v, want 1", got) | |||
| 8 | + | } | |||
| 9 | + | if got := NormalizeScale(1.2); got != 1.2 { | |||
| 10 | + | t.Fatalf("NormalizeScale(1.2) = %v, want 1.2", got) | |||
| 11 | + | } | |||
| 12 | + | } | |||
| 13 | + | ||||
| 14 | + | func TestDeviceLogicalConversions(t *testing.T) { | |||
| 15 | + | if got := DeviceToLogical(320, 1.25); got != 256 { | |||
| 16 | + | t.Fatalf("DeviceToLogical(320, 1.25) = %d, want 256", got) | |||
| 17 | + | } | |||
| 18 | + | } | |||
M internal/ui/coordinator/content/lifecycle.go +5 -3
| @@ -6,11 +6,11 @@ import ( | |||
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/gtkutil" | |
| 9 | 10 | "github.com/bnema/dumber/internal/logging" | |
| 10 | 11 | "github.com/bnema/dumber/internal/ui/component" | |
| 11 | 12 | "github.com/bnema/dumber/internal/ui/input" | |
| 12 | 13 | "github.com/bnema/dumber/internal/ui/layout" | |
| 13 | - | "github.com/bnema/puregotk/v4/gtk" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 16 | // EnsureWebView acquires or reuses a WebView for the given pane. | |
| @@ -167,8 +167,10 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Wi | |||
| 167 | 167 | return nil | |
| 168 | 168 | } | |
| 169 | 169 | ||
| 170 | - | gtkWidget := >k.Widget{} | |
| 171 | - | gtkWidget.Ptr = ptr | |
| 170 | + | gtkWidget := gtkutil.WidgetFromNativePointer(ptr) | |
| 171 | + | if gtkWidget == nil { | |
| 172 | + | return nil | |
| 173 | + | } | |
| 172 | 174 | widget := c.widgetFactory.WrapWidget(gtkWidget) | |
| 173 | 175 | ||
| 174 | 176 | // Attach gesture handler for mouse button 8/9 navigation | |
| @@ -6,11 +6,11 @@ import ( | |||||
| 6 | 6 | ||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 7 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/gtkutil" | |||
| 9 | "github.com/bnema/dumber/internal/logging" | 10 | "github.com/bnema/dumber/internal/logging" | ||
| 10 | "github.com/bnema/dumber/internal/ui/component" | 11 | "github.com/bnema/dumber/internal/ui/component" | ||
| 11 | "github.com/bnema/dumber/internal/ui/input" | 12 | "github.com/bnema/dumber/internal/ui/input" | ||
| 12 | "github.com/bnema/dumber/internal/ui/layout" | 13 | "github.com/bnema/dumber/internal/ui/layout" | ||
| 13 | - | "github.com/bnema/puregotk/v4/gtk" | |||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | // EnsureWebView acquires or reuses a WebView for the given pane. | 16 | // EnsureWebView acquires or reuses a WebView for the given pane. | ||
| @@ -167,8 +167,10 @@ func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Wi | |||||
| 167 | return nil | 167 | return nil | ||
| 168 | } | 168 | } | ||
| 169 | 169 | ||||
| 170 | - | gtkWidget := >k.Widget{} | 170 | + | gtkWidget := gtkutil.WidgetFromNativePointer(ptr) |
| 171 | - | gtkWidget.Ptr = ptr | 171 | + | if gtkWidget == nil { |
| 172 | + | return nil | |||
| 173 | + | } | |||
| 172 | widget := c.widgetFactory.WrapWidget(gtkWidget) | 174 | widget := c.widgetFactory.WrapWidget(gtkWidget) | ||
| 173 | 175 | ||||
| 174 | // Attach gesture handler for mouse button 8/9 navigation | 176 | // Attach gesture handler for mouse button 8/9 navigation | ||