detached 0fa06a1

fix: tighten CEF scale helpers

commit
0fa06a1116f716446411f33a6c6e34ae76eeff80
author
brice · · 2026-05-16 15:07 UTC
parent
b6c23e2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +67 -33
M internal/infrastructure/cef/context_menu.go +1 -2
@@ -8,7 +8,6 @@ import (
88 purecef "github.com/bnema/purego-cef/cef"
99
1010 "github.com/bnema/dumber/internal/application/port"
11- "github.com/bnema/dumber/internal/infrastructure/gtkutil"
1211 "github.com/bnema/dumber/internal/logging"
1312 "github.com/bnema/puregotk/v4/gtk"
1413 )
@@ -134,7 +133,7 @@ func contextMenuAnchorPosition(params purecef.ContextMenuParams, scale, backingS
134133 if backingScale <= 1 {
135134 return params.GetXcoord(), params.GetYcoord()
136135 }
137- return gtkutil.DeviceToLogical(params.GetXcoord(), scale), gtkutil.DeviceToLogical(params.GetYcoord(), scale)
136+ return deviceToLogicalCoord(params.GetXcoord(), scale), deviceToLogicalCoord(params.GetYcoord(), scale)
138137 }
139138
140139 func logContextMenuPopupRequest(
@@ -8,7 +8,6 @@ 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"
12 "github.com/bnema/dumber/internal/logging"11 "github.com/bnema/dumber/internal/logging"
13 "github.com/bnema/puregotk/v4/gtk"12 "github.com/bnema/puregotk/v4/gtk"
14 )13 )
@@ -134,7 +133,7 @@ func contextMenuAnchorPosition(params purecef.ContextMenuParams, scale, backingS
134 if backingScale <= 1 {133 if backingScale <= 1 {
135 return params.GetXcoord(), params.GetYcoord()134 return params.GetXcoord(), params.GetYcoord()
136 }135 }
137- return gtkutil.DeviceToLogical(params.GetXcoord(), scale), gtkutil.DeviceToLogical(params.GetYcoord(), scale)136+ return deviceToLogicalCoord(params.GetXcoord(), scale), deviceToLogicalCoord(params.GetYcoord(), scale)
138 }137 }
139 138
140 func logContextMenuPopupRequest(139 func logContextMenuPopupRequest(
A internal/infrastructure/cef/scale.go +14 -0
@@ -0,0 +1,14 @@
1+package cef
2+
3+import "math"
4+
5+func normalizeScale(scale float64) float64 {
6+ if math.IsNaN(scale) || math.IsInf(scale, 0) || scale <= 0 {
7+ return 1
8+ }
9+ return scale
10+}
11+
12+func deviceToLogicalCoord(value int32, scale float64) int32 {
13+ return int32(math.Floor(float64(value) / normalizeScale(scale)))
14+}
@@ -0,0 +1,14 @@
1+package cef
2+
3+import "math"
4+
5+func normalizeScale(scale float64) float64 {
6+ if math.IsNaN(scale) || math.IsInf(scale, 0) || scale <= 0 {
7+ return 1
8+ }
9+ return scale
10+}
11+
12+func deviceToLogicalCoord(value int32, scale float64) int32 {
13+ return int32(math.Floor(float64(value) / normalizeScale(scale)))
14+}
M internal/infrastructure/cef/webview.go +22 -19
@@ -20,7 +20,6 @@ import (
2020
2121 "github.com/bnema/dumber/internal/application/dto"
2222 "github.com/bnema/dumber/internal/application/port"
23- "github.com/bnema/dumber/internal/infrastructure/gtkutil"
2423 "github.com/bnema/dumber/internal/logging"
2524 )
2625
@@ -445,11 +444,27 @@ func cefZoomFromFactor(factor float64) float64 {
445444 }
446445
447446 func cefZoomFromPageAndBackingFactor(pageZoom, backingScale float64) float64 {
448- return cefZoomFromFactor(pageZoom / gtkutil.NormalizeScale(backingScale))
447+ return cefZoomFromFactor(pageZoom / normalizeScale(backingScale))
449448 }
450449
451450 func pageZoomFromCEFAndBackingLevel(level, backingScale float64) float64 {
452- return factorFromCEFZoom(level) * gtkutil.NormalizeScale(backingScale)
451+ return factorFromCEFZoom(level) * normalizeScale(backingScale)
452+}
453+
454+func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel float64) {
455+ host.SetZoomLevel(cefLevel)
456+ // Force CEF to produce a new frame at the new zoom level. In OSR mode,
457+ // SetZoomLevel changes the Blink layout zoom but doesn't guarantee a
458+ // repaint. WasResized is a no-op when view dimensions haven't changed.
459+ // NotifyScreenInfoChanged forces surface ID invalidation + a full
460+ // SynchronizeVisualProperties cycle, which makes the renderer produce
461+ // a new compositor frame at the new zoom level.
462+ host.NotifyScreenInfoChanged()
463+ // Zoom is applied asynchronously in the renderer process. Request a couple
464+ // of follow-up refreshes on the CEF UI thread so OSR captures the updated
465+ // compositor frame after the zoom IPC has been processed.
466+ wv.scheduleZoomRefresh()
467+ wv.scheduleZoomReadback(factor, cefLevel)
453468 }
454469
455470 // factorFromCEFZoom converts a Chromium/CEF logarithmic zoom level back to a
@@ -484,20 +499,8 @@ func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error {
484499 Float64("cef_level", cefLevel).
485500 Float64("osr_backing_scale", backingScale).
486501 Msg("cef: SetZoomLevel")
487- host.SetZoomLevel(cefLevel)
502+ wv.applyCEFZoomLevel(host, factor, cefLevel)
488503 wv.zoomFactor.Store(factor)
489- // Force CEF to produce a new frame at the new zoom level. In OSR mode,
490- // SetZoomLevel changes the Blink layout zoom but doesn't guarantee a
491- // repaint. WasResized is a no-op when view dimensions haven't changed.
492- // NotifyScreenInfoChanged forces surface ID invalidation + a full
493- // SynchronizeVisualProperties cycle, which makes the renderer produce
494- // a new compositor frame at the new zoom level.
495- host.NotifyScreenInfoChanged()
496- // Zoom is applied asynchronously in the renderer process. Request a couple
497- // of follow-up refreshes on the CEF UI thread so OSR captures the updated
498- // compositor frame after the zoom IPC has been processed.
499- wv.scheduleZoomRefresh()
500- wv.scheduleZoomReadback(factor, cefLevel)
501504 return nil
502505 }
503506
@@ -1391,14 +1394,14 @@ func (wv *WebView) viewBridgeScale() float64 {
13911394 if wv == nil || wv.viewBridge == nil {
13921395 return 1
13931396 }
1394- return gtkutil.NormalizeScale(float64(wv.viewBridge.DeviceScaleFactor()))
1397+ return normalizeScale(float64(wv.viewBridge.DeviceScaleFactor()))
13951398 }
13961399
13971400 func (wv *WebView) osrBackingScaleFactor() float64 {
13981401 if wv == nil || wv.viewBridge == nil {
13991402 return 1
14001403 }
1401- return gtkutil.NormalizeScale(wv.viewBridge.OSRBackingScaleFactor())
1404+ return normalizeScale(wv.viewBridge.OSRBackingScaleFactor())
14021405 }
14031406
14041407 func (wv *WebView) handleMiddleClickFromBridge() bool {
@@ -1679,7 +1682,7 @@ func (wv *WebView) reapplyCurrentZoomForBackingScale(reason string) {
16791682 factor := wv.GetZoomLevel()
16801683 backingScale := wv.osrBackingScaleFactor()
16811684 cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale)
1682- host.SetZoomLevel(cefLevel)
1685+ wv.applyCEFZoomLevel(host, factor, cefLevel)
16831686 logging.FromContext(wv.ctx).Debug().
16841687 Str("reason", reason).
16851688 Float64("factor", factor).
@@ -20,7 +20,6 @@ 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"
24 "github.com/bnema/dumber/internal/logging"23 "github.com/bnema/dumber/internal/logging"
25 )24 )
26 25
@@ -445,11 +444,27 @@ func cefZoomFromFactor(factor float64) float64 {
445 }444 }
446 445
447 func cefZoomFromPageAndBackingFactor(pageZoom, backingScale float64) float64 {446 func cefZoomFromPageAndBackingFactor(pageZoom, backingScale float64) float64 {
448- return cefZoomFromFactor(pageZoom / gtkutil.NormalizeScale(backingScale))447+ return cefZoomFromFactor(pageZoom / normalizeScale(backingScale))
449 }448 }
450 449
451 func pageZoomFromCEFAndBackingLevel(level, backingScale float64) float64 {450 func pageZoomFromCEFAndBackingLevel(level, backingScale float64) float64 {
452- return factorFromCEFZoom(level) * gtkutil.NormalizeScale(backingScale)451+ return factorFromCEFZoom(level) * normalizeScale(backingScale)
452+}
453+
454+func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel float64) {
455+ host.SetZoomLevel(cefLevel)
456+ // Force CEF to produce a new frame at the new zoom level. In OSR mode,
457+ // SetZoomLevel changes the Blink layout zoom but doesn't guarantee a
458+ // repaint. WasResized is a no-op when view dimensions haven't changed.
459+ // NotifyScreenInfoChanged forces surface ID invalidation + a full
460+ // SynchronizeVisualProperties cycle, which makes the renderer produce
461+ // a new compositor frame at the new zoom level.
462+ host.NotifyScreenInfoChanged()
463+ // Zoom is applied asynchronously in the renderer process. Request a couple
464+ // of follow-up refreshes on the CEF UI thread so OSR captures the updated
465+ // compositor frame after the zoom IPC has been processed.
466+ wv.scheduleZoomRefresh()
467+ wv.scheduleZoomReadback(factor, cefLevel)
453 }468 }
454 469
455 // factorFromCEFZoom converts a Chromium/CEF logarithmic zoom level back to a470 // factorFromCEFZoom converts a Chromium/CEF logarithmic zoom level back to a
@@ -484,20 +499,8 @@ func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error {
484 Float64("cef_level", cefLevel).499 Float64("cef_level", cefLevel).
485 Float64("osr_backing_scale", backingScale).500 Float64("osr_backing_scale", backingScale).
486 Msg("cef: SetZoomLevel")501 Msg("cef: SetZoomLevel")
487- host.SetZoomLevel(cefLevel)502+ wv.applyCEFZoomLevel(host, factor, cefLevel)
488 wv.zoomFactor.Store(factor)503 wv.zoomFactor.Store(factor)
489- // Force CEF to produce a new frame at the new zoom level. In OSR mode,
490- // SetZoomLevel changes the Blink layout zoom but doesn't guarantee a
491- // repaint. WasResized is a no-op when view dimensions haven't changed.
492- // NotifyScreenInfoChanged forces surface ID invalidation + a full
493- // SynchronizeVisualProperties cycle, which makes the renderer produce
494- // a new compositor frame at the new zoom level.
495- host.NotifyScreenInfoChanged()
496- // Zoom is applied asynchronously in the renderer process. Request a couple
497- // of follow-up refreshes on the CEF UI thread so OSR captures the updated
498- // compositor frame after the zoom IPC has been processed.
499- wv.scheduleZoomRefresh()
500- wv.scheduleZoomReadback(factor, cefLevel)
501 return nil504 return nil
502 }505 }
503 506
@@ -1391,14 +1394,14 @@ func (wv *WebView) viewBridgeScale() float64 {
1391 if wv == nil || wv.viewBridge == nil {1394 if wv == nil || wv.viewBridge == nil {
1392 return 11395 return 1
1393 }1396 }
1394- return gtkutil.NormalizeScale(float64(wv.viewBridge.DeviceScaleFactor()))1397+ return normalizeScale(float64(wv.viewBridge.DeviceScaleFactor()))
1395 }1398 }
1396 1399
1397 func (wv *WebView) osrBackingScaleFactor() float64 {1400 func (wv *WebView) osrBackingScaleFactor() float64 {
1398 if wv == nil || wv.viewBridge == nil {1401 if wv == nil || wv.viewBridge == nil {
1399 return 11402 return 1
1400 }1403 }
1401- return gtkutil.NormalizeScale(wv.viewBridge.OSRBackingScaleFactor())1404+ return normalizeScale(wv.viewBridge.OSRBackingScaleFactor())
1402 }1405 }
1403 1406
1404 func (wv *WebView) handleMiddleClickFromBridge() bool {1407 func (wv *WebView) handleMiddleClickFromBridge() bool {
@@ -1679,7 +1682,7 @@ func (wv *WebView) reapplyCurrentZoomForBackingScale(reason string) {
1679 factor := wv.GetZoomLevel()1682 factor := wv.GetZoomLevel()
1680 backingScale := wv.osrBackingScaleFactor()1683 backingScale := wv.osrBackingScaleFactor()
1681 cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale)1684 cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale)
1682- host.SetZoomLevel(cefLevel)1685+ wv.applyCEFZoomLevel(host, factor, cefLevel)
1683 logging.FromContext(wv.ctx).Debug().1686 logging.FromContext(wv.ctx).Debug().
1684 Str("reason", reason).1687 Str("reason", reason).
1685 Float64("factor", factor).1688 Float64("factor", factor).
M internal/infrastructure/cef/webview_zoom_test.go +9 -7
@@ -19,11 +19,13 @@ func TestZoomConversionsRoundTrip(t *testing.T) {
1919 }
2020
2121 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)
22+ for _, backing := range []float64{1.0, 1.25, 1.5, 2.0} {
23+ for _, pageZoom := range []float64{1.0, 1.75} {
24+ t.Run(fmt.Sprintf("backing_%.2f_page_%.2f", backing, pageZoom), func(t *testing.T) {
25+ level := cefZoomFromPageAndBackingFactor(pageZoom, backing)
26+ assert.InDelta(t, pageZoom/backing, factorFromCEFZoom(level), 1e-9)
27+ assert.InDelta(t, pageZoom, pageZoomFromCEFAndBackingLevel(level, backing), 1e-9)
28+ })
29+ }
30+ }
2931 }
@@ -19,11 +19,13 @@ func TestZoomConversionsRoundTrip(t *testing.T) {
19 }19 }
20 20
21 func TestZoomConversionsCompensateOSRBackingScale(t *testing.T) {21 func TestZoomConversionsCompensateOSRBackingScale(t *testing.T) {
22- level := cefZoomFromPageAndBackingFactor(1.0, 1.2)22+ for _, backing := range []float64{1.0, 1.25, 1.5, 2.0} {
23- assert.InDelta(t, 1.0/1.2, factorFromCEFZoom(level), 1e-9)23+ for _, pageZoom := range []float64{1.0, 1.75} {
24- assert.InDelta(t, 1.0, pageZoomFromCEFAndBackingLevel(level, 1.2), 1e-9)24+ t.Run(fmt.Sprintf("backing_%.2f_page_%.2f", backing, pageZoom), func(t *testing.T) {
25-25+ level := cefZoomFromPageAndBackingFactor(pageZoom, backing)
26- level = cefZoomFromPageAndBackingFactor(1.75, 1.2)26+ assert.InDelta(t, pageZoom/backing, factorFromCEFZoom(level), 1e-9)
27- assert.InDelta(t, 1.75/1.2, factorFromCEFZoom(level), 1e-9)27+ assert.InDelta(t, pageZoom, pageZoomFromCEFAndBackingLevel(level, backing), 1e-9)
28- assert.InDelta(t, 1.75, pageZoomFromCEFAndBackingLevel(level, 1.2), 1e-9)28+ })
29+ }
30+ }
29 }31 }
M internal/infrastructure/gtkutil/scale_test.go +21 -5
@@ -1,13 +1,29 @@
11 package gtkutil
22
3-import "testing"
3+import (
4+ "math"
5+ "testing"
6+)
47
58 func TestNormalizeScale(t *testing.T) {
6- if got := NormalizeScale(0); got != 1 {
7- t.Fatalf("NormalizeScale(0) = %v, want 1", got)
9+ tests := []struct {
10+ name string
11+ input float64
12+ want float64
13+ }{
14+ {name: "zero", input: 0, want: 1},
15+ {name: "fractional", input: 1.2, want: 1.2},
16+ {name: "nan", input: math.NaN(), want: 1},
17+ {name: "positive_infinity", input: math.Inf(1), want: 1},
18+ {name: "negative_infinity", input: math.Inf(-1), want: 1},
19+ {name: "negative", input: -1, want: 1},
820 }
9- if got := NormalizeScale(1.2); got != 1.2 {
10- t.Fatalf("NormalizeScale(1.2) = %v, want 1.2", got)
21+ for _, tt := range tests {
22+ t.Run(tt.name, func(t *testing.T) {
23+ if got := NormalizeScale(tt.input); got != tt.want {
24+ t.Fatalf("NormalizeScale(%v) = %v, want %v", tt.input, got, tt.want)
25+ }
26+ })
1127 }
1228 }
1329
@@ -1,13 +1,29 @@
1 package gtkutil1 package gtkutil
2 2
3-import "testing"3+import (
4+ "math"
5+ "testing"
6+)
4 7
5 func TestNormalizeScale(t *testing.T) {8 func TestNormalizeScale(t *testing.T) {
6- if got := NormalizeScale(0); got != 1 {9+ tests := []struct {
7- t.Fatalf("NormalizeScale(0) = %v, want 1", got)10+ name string
11+ input float64
12+ want float64
13+ }{
14+ {name: "zero", input: 0, want: 1},
15+ {name: "fractional", input: 1.2, want: 1.2},
16+ {name: "nan", input: math.NaN(), want: 1},
17+ {name: "positive_infinity", input: math.Inf(1), want: 1},
18+ {name: "negative_infinity", input: math.Inf(-1), want: 1},
19+ {name: "negative", input: -1, want: 1},
8 }20 }
9- if got := NormalizeScale(1.2); got != 1.2 {21+ for _, tt := range tests {
10- t.Fatalf("NormalizeScale(1.2) = %v, want 1.2", got)22+ t.Run(tt.name, func(t *testing.T) {
23+ if got := NormalizeScale(tt.input); got != tt.want {
24+ t.Fatalf("NormalizeScale(%v) = %v, want %v", tt.input, got, tt.want)
25+ }
26+ })
11 }27 }
12 }28 }
13 29

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close