detached b805321

feat(cef): add touchpad swipe navigation

commit
b805321bfdc7b6a21f8d0577dd099e566bcd43f2
author
brice · · 2026-06-02 03:34 UTC
committer
brice · · 2026-06-02 04:20 UTC
parent
e84a0aa
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +255 -36
M docs/config/index.md +3 -0
@@ -199,6 +199,9 @@ border = "#363636"
199199 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |
200200 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier |
201201 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |
202+| `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
203+| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |
204+| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
202205 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
203206 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
204207
@@ -199,6 +199,9 @@ border = "#363636"
199 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |199 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |
200 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier |200 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier |
201 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |201 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |
202+| `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
203+| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |
204+| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
202 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |205 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
203 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |206 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
204 207
M docs/reference/configuration.md +3 -0
@@ -69,6 +69,9 @@
6969 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 |
7070 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |
7171 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |
72+| `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
73+| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 |
74+| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
7275 | `default_ui_scale` | float | `1.0` | > 0 |
7376 | `default_webpage_zoom` | float | `1.2` | > 0 |
7477 | `workspace.new_pane_url` | string | `about:blank` | |
@@ -69,6 +69,9 @@
69 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 |69 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 |
70 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |70 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |
71 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |71 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |
72+| `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
73+| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 |
74+| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
72 | `default_ui_scale` | float | `1.0` | > 0 |75 | `default_ui_scale` | float | `1.0` | > 0 |
73 | `default_webpage_zoom` | float | `1.2` | > 0 |76 | `default_webpage_zoom` | float | `1.2` | > 0 |
74 | `workspace.new_pane_url` | string | `about:blank` | |77 | `workspace.new_pane_url` | string | `about:blank` | |
M go.mod +2 -2
@@ -7,10 +7,10 @@ require (
77 github.com/andybalholm/brotli v1.2.1
88 github.com/bnema/purego v0.11.0-bnema.3
99 github.com/bnema/purego-cef v0.13.2
10- github.com/bnema/purego-cef2gtk v0.6.0
10+ github.com/bnema/purego-cef2gtk v0.7.0
1111 github.com/bnema/purego-pipewire v0.1.4
1212 github.com/bnema/purego-sqlite v0.1.3
13- github.com/bnema/puregotk v0.5.4
13+ github.com/bnema/puregotk v0.6.0
1414 github.com/charmbracelet/bubbles v1.0.0
1515 github.com/charmbracelet/bubbletea v1.3.10
1616 github.com/charmbracelet/lipgloss v1.1.0
@@ -7,10 +7,10 @@ require (
7 github.com/andybalholm/brotli v1.2.17 github.com/andybalholm/brotli v1.2.1
8 github.com/bnema/purego v0.11.0-bnema.38 github.com/bnema/purego v0.11.0-bnema.3
9 github.com/bnema/purego-cef v0.13.29 github.com/bnema/purego-cef v0.13.2
10- github.com/bnema/purego-cef2gtk v0.6.010+ github.com/bnema/purego-cef2gtk v0.7.0
11 github.com/bnema/purego-pipewire v0.1.411 github.com/bnema/purego-pipewire v0.1.4
12 github.com/bnema/purego-sqlite v0.1.312 github.com/bnema/purego-sqlite v0.1.3
13- github.com/bnema/puregotk v0.5.413+ github.com/bnema/puregotk v0.6.0
14 github.com/charmbracelet/bubbles v1.0.014 github.com/charmbracelet/bubbles v1.0.0
15 github.com/charmbracelet/bubbletea v1.3.1015 github.com/charmbracelet/bubbletea v1.3.10
16 github.com/charmbracelet/lipgloss v1.1.016 github.com/charmbracelet/lipgloss v1.1.0
M go.sum +4 -4
@@ -14,14 +14,14 @@ github.com/bnema/purego v0.11.0-bnema.3 h1:lpTXuapRIBH2OTgLJTjYlK1olHmZkEQLloqgP
1414 github.com/bnema/purego v0.11.0-bnema.3/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng=
1515 github.com/bnema/purego-cef v0.13.2 h1:weSttuJVxjyzp/A5J15ZdzOAVWO/jvvd6nBf8/N4zsI=
1616 github.com/bnema/purego-cef v0.13.2/go.mod h1:Jbc4GgUvGakEOfbRnXJGYrDbXz03YEpVgTWKPk5ss9k=
17-github.com/bnema/purego-cef2gtk v0.6.0 h1:I8WJfqa7FmylE+JwZn7k3gsSKw0aRXmyHbQnXtyMdas=
18-github.com/bnema/purego-cef2gtk v0.6.0/go.mod h1:LGMP/8ip347wu5wnLiQh8m+I+yq4H+aiuMAEICrbVFk=
17+github.com/bnema/purego-cef2gtk v0.7.0 h1:sv8VUB8VIqwrwV9M5E2u6pZ24ly4+erCeooRyCMz1mc=
18+github.com/bnema/purego-cef2gtk v0.7.0/go.mod h1:eLgf2GwRirBmRNzPWAp/VMHf4MyAN8xoWbdoUNgW48A=
1919 github.com/bnema/purego-pipewire v0.1.4 h1:ThuPiZo9BeYn1w06hg/g0INgQUIBi8n9Gq1o5VRtjxk=
2020 github.com/bnema/purego-pipewire v0.1.4/go.mod h1:dn9RGRtNteqIarb8gTv97VD/FQgur7ZJ1BE/Ub9NYSk=
2121 github.com/bnema/purego-sqlite v0.1.3 h1:aXysq9+wuRkbFzlVe2oIiooYseFZFb6ArwbfDYYafvY=
2222 github.com/bnema/purego-sqlite v0.1.3/go.mod h1:a15W2PgD/3b9W9HytbmPXz3pBAbjwf7dhNv997fSbhI=
23-github.com/bnema/puregotk v0.5.4 h1:SYIS5r5JySaFVw/PrOMoO+j8YlYyB9sJ7g4At8Pv0EI=
24-github.com/bnema/puregotk v0.5.4/go.mod h1:pXquWfuhZzVdDJU08CH3De8l9N4MsuOpTjbKHpiOeIs=
23+github.com/bnema/puregotk v0.6.0 h1:yZJoCdY9f+souLxWV5JJfI+yBB/MD0aEQbSF0aLDNII=
24+github.com/bnema/puregotk v0.6.0/go.mod h1:pXquWfuhZzVdDJU08CH3De8l9N4MsuOpTjbKHpiOeIs=
2525 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
2626 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
2727 github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
@@ -14,14 +14,14 @@ github.com/bnema/purego v0.11.0-bnema.3 h1:lpTXuapRIBH2OTgLJTjYlK1olHmZkEQLloqgP
14 github.com/bnema/purego v0.11.0-bnema.3/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng=14 github.com/bnema/purego v0.11.0-bnema.3/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng=
15 github.com/bnema/purego-cef v0.13.2 h1:weSttuJVxjyzp/A5J15ZdzOAVWO/jvvd6nBf8/N4zsI=15 github.com/bnema/purego-cef v0.13.2 h1:weSttuJVxjyzp/A5J15ZdzOAVWO/jvvd6nBf8/N4zsI=
16 github.com/bnema/purego-cef v0.13.2/go.mod h1:Jbc4GgUvGakEOfbRnXJGYrDbXz03YEpVgTWKPk5ss9k=16 github.com/bnema/purego-cef v0.13.2/go.mod h1:Jbc4GgUvGakEOfbRnXJGYrDbXz03YEpVgTWKPk5ss9k=
17-github.com/bnema/purego-cef2gtk v0.6.0 h1:I8WJfqa7FmylE+JwZn7k3gsSKw0aRXmyHbQnXtyMdas=17+github.com/bnema/purego-cef2gtk v0.7.0 h1:sv8VUB8VIqwrwV9M5E2u6pZ24ly4+erCeooRyCMz1mc=
18-github.com/bnema/purego-cef2gtk v0.6.0/go.mod h1:LGMP/8ip347wu5wnLiQh8m+I+yq4H+aiuMAEICrbVFk=18+github.com/bnema/purego-cef2gtk v0.7.0/go.mod h1:eLgf2GwRirBmRNzPWAp/VMHf4MyAN8xoWbdoUNgW48A=
19 github.com/bnema/purego-pipewire v0.1.4 h1:ThuPiZo9BeYn1w06hg/g0INgQUIBi8n9Gq1o5VRtjxk=19 github.com/bnema/purego-pipewire v0.1.4 h1:ThuPiZo9BeYn1w06hg/g0INgQUIBi8n9Gq1o5VRtjxk=
20 github.com/bnema/purego-pipewire v0.1.4/go.mod h1:dn9RGRtNteqIarb8gTv97VD/FQgur7ZJ1BE/Ub9NYSk=20 github.com/bnema/purego-pipewire v0.1.4/go.mod h1:dn9RGRtNteqIarb8gTv97VD/FQgur7ZJ1BE/Ub9NYSk=
21 github.com/bnema/purego-sqlite v0.1.3 h1:aXysq9+wuRkbFzlVe2oIiooYseFZFb6ArwbfDYYafvY=21 github.com/bnema/purego-sqlite v0.1.3 h1:aXysq9+wuRkbFzlVe2oIiooYseFZFb6ArwbfDYYafvY=
22 github.com/bnema/purego-sqlite v0.1.3/go.mod h1:a15W2PgD/3b9W9HytbmPXz3pBAbjwf7dhNv997fSbhI=22 github.com/bnema/purego-sqlite v0.1.3/go.mod h1:a15W2PgD/3b9W9HytbmPXz3pBAbjwf7dhNv997fSbhI=
23-github.com/bnema/puregotk v0.5.4 h1:SYIS5r5JySaFVw/PrOMoO+j8YlYyB9sJ7g4At8Pv0EI=23+github.com/bnema/puregotk v0.6.0 h1:yZJoCdY9f+souLxWV5JJfI+yBB/MD0aEQbSF0aLDNII=
24-github.com/bnema/puregotk v0.5.4/go.mod h1:pXquWfuhZzVdDJU08CH3De8l9N4MsuOpTjbKHpiOeIs=24+github.com/bnema/puregotk v0.6.0/go.mod h1:pXquWfuhZzVdDJU08CH3De8l9N4MsuOpTjbKHpiOeIs=
25 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=25 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
26 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=26 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
27 github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=27 github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
M internal/bootstrap/engine.go +8 -5
@@ -85,11 +85,14 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
8585 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,
8686 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),
8787 Input: cef.RuntimeInputConfig{
88- ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,
89- ScrollTouchpadMultiplier: cfg.Engine.CEF.Input.ScrollTouchpadMultiplier,
90- ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,
91- ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,
92- ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,
88+ ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,
89+ ScrollTouchpadMultiplier: cfg.Engine.CEF.Input.ScrollTouchpadMultiplier,
90+ ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,
91+ ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,
92+ ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,
93+ TouchpadNavigationEnabled: cfg.Engine.CEF.Input.TouchpadNavigationEnabled,
94+ TouchpadNavigationMinDelta: cfg.Engine.CEF.Input.TouchpadNavigationMinDelta,
95+ TouchpadNavigationMaxVerticalRatio: cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio,
9396 },
9497 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
9598 TraceHandlers: cfg.Engine.CEF.TraceHandlers,
@@ -85,11 +85,14 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
85 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,85 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,
86 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),86 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),
87 Input: cef.RuntimeInputConfig{87 Input: cef.RuntimeInputConfig{
88- ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,88+ ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,
89- ScrollTouchpadMultiplier: cfg.Engine.CEF.Input.ScrollTouchpadMultiplier,89+ ScrollTouchpadMultiplier: cfg.Engine.CEF.Input.ScrollTouchpadMultiplier,
90- ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,90+ ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,
91- ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,91+ ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,
92- ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,92+ ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,
93+ TouchpadNavigationEnabled: cfg.Engine.CEF.Input.TouchpadNavigationEnabled,
94+ TouchpadNavigationMinDelta: cfg.Engine.CEF.Input.TouchpadNavigationMinDelta,
95+ TouchpadNavigationMaxVerticalRatio: cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio,
93 },96 },
94 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,97 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
95 TraceHandlers: cfg.Engine.CEF.TraceHandlers,98 TraceHandlers: cfg.Engine.CEF.TraceHandlers,
M internal/infrastructure/cef/runtime_config.go +8 -5
@@ -29,11 +29,14 @@ type RuntimeConfig struct {
2929 }
3030
3131 type RuntimeInputConfig struct {
32- ScrollWheelMultiplier float64
33- ScrollTouchpadMultiplier float64
34- ScrollHorizontalMultiplier float64
35- ScrollVerticalMultiplier float64
36- ScrollMaxDelta int32
32+ ScrollWheelMultiplier float64
33+ ScrollTouchpadMultiplier float64
34+ ScrollHorizontalMultiplier float64
35+ ScrollVerticalMultiplier float64
36+ ScrollMaxDelta int32
37+ TouchpadNavigationEnabled bool
38+ TouchpadNavigationMinDelta float64
39+ TouchpadNavigationMaxVerticalRatio float64
3740 }
3841
3942 type HandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error
@@ -29,11 +29,14 @@ type RuntimeConfig struct {
29 }29 }
30 30
31 type RuntimeInputConfig struct {31 type RuntimeInputConfig struct {
32- ScrollWheelMultiplier float6432+ ScrollWheelMultiplier float64
33- ScrollTouchpadMultiplier float6433+ ScrollTouchpadMultiplier float64
34- ScrollHorizontalMultiplier float6434+ ScrollHorizontalMultiplier float64
35- ScrollVerticalMultiplier float6435+ ScrollVerticalMultiplier float64
36- ScrollMaxDelta int3236+ ScrollMaxDelta int32
37+ TouchpadNavigationEnabled bool
38+ TouchpadNavigationMinDelta float64
39+ TouchpadNavigationMaxVerticalRatio float64
37 }40 }
38 41
39 type HandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error42 type HandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error
M internal/infrastructure/cef/webview.go +99 -0
@@ -205,6 +205,8 @@ type WebView struct {
205205 windowlessFrameRate int32
206206 backgroundColor uint32
207207 inputConfig RuntimeInputConfig
208+ touchpadSwipeDX float64
209+ touchpadSwipeDY float64
208210
209211 // Audio output factory and active stream.
210212 audioOutputFactory port.AudioOutputFactory
@@ -1439,6 +1441,12 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14391441 OnMiddleClick: func(_, _ float64) bool {
14401442 return wv.handleMiddleClickFromBridge()
14411443 },
1444+ OnTouchpadSwipe: func(event cef2gtk.TouchpadSwipeEvent) cef2gtk.TouchpadSwipeDecision {
1445+ if wv.handleTouchpadNavigationSwipe(event) {
1446+ return cef2gtk.TouchpadSwipeConsume
1447+ }
1448+ return cef2gtk.TouchpadSwipePassthrough
1449+ },
14421450 SelectionText: wv.selectedTextSnapshot,
14431451 OnClipboardShortcut: func(action, text string) {
14441452 if wv.engine != nil {
@@ -1448,6 +1456,97 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14481456 }
14491457 }
14501458
1459+type touchpadNavigationAction int
1460+
1461+const (
1462+ touchpadNavigationNone touchpadNavigationAction = iota
1463+ touchpadNavigationBack
1464+ touchpadNavigationForward
1465+)
1466+
1467+func (wv *WebView) handleTouchpadNavigationSwipe(event cef2gtk.TouchpadSwipeEvent) bool {
1468+ cfg := wv.inputConfig
1469+ if !cfg.TouchpadNavigationEnabled || event.Fingers != 2 {
1470+ wv.resetTouchpadNavigationSwipe()
1471+ return false
1472+ }
1473+
1474+ switch event.Phase {
1475+ case cef2gtk.TouchpadGesturePhaseBegin:
1476+ wv.resetTouchpadNavigationSwipe()
1477+ return false
1478+ case cef2gtk.TouchpadGesturePhaseUpdate:
1479+ wv.accumulateTouchpadNavigationSwipe(event.DX, event.DY)
1480+ return false
1481+ case cef2gtk.TouchpadGesturePhaseEnd:
1482+ dx, dy := wv.finishTouchpadNavigationSwipe()
1483+ switch chooseTouchpadNavigationAction(cfg, dx, dy, wv.CanGoBack(), wv.CanGoForward()) {
1484+ case touchpadNavigationBack:
1485+ if err := wv.GoBack(wv.ctx); err != nil {
1486+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad back navigation failed")
1487+ return false
1488+ }
1489+ return true
1490+ case touchpadNavigationForward:
1491+ if err := wv.GoForward(wv.ctx); err != nil {
1492+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad forward navigation failed")
1493+ return false
1494+ }
1495+ return true
1496+ default:
1497+ return false
1498+ }
1499+ case cef2gtk.TouchpadGesturePhaseCancel:
1500+ wv.resetTouchpadNavigationSwipe()
1501+ return false
1502+ default:
1503+ return false
1504+ }
1505+}
1506+
1507+func (wv *WebView) resetTouchpadNavigationSwipe() {
1508+ wv.mu.Lock()
1509+ wv.touchpadSwipeDX = 0
1510+ wv.touchpadSwipeDY = 0
1511+ wv.mu.Unlock()
1512+}
1513+
1514+func (wv *WebView) accumulateTouchpadNavigationSwipe(dx, dy float64) {
1515+ wv.mu.Lock()
1516+ wv.touchpadSwipeDX += dx
1517+ wv.touchpadSwipeDY += dy
1518+ wv.mu.Unlock()
1519+}
1520+
1521+func (wv *WebView) finishTouchpadNavigationSwipe() (float64, float64) {
1522+ wv.mu.Lock()
1523+ defer wv.mu.Unlock()
1524+ dx, dy := wv.touchpadSwipeDX, wv.touchpadSwipeDY
1525+ wv.touchpadSwipeDX = 0
1526+ wv.touchpadSwipeDY = 0
1527+ return dx, dy
1528+}
1529+
1530+func chooseTouchpadNavigationAction(cfg RuntimeInputConfig, dx, dy float64, canGoBack, canGoForward bool) touchpadNavigationAction {
1531+ if !cfg.TouchpadNavigationEnabled {
1532+ return touchpadNavigationNone
1533+ }
1534+ absDX, absDY := math.Abs(dx), math.Abs(dy)
1535+ if absDX < cfg.TouchpadNavigationMinDelta {
1536+ return touchpadNavigationNone
1537+ }
1538+ if absDY > absDX*cfg.TouchpadNavigationMaxVerticalRatio {
1539+ return touchpadNavigationNone
1540+ }
1541+ if dx > 0 && canGoBack {
1542+ return touchpadNavigationBack
1543+ }
1544+ if dx < 0 && canGoForward {
1545+ return touchpadNavigationForward
1546+ }
1547+ return touchpadNavigationNone
1548+}
1549+
14511550 func (wv *WebView) viewBridgeScale() float64 {
14521551 if wv == nil || wv.viewBridge == nil {
14531552 return 1
@@ -205,6 +205,8 @@ type WebView struct {
205 windowlessFrameRate int32205 windowlessFrameRate int32
206 backgroundColor uint32206 backgroundColor uint32
207 inputConfig RuntimeInputConfig207 inputConfig RuntimeInputConfig
208+ touchpadSwipeDX float64
209+ touchpadSwipeDY float64
208 210
209 // Audio output factory and active stream.211 // Audio output factory and active stream.
210 audioOutputFactory port.AudioOutputFactory212 audioOutputFactory port.AudioOutputFactory
@@ -1439,6 +1441,12 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1439 OnMiddleClick: func(_, _ float64) bool {1441 OnMiddleClick: func(_, _ float64) bool {
1440 return wv.handleMiddleClickFromBridge()1442 return wv.handleMiddleClickFromBridge()
1441 },1443 },
1444+ OnTouchpadSwipe: func(event cef2gtk.TouchpadSwipeEvent) cef2gtk.TouchpadSwipeDecision {
1445+ if wv.handleTouchpadNavigationSwipe(event) {
1446+ return cef2gtk.TouchpadSwipeConsume
1447+ }
1448+ return cef2gtk.TouchpadSwipePassthrough
1449+ },
1442 SelectionText: wv.selectedTextSnapshot,1450 SelectionText: wv.selectedTextSnapshot,
1443 OnClipboardShortcut: func(action, text string) {1451 OnClipboardShortcut: func(action, text string) {
1444 if wv.engine != nil {1452 if wv.engine != nil {
@@ -1448,6 +1456,97 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1448 }1456 }
1449 }1457 }
1450 1458
1459+type touchpadNavigationAction int
1460+
1461+const (
1462+ touchpadNavigationNone touchpadNavigationAction = iota
1463+ touchpadNavigationBack
1464+ touchpadNavigationForward
1465+)
1466+
1467+func (wv *WebView) handleTouchpadNavigationSwipe(event cef2gtk.TouchpadSwipeEvent) bool {
1468+ cfg := wv.inputConfig
1469+ if !cfg.TouchpadNavigationEnabled || event.Fingers != 2 {
1470+ wv.resetTouchpadNavigationSwipe()
1471+ return false
1472+ }
1473+
1474+ switch event.Phase {
1475+ case cef2gtk.TouchpadGesturePhaseBegin:
1476+ wv.resetTouchpadNavigationSwipe()
1477+ return false
1478+ case cef2gtk.TouchpadGesturePhaseUpdate:
1479+ wv.accumulateTouchpadNavigationSwipe(event.DX, event.DY)
1480+ return false
1481+ case cef2gtk.TouchpadGesturePhaseEnd:
1482+ dx, dy := wv.finishTouchpadNavigationSwipe()
1483+ switch chooseTouchpadNavigationAction(cfg, dx, dy, wv.CanGoBack(), wv.CanGoForward()) {
1484+ case touchpadNavigationBack:
1485+ if err := wv.GoBack(wv.ctx); err != nil {
1486+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad back navigation failed")
1487+ return false
1488+ }
1489+ return true
1490+ case touchpadNavigationForward:
1491+ if err := wv.GoForward(wv.ctx); err != nil {
1492+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad forward navigation failed")
1493+ return false
1494+ }
1495+ return true
1496+ default:
1497+ return false
1498+ }
1499+ case cef2gtk.TouchpadGesturePhaseCancel:
1500+ wv.resetTouchpadNavigationSwipe()
1501+ return false
1502+ default:
1503+ return false
1504+ }
1505+}
1506+
1507+func (wv *WebView) resetTouchpadNavigationSwipe() {
1508+ wv.mu.Lock()
1509+ wv.touchpadSwipeDX = 0
1510+ wv.touchpadSwipeDY = 0
1511+ wv.mu.Unlock()
1512+}
1513+
1514+func (wv *WebView) accumulateTouchpadNavigationSwipe(dx, dy float64) {
1515+ wv.mu.Lock()
1516+ wv.touchpadSwipeDX += dx
1517+ wv.touchpadSwipeDY += dy
1518+ wv.mu.Unlock()
1519+}
1520+
1521+func (wv *WebView) finishTouchpadNavigationSwipe() (float64, float64) {
1522+ wv.mu.Lock()
1523+ defer wv.mu.Unlock()
1524+ dx, dy := wv.touchpadSwipeDX, wv.touchpadSwipeDY
1525+ wv.touchpadSwipeDX = 0
1526+ wv.touchpadSwipeDY = 0
1527+ return dx, dy
1528+}
1529+
1530+func chooseTouchpadNavigationAction(cfg RuntimeInputConfig, dx, dy float64, canGoBack, canGoForward bool) touchpadNavigationAction {
1531+ if !cfg.TouchpadNavigationEnabled {
1532+ return touchpadNavigationNone
1533+ }
1534+ absDX, absDY := math.Abs(dx), math.Abs(dy)
1535+ if absDX < cfg.TouchpadNavigationMinDelta {
1536+ return touchpadNavigationNone
1537+ }
1538+ if absDY > absDX*cfg.TouchpadNavigationMaxVerticalRatio {
1539+ return touchpadNavigationNone
1540+ }
1541+ if dx > 0 && canGoBack {
1542+ return touchpadNavigationBack
1543+ }
1544+ if dx < 0 && canGoForward {
1545+ return touchpadNavigationForward
1546+ }
1547+ return touchpadNavigationNone
1548+}
1549+
1451 func (wv *WebView) viewBridgeScale() float64 {1550 func (wv *WebView) viewBridgeScale() float64 {
1452 if wv == nil || wv.viewBridge == nil {1551 if wv == nil || wv.viewBridge == nil {
1453 return 11552 return 1
M internal/infrastructure/cef/webview_input_test.go +46 -5
@@ -12,11 +12,14 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
1212 wv := &WebView{
1313 nativeWidget: nativeWidget,
1414 inputConfig: RuntimeInputConfig{
15- ScrollWheelMultiplier: 1.25,
16- ScrollTouchpadMultiplier: 0.35,
17- ScrollHorizontalMultiplier: 0.75,
18- ScrollVerticalMultiplier: 1.5,
19- ScrollMaxDelta: 120,
15+ ScrollWheelMultiplier: 1.25,
16+ ScrollTouchpadMultiplier: 0.35,
17+ ScrollHorizontalMultiplier: 0.75,
18+ ScrollVerticalMultiplier: 1.5,
19+ ScrollMaxDelta: 120,
20+ TouchpadNavigationEnabled: true,
21+ TouchpadNavigationMinDelta: 80,
22+ TouchpadNavigationMaxVerticalRatio: 0.5,
2023 },
2124 }
2225
@@ -29,7 +32,45 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
2932 require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)
3033 require.Equal(t, int32(120), opts.Scroll.MaxDelta)
3134 require.NotNil(t, opts.OnMiddleClick)
35+ require.NotNil(t, opts.OnTouchpadSwipe)
3236 require.NotNil(t, opts.SelectionText)
3337 require.NotNil(t, opts.OnClipboardShortcut)
3438 require.Same(t, nativeWidget, wv.nativeWidget)
3539 }
40+
41+func TestChooseTouchpadNavigationAction(t *testing.T) {
42+ cfg := RuntimeInputConfig{
43+ TouchpadNavigationEnabled: true,
44+ TouchpadNavigationMinDelta: 80,
45+ TouchpadNavigationMaxVerticalRatio: 0.5,
46+ }
47+
48+ tests := []struct {
49+ name string
50+ cfg *RuntimeInputConfig
51+ dx, dy float64
52+ canGoBack bool
53+ canGoForward bool
54+ want touchpadNavigationAction
55+ }{
56+ {name: "right swipe goes back", dx: 100, dy: 10, canGoBack: true, want: touchpadNavigationBack},
57+ {name: "left swipe goes forward", dx: -100, dy: 10, canGoForward: true, want: touchpadNavigationForward},
58+ {name: "below threshold ignored", dx: 40, dy: 0, canGoBack: true, want: touchpadNavigationNone},
59+ {name: "disabled ignored", cfg: &RuntimeInputConfig{TouchpadNavigationEnabled: false, TouchpadNavigationMinDelta: 80, TouchpadNavigationMaxVerticalRatio: 0.5}, dx: 100, dy: 0, canGoBack: true, want: touchpadNavigationNone},
60+ {name: "exact threshold goes back", dx: 80, dy: 0, canGoBack: true, want: touchpadNavigationBack},
61+ {name: "vertical diagonal ignored", dx: 100, dy: 80, canGoBack: true, want: touchpadNavigationNone},
62+ {name: "unavailable back ignored", dx: 100, dy: 0, canGoBack: false, want: touchpadNavigationNone},
63+ {name: "unavailable forward ignored", dx: -100, dy: 0, canGoForward: false, want: touchpadNavigationNone},
64+ }
65+
66+ for _, tt := range tests {
67+ t.Run(tt.name, func(t *testing.T) {
68+ testCfg := cfg
69+ if tt.cfg != nil {
70+ testCfg = *tt.cfg
71+ }
72+ got := chooseTouchpadNavigationAction(testCfg, tt.dx, tt.dy, tt.canGoBack, tt.canGoForward)
73+ require.Equal(t, tt.want, got)
74+ })
75+ }
76+}
@@ -12,11 +12,14 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
12 wv := &WebView{12 wv := &WebView{
13 nativeWidget: nativeWidget,13 nativeWidget: nativeWidget,
14 inputConfig: RuntimeInputConfig{14 inputConfig: RuntimeInputConfig{
15- ScrollWheelMultiplier: 1.25,15+ ScrollWheelMultiplier: 1.25,
16- ScrollTouchpadMultiplier: 0.35,16+ ScrollTouchpadMultiplier: 0.35,
17- ScrollHorizontalMultiplier: 0.75,17+ ScrollHorizontalMultiplier: 0.75,
18- ScrollVerticalMultiplier: 1.5,18+ ScrollVerticalMultiplier: 1.5,
19- ScrollMaxDelta: 120,19+ ScrollMaxDelta: 120,
20+ TouchpadNavigationEnabled: true,
21+ TouchpadNavigationMinDelta: 80,
22+ TouchpadNavigationMaxVerticalRatio: 0.5,
20 },23 },
21 }24 }
22 25
@@ -29,7 +32,45 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
29 require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)32 require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)
30 require.Equal(t, int32(120), opts.Scroll.MaxDelta)33 require.Equal(t, int32(120), opts.Scroll.MaxDelta)
31 require.NotNil(t, opts.OnMiddleClick)34 require.NotNil(t, opts.OnMiddleClick)
35+ require.NotNil(t, opts.OnTouchpadSwipe)
32 require.NotNil(t, opts.SelectionText)36 require.NotNil(t, opts.SelectionText)
33 require.NotNil(t, opts.OnClipboardShortcut)37 require.NotNil(t, opts.OnClipboardShortcut)
34 require.Same(t, nativeWidget, wv.nativeWidget)38 require.Same(t, nativeWidget, wv.nativeWidget)
35 }39 }
40+
41+func TestChooseTouchpadNavigationAction(t *testing.T) {
42+ cfg := RuntimeInputConfig{
43+ TouchpadNavigationEnabled: true,
44+ TouchpadNavigationMinDelta: 80,
45+ TouchpadNavigationMaxVerticalRatio: 0.5,
46+ }
47+
48+ tests := []struct {
49+ name string
50+ cfg *RuntimeInputConfig
51+ dx, dy float64
52+ canGoBack bool
53+ canGoForward bool
54+ want touchpadNavigationAction
55+ }{
56+ {name: "right swipe goes back", dx: 100, dy: 10, canGoBack: true, want: touchpadNavigationBack},
57+ {name: "left swipe goes forward", dx: -100, dy: 10, canGoForward: true, want: touchpadNavigationForward},
58+ {name: "below threshold ignored", dx: 40, dy: 0, canGoBack: true, want: touchpadNavigationNone},
59+ {name: "disabled ignored", cfg: &RuntimeInputConfig{TouchpadNavigationEnabled: false, TouchpadNavigationMinDelta: 80, TouchpadNavigationMaxVerticalRatio: 0.5}, dx: 100, dy: 0, canGoBack: true, want: touchpadNavigationNone},
60+ {name: "exact threshold goes back", dx: 80, dy: 0, canGoBack: true, want: touchpadNavigationBack},
61+ {name: "vertical diagonal ignored", dx: 100, dy: 80, canGoBack: true, want: touchpadNavigationNone},
62+ {name: "unavailable back ignored", dx: 100, dy: 0, canGoBack: false, want: touchpadNavigationNone},
63+ {name: "unavailable forward ignored", dx: -100, dy: 0, canGoForward: false, want: touchpadNavigationNone},
64+ }
65+
66+ for _, tt := range tests {
67+ t.Run(tt.name, func(t *testing.T) {
68+ testCfg := cfg
69+ if tt.cfg != nil {
70+ testCfg = *tt.cfg
71+ }
72+ got := chooseTouchpadNavigationAction(testCfg, tt.dx, tt.dy, tt.canGoBack, tt.canGoForward)
73+ require.Equal(t, tt.want, got)
74+ })
75+ }
76+}
M internal/infrastructure/config/defaults.go +11 -5
@@ -74,6 +74,9 @@ const (
7474 defaultCEFScrollMultiplier = 1.0
7575 defaultCEFScrollTouchpadMultiplier = 0.35
7676 defaultCEFScrollMaxDelta = 0
77+ defaultCEFTouchpadNavigation = true
78+ defaultCEFTouchpadNavigationDelta = 80.0
79+ defaultCEFTouchpadNavigationRatio = 0.5
7780
7881 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
7982 defaultSkiaCPUPaintingThreads = 0
@@ -182,11 +185,14 @@ func DefaultConfig() *Config {
182185 LogFile: "",
183186 EnableAudioHandler: true,
184187 Input: CEFInputConfig{
185- ScrollWheelMultiplier: defaultCEFScrollMultiplier,
186- ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,
187- ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
188- ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
189- ScrollMaxDelta: defaultCEFScrollMaxDelta,
188+ ScrollWheelMultiplier: defaultCEFScrollMultiplier,
189+ ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,
190+ ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
191+ ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
192+ ScrollMaxDelta: defaultCEFScrollMaxDelta,
193+ TouchpadNavigationEnabled: defaultCEFTouchpadNavigation,
194+ TouchpadNavigationMinDelta: defaultCEFTouchpadNavigationDelta,
195+ TouchpadNavigationMaxVerticalRatio: defaultCEFTouchpadNavigationRatio,
190196 },
191197 },
192198 WebKit: WebKitEngineConfig{
@@ -74,6 +74,9 @@ const (
74 defaultCEFScrollMultiplier = 1.074 defaultCEFScrollMultiplier = 1.0
75 defaultCEFScrollTouchpadMultiplier = 0.3575 defaultCEFScrollTouchpadMultiplier = 0.35
76 defaultCEFScrollMaxDelta = 076 defaultCEFScrollMaxDelta = 0
77+ defaultCEFTouchpadNavigation = true
78+ defaultCEFTouchpadNavigationDelta = 80.0
79+ defaultCEFTouchpadNavigationRatio = 0.5
77 80
78 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)81 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
79 defaultSkiaCPUPaintingThreads = 082 defaultSkiaCPUPaintingThreads = 0
@@ -182,11 +185,14 @@ func DefaultConfig() *Config {
182 LogFile: "",185 LogFile: "",
183 EnableAudioHandler: true,186 EnableAudioHandler: true,
184 Input: CEFInputConfig{187 Input: CEFInputConfig{
185- ScrollWheelMultiplier: defaultCEFScrollMultiplier,188+ ScrollWheelMultiplier: defaultCEFScrollMultiplier,
186- ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,189+ ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,
187- ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,190+ ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
188- ScrollVerticalMultiplier: defaultCEFScrollMultiplier,191+ ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
189- ScrollMaxDelta: defaultCEFScrollMaxDelta,192+ ScrollMaxDelta: defaultCEFScrollMaxDelta,
193+ TouchpadNavigationEnabled: defaultCEFTouchpadNavigation,
194+ TouchpadNavigationMinDelta: defaultCEFTouchpadNavigationDelta,
195+ TouchpadNavigationMaxVerticalRatio: defaultCEFTouchpadNavigationRatio,
190 },196 },
191 },197 },
192 WebKit: WebKitEngineConfig{198 WebKit: WebKitEngineConfig{
M internal/infrastructure/config/defaults_test.go +3 -0
@@ -30,6 +30,9 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
3030 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollHorizontalMultiplier, 0.001)
3131 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollVerticalMultiplier, 0.001)
3232 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
33+ assert.Equal(t, defaultCEFTouchpadNavigation, cfg.Engine.CEF.Input.TouchpadNavigationEnabled)
34+ assert.InDelta(t, defaultCEFTouchpadNavigationDelta, cfg.Engine.CEF.Input.TouchpadNavigationMinDelta, 0.001)
35+ assert.InDelta(t, defaultCEFTouchpadNavigationRatio, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)
3336 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
3437
3538 // System view shortcuts are first-class global actions.
@@ -30,6 +30,9 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
30 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollHorizontalMultiplier, 0.001)30 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollHorizontalMultiplier, 0.001)
31 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollVerticalMultiplier, 0.001)31 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollVerticalMultiplier, 0.001)
32 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)32 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
33+ assert.Equal(t, defaultCEFTouchpadNavigation, cfg.Engine.CEF.Input.TouchpadNavigationEnabled)
34+ assert.InDelta(t, defaultCEFTouchpadNavigationDelta, cfg.Engine.CEF.Input.TouchpadNavigationMinDelta, 0.001)
35+ assert.InDelta(t, defaultCEFTouchpadNavigationRatio, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)
33 assert.True(t, cfg.Engine.WebKit.ITPEnabled)36 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
34 37
35 // System view shortcuts are first-class global actions.38 // System view shortcuts are first-class global actions.
M internal/infrastructure/config/engine.go +8 -5
@@ -120,11 +120,14 @@ type CEFEngineConfig struct {
120120
121121 // CEFInputConfig holds CEF-specific input translation options.
122122 type CEFInputConfig struct {
123- ScrollWheelMultiplier float64 `mapstructure:"scroll_wheel_multiplier" toml:"scroll_wheel_multiplier" yaml:"scroll_wheel_multiplier"` //nolint:lll // struct tags exceed lll limit
124- ScrollTouchpadMultiplier float64 `mapstructure:"scroll_touchpad_multiplier" toml:"scroll_touchpad_multiplier" yaml:"scroll_touchpad_multiplier"` //nolint:lll // struct tags exceed lll limit
125- ScrollHorizontalMultiplier float64 `mapstructure:"scroll_horizontal_multiplier" toml:"scroll_horizontal_multiplier" yaml:"scroll_horizontal_multiplier"` //nolint:lll // struct tags exceed lll limit
126- ScrollVerticalMultiplier float64 `mapstructure:"scroll_vertical_multiplier" toml:"scroll_vertical_multiplier" yaml:"scroll_vertical_multiplier"` //nolint:lll // struct tags exceed lll limit
127- ScrollMaxDelta int32 `mapstructure:"scroll_max_delta" toml:"scroll_max_delta" yaml:"scroll_max_delta"` //nolint:lll // struct tags exceed lll limit
123+ ScrollWheelMultiplier float64 `mapstructure:"scroll_wheel_multiplier" toml:"scroll_wheel_multiplier" yaml:"scroll_wheel_multiplier"` //nolint:lll // struct tags exceed lll limit
124+ ScrollTouchpadMultiplier float64 `mapstructure:"scroll_touchpad_multiplier" toml:"scroll_touchpad_multiplier" yaml:"scroll_touchpad_multiplier"` //nolint:lll // struct tags exceed lll limit
125+ ScrollHorizontalMultiplier float64 `mapstructure:"scroll_horizontal_multiplier" toml:"scroll_horizontal_multiplier" yaml:"scroll_horizontal_multiplier"` //nolint:lll // struct tags exceed lll limit
126+ ScrollVerticalMultiplier float64 `mapstructure:"scroll_vertical_multiplier" toml:"scroll_vertical_multiplier" yaml:"scroll_vertical_multiplier"` //nolint:lll // struct tags exceed lll limit
127+ ScrollMaxDelta int32 `mapstructure:"scroll_max_delta" toml:"scroll_max_delta" yaml:"scroll_max_delta"` //nolint:lll // struct tags exceed lll limit
128+ TouchpadNavigationEnabled bool `mapstructure:"touchpad_navigation_enabled" toml:"touchpad_navigation_enabled" yaml:"touchpad_navigation_enabled"` //nolint:lll // struct tags exceed lll limit
129+ TouchpadNavigationMinDelta float64 `mapstructure:"touchpad_navigation_min_delta" toml:"touchpad_navigation_min_delta" yaml:"touchpad_navigation_min_delta"` //nolint:lll // struct tags exceed lll limit
130+ TouchpadNavigationMaxVerticalRatio float64 `mapstructure:"touchpad_navigation_max_vertical_ratio" toml:"touchpad_navigation_max_vertical_ratio" yaml:"touchpad_navigation_max_vertical_ratio"` //nolint:lll // struct tags exceed lll limit
128131 }
129132
130133 // CEFRenderStack returns the effective CEF GPU render stack.
@@ -120,11 +120,14 @@ type CEFEngineConfig struct {
120 120
121 // CEFInputConfig holds CEF-specific input translation options.121 // CEFInputConfig holds CEF-specific input translation options.
122 type CEFInputConfig struct {122 type CEFInputConfig struct {
123- ScrollWheelMultiplier float64 `mapstructure:"scroll_wheel_multiplier" toml:"scroll_wheel_multiplier" yaml:"scroll_wheel_multiplier"` //nolint:lll // struct tags exceed lll limit123+ ScrollWheelMultiplier float64 `mapstructure:"scroll_wheel_multiplier" toml:"scroll_wheel_multiplier" yaml:"scroll_wheel_multiplier"` //nolint:lll // struct tags exceed lll limit
124- ScrollTouchpadMultiplier float64 `mapstructure:"scroll_touchpad_multiplier" toml:"scroll_touchpad_multiplier" yaml:"scroll_touchpad_multiplier"` //nolint:lll // struct tags exceed lll limit124+ ScrollTouchpadMultiplier float64 `mapstructure:"scroll_touchpad_multiplier" toml:"scroll_touchpad_multiplier" yaml:"scroll_touchpad_multiplier"` //nolint:lll // struct tags exceed lll limit
125- ScrollHorizontalMultiplier float64 `mapstructure:"scroll_horizontal_multiplier" toml:"scroll_horizontal_multiplier" yaml:"scroll_horizontal_multiplier"` //nolint:lll // struct tags exceed lll limit125+ ScrollHorizontalMultiplier float64 `mapstructure:"scroll_horizontal_multiplier" toml:"scroll_horizontal_multiplier" yaml:"scroll_horizontal_multiplier"` //nolint:lll // struct tags exceed lll limit
126- ScrollVerticalMultiplier float64 `mapstructure:"scroll_vertical_multiplier" toml:"scroll_vertical_multiplier" yaml:"scroll_vertical_multiplier"` //nolint:lll // struct tags exceed lll limit126+ ScrollVerticalMultiplier float64 `mapstructure:"scroll_vertical_multiplier" toml:"scroll_vertical_multiplier" yaml:"scroll_vertical_multiplier"` //nolint:lll // struct tags exceed lll limit
127- ScrollMaxDelta int32 `mapstructure:"scroll_max_delta" toml:"scroll_max_delta" yaml:"scroll_max_delta"` //nolint:lll // struct tags exceed lll limit127+ ScrollMaxDelta int32 `mapstructure:"scroll_max_delta" toml:"scroll_max_delta" yaml:"scroll_max_delta"` //nolint:lll // struct tags exceed lll limit
128+ TouchpadNavigationEnabled bool `mapstructure:"touchpad_navigation_enabled" toml:"touchpad_navigation_enabled" yaml:"touchpad_navigation_enabled"` //nolint:lll // struct tags exceed lll limit
129+ TouchpadNavigationMinDelta float64 `mapstructure:"touchpad_navigation_min_delta" toml:"touchpad_navigation_min_delta" yaml:"touchpad_navigation_min_delta"` //nolint:lll // struct tags exceed lll limit
130+ TouchpadNavigationMaxVerticalRatio float64 `mapstructure:"touchpad_navigation_max_vertical_ratio" toml:"touchpad_navigation_max_vertical_ratio" yaml:"touchpad_navigation_max_vertical_ratio"` //nolint:lll // struct tags exceed lll limit
128 }131 }
129 132
130 // CEFRenderStack returns the effective CEF GPU render stack.133 // CEFRenderStack returns the effective CEF GPU render stack.
M internal/infrastructure/config/loader.go +12 -5
@@ -746,11 +746,7 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
746746 m.viper.SetDefault("engine.cef.windowless_frame_rate_max", ce.CEFWindowlessFrameRateMax())
747747 m.viper.SetDefault("engine.cef.enable_audio_handler", ce.EnableAudioHandler)
748748 m.viper.SetDefault("engine.cef.trace_handlers", ce.TraceHandlers)
749- m.viper.SetDefault("engine.cef.input.scroll_wheel_multiplier", ce.Input.ScrollWheelMultiplier)
750- m.viper.SetDefault("engine.cef.input.scroll_touchpad_multiplier", ce.Input.ScrollTouchpadMultiplier)
751- m.viper.SetDefault("engine.cef.input.scroll_horizontal_multiplier", ce.Input.ScrollHorizontalMultiplier)
752- m.viper.SetDefault("engine.cef.input.scroll_vertical_multiplier", ce.Input.ScrollVerticalMultiplier)
753- m.viper.SetDefault("engine.cef.input.scroll_max_delta", ce.Input.ScrollMaxDelta)
749+ m.setCEFInputDefaults(ce.Input)
754750
755751 wk := e.WebKit
756752 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)
@@ -781,6 +777,17 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
781777 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)
782778 }
783779
780+func (m *Manager) setCEFInputDefaults(input CEFInputConfig) {
781+ m.viper.SetDefault("engine.cef.input.scroll_wheel_multiplier", input.ScrollWheelMultiplier)
782+ m.viper.SetDefault("engine.cef.input.scroll_touchpad_multiplier", input.ScrollTouchpadMultiplier)
783+ m.viper.SetDefault("engine.cef.input.scroll_horizontal_multiplier", input.ScrollHorizontalMultiplier)
784+ m.viper.SetDefault("engine.cef.input.scroll_vertical_multiplier", input.ScrollVerticalMultiplier)
785+ m.viper.SetDefault("engine.cef.input.scroll_max_delta", input.ScrollMaxDelta)
786+ m.viper.SetDefault("engine.cef.input.touchpad_navigation_enabled", input.TouchpadNavigationEnabled)
787+ m.viper.SetDefault("engine.cef.input.touchpad_navigation_min_delta", input.TouchpadNavigationMinDelta)
788+ m.viper.SetDefault("engine.cef.input.touchpad_navigation_max_vertical_ratio", input.TouchpadNavigationMaxVerticalRatio)
789+}
790+
784791 // New returns a new default configuration instance.
785792 // This is a convenience function for getting default config without the full manager.
786793 func New() *Config {
@@ -746,11 +746,7 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
746 m.viper.SetDefault("engine.cef.windowless_frame_rate_max", ce.CEFWindowlessFrameRateMax())746 m.viper.SetDefault("engine.cef.windowless_frame_rate_max", ce.CEFWindowlessFrameRateMax())
747 m.viper.SetDefault("engine.cef.enable_audio_handler", ce.EnableAudioHandler)747 m.viper.SetDefault("engine.cef.enable_audio_handler", ce.EnableAudioHandler)
748 m.viper.SetDefault("engine.cef.trace_handlers", ce.TraceHandlers)748 m.viper.SetDefault("engine.cef.trace_handlers", ce.TraceHandlers)
749- m.viper.SetDefault("engine.cef.input.scroll_wheel_multiplier", ce.Input.ScrollWheelMultiplier)749+ m.setCEFInputDefaults(ce.Input)
750- m.viper.SetDefault("engine.cef.input.scroll_touchpad_multiplier", ce.Input.ScrollTouchpadMultiplier)
751- m.viper.SetDefault("engine.cef.input.scroll_horizontal_multiplier", ce.Input.ScrollHorizontalMultiplier)
752- m.viper.SetDefault("engine.cef.input.scroll_vertical_multiplier", ce.Input.ScrollVerticalMultiplier)
753- m.viper.SetDefault("engine.cef.input.scroll_max_delta", ce.Input.ScrollMaxDelta)
754 750
755 wk := e.WebKit751 wk := e.WebKit
756 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)752 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)
@@ -781,6 +777,17 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
781 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)777 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)
782 }778 }
783 779
780+func (m *Manager) setCEFInputDefaults(input CEFInputConfig) {
781+ m.viper.SetDefault("engine.cef.input.scroll_wheel_multiplier", input.ScrollWheelMultiplier)
782+ m.viper.SetDefault("engine.cef.input.scroll_touchpad_multiplier", input.ScrollTouchpadMultiplier)
783+ m.viper.SetDefault("engine.cef.input.scroll_horizontal_multiplier", input.ScrollHorizontalMultiplier)
784+ m.viper.SetDefault("engine.cef.input.scroll_vertical_multiplier", input.ScrollVerticalMultiplier)
785+ m.viper.SetDefault("engine.cef.input.scroll_max_delta", input.ScrollMaxDelta)
786+ m.viper.SetDefault("engine.cef.input.touchpad_navigation_enabled", input.TouchpadNavigationEnabled)
787+ m.viper.SetDefault("engine.cef.input.touchpad_navigation_min_delta", input.TouchpadNavigationMinDelta)
788+ m.viper.SetDefault("engine.cef.input.touchpad_navigation_max_vertical_ratio", input.TouchpadNavigationMaxVerticalRatio)
789+}
790+
784 // New returns a new default configuration instance.791 // New returns a new default configuration instance.
785 // This is a convenience function for getting default config without the full manager.792 // This is a convenience function for getting default config without the full manager.
786 func New() *Config {793 func New() *Config {
M internal/infrastructure/config/loader_test.go +3 -0
@@ -22,6 +22,9 @@ func TestSetEngineDefaults(t *testing.T) {
2222 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_horizontal_multiplier"), 0.001)
2323 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_vertical_multiplier"), 0.001)
2424 assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))
25+ assert.Equal(t, defaultCEFTouchpadNavigation, mgr.viper.GetBool("engine.cef.input.touchpad_navigation_enabled"))
26+ assert.InDelta(t, defaultCEFTouchpadNavigationDelta, mgr.viper.GetFloat64("engine.cef.input.touchpad_navigation_min_delta"), 0.001)
27+ assert.InDelta(t, defaultCEFTouchpadNavigationRatio, mgr.viper.GetFloat64("engine.cef.input.touchpad_navigation_max_vertical_ratio"), 0.001)
2528
2629 // WebKit-specific engine fields
2730 assert.True(t, mgr.viper.GetBool("engine.webkit.itp_enabled"))
@@ -22,6 +22,9 @@ func TestSetEngineDefaults(t *testing.T) {
22 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_horizontal_multiplier"), 0.001)22 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_horizontal_multiplier"), 0.001)
23 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_vertical_multiplier"), 0.001)23 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_vertical_multiplier"), 0.001)
24 assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))24 assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))
25+ assert.Equal(t, defaultCEFTouchpadNavigation, mgr.viper.GetBool("engine.cef.input.touchpad_navigation_enabled"))
26+ assert.InDelta(t, defaultCEFTouchpadNavigationDelta, mgr.viper.GetFloat64("engine.cef.input.touchpad_navigation_min_delta"), 0.001)
27+ assert.InDelta(t, defaultCEFTouchpadNavigationRatio, mgr.viper.GetFloat64("engine.cef.input.touchpad_navigation_max_vertical_ratio"), 0.001)
25 28
26 // WebKit-specific engine fields29 // WebKit-specific engine fields
27 assert.True(t, mgr.viper.GetBool("engine.webkit.itp_enabled"))30 assert.True(t, mgr.viper.GetBool("engine.webkit.itp_enabled"))
M internal/infrastructure/config/schema_provider.go +23 -0
@@ -1002,6 +1002,29 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
10021002 Range: ">=0",
10031003 Section: SectionPerformance,
10041004 },
1005+ {
1006+ Key: "engine.cef.input.touchpad_navigation_enabled",
1007+ Type: "bool",
1008+ Default: fmt.Sprintf("%t", defaults.Engine.CEF.Input.TouchpadNavigationEnabled),
1009+ Description: "Enable two-finger touchpad swipe back/forward navigation for CEF views",
1010+ Section: SectionPerformance,
1011+ },
1012+ {
1013+ Key: "engine.cef.input.touchpad_navigation_min_delta",
1014+ Type: "float64",
1015+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.TouchpadNavigationMinDelta),
1016+ Description: "Minimum accumulated horizontal touchpad swipe delta required for CEF back/forward navigation",
1017+ Range: ">0",
1018+ Section: SectionPerformance,
1019+ },
1020+ {
1021+ Key: "engine.cef.input.touchpad_navigation_max_vertical_ratio",
1022+ Type: "float64",
1023+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio),
1024+ Description: "Maximum vertical-to-horizontal delta ratio allowed for CEF touchpad navigation swipes",
1025+ Range: ">0",
1026+ Section: SectionPerformance,
1027+ },
10051028 {
10061029 Key: "engine.webkit.skia_cpu_painting_threads",
10071030 Type: "int",
@@ -1002,6 +1002,29 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
1002 Range: ">=0",1002 Range: ">=0",
1003 Section: SectionPerformance,1003 Section: SectionPerformance,
1004 },1004 },
1005+ {
1006+ Key: "engine.cef.input.touchpad_navigation_enabled",
1007+ Type: "bool",
1008+ Default: fmt.Sprintf("%t", defaults.Engine.CEF.Input.TouchpadNavigationEnabled),
1009+ Description: "Enable two-finger touchpad swipe back/forward navigation for CEF views",
1010+ Section: SectionPerformance,
1011+ },
1012+ {
1013+ Key: "engine.cef.input.touchpad_navigation_min_delta",
1014+ Type: "float64",
1015+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.TouchpadNavigationMinDelta),
1016+ Description: "Minimum accumulated horizontal touchpad swipe delta required for CEF back/forward navigation",
1017+ Range: ">0",
1018+ Section: SectionPerformance,
1019+ },
1020+ {
1021+ Key: "engine.cef.input.touchpad_navigation_max_vertical_ratio",
1022+ Type: "float64",
1023+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio),
1024+ Description: "Maximum vertical-to-horizontal delta ratio allowed for CEF touchpad navigation swipes",
1025+ Range: ">0",
1026+ Section: SectionPerformance,
1027+ },
1005 {1028 {
1006 Key: "engine.webkit.skia_cpu_painting_threads",1029 Key: "engine.webkit.skia_cpu_painting_threads",
1007 Type: "int",1030 Type: "int",
M internal/infrastructure/config/validation.go +6 -0
@@ -379,6 +379,12 @@ func validateCEF(config *Config) []string {
379379 if input.ScrollMaxDelta < 0 {
380380 validationErrors = append(validationErrors, "engine.cef.input.scroll_max_delta must be >= 0")
381381 }
382+ if invalidPositiveFloat(input.TouchpadNavigationMinDelta) {
383+ validationErrors = append(validationErrors, "engine.cef.input.touchpad_navigation_min_delta must be > 0")
384+ }
385+ if invalidPositiveFloat(input.TouchpadNavigationMaxVerticalRatio) {
386+ validationErrors = append(validationErrors, "engine.cef.input.touchpad_navigation_max_vertical_ratio must be > 0")
387+ }
382388
383389 return validationErrors
384390 }
@@ -379,6 +379,12 @@ func validateCEF(config *Config) []string {
379 if input.ScrollMaxDelta < 0 {379 if input.ScrollMaxDelta < 0 {
380 validationErrors = append(validationErrors, "engine.cef.input.scroll_max_delta must be >= 0")380 validationErrors = append(validationErrors, "engine.cef.input.scroll_max_delta must be >= 0")
381 }381 }
382+ if invalidPositiveFloat(input.TouchpadNavigationMinDelta) {
383+ validationErrors = append(validationErrors, "engine.cef.input.touchpad_navigation_min_delta must be > 0")
384+ }
385+ if invalidPositiveFloat(input.TouchpadNavigationMaxVerticalRatio) {
386+ validationErrors = append(validationErrors, "engine.cef.input.touchpad_navigation_max_vertical_ratio must be > 0")
387+ }
382 388
383 return validationErrors389 return validationErrors
384 }390 }
M internal/infrastructure/config/validation_test.go +16 -0
@@ -135,6 +135,22 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
135135 wantErr: true,
136136 wantText: "engine.cef.input.scroll_max_delta",
137137 },
138+ {
139+ name: "zero touchpad navigation min delta",
140+ mutate: func(cfg *Config) {
141+ cfg.Engine.CEF.Input.TouchpadNavigationMinDelta = 0
142+ },
143+ wantErr: true,
144+ wantText: "engine.cef.input.touchpad_navigation_min_delta",
145+ },
146+ {
147+ name: "nan touchpad navigation max vertical ratio",
148+ mutate: func(cfg *Config) {
149+ cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio = math.NaN()
150+ },
151+ wantErr: true,
152+ wantText: "engine.cef.input.touchpad_navigation_max_vertical_ratio",
153+ },
138154 }
139155
140156 for _, tt := range tests {
@@ -135,6 +135,22 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
135 wantErr: true,135 wantErr: true,
136 wantText: "engine.cef.input.scroll_max_delta",136 wantText: "engine.cef.input.scroll_max_delta",
137 },137 },
138+ {
139+ name: "zero touchpad navigation min delta",
140+ mutate: func(cfg *Config) {
141+ cfg.Engine.CEF.Input.TouchpadNavigationMinDelta = 0
142+ },
143+ wantErr: true,
144+ wantText: "engine.cef.input.touchpad_navigation_min_delta",
145+ },
146+ {
147+ name: "nan touchpad navigation max vertical ratio",
148+ mutate: func(cfg *Config) {
149+ cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio = math.NaN()
150+ },
151+ wantErr: true,
152+ wantText: "engine.cef.input.touchpad_navigation_max_vertical_ratio",
153+ },
138 }154 }
139 155
140 for _, tt := range tests {156 for _, tt := range tests {

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