detached 40f7675

feat(cef): add scroll input tuning

commit
40f76751e0c937fa90985235fadcd6815ba69c28
author
brice · · 2026-06-02 03:20 UTC
parent
cad3fd5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
18 files · +226 -11
M docs/config/index.md +5 -0
@@ -194,6 +194,11 @@ border = "#363636"
194194 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | - | Enable adaptive CEF OSR FPS polling (only applies when `windowless_frame_rate = 0`) |
195195 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |
196196 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |
197+| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 | Mouse wheel scroll sensitivity multiplier |
198+| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 | Touchpad/smooth 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 |
201+| `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |
197202 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
198203 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
199204
@@ -194,6 +194,11 @@ border = "#363636"
194 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | - | Enable adaptive CEF OSR FPS polling (only applies when `windowless_frame_rate = 0`) |194 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | - | Enable adaptive CEF OSR FPS polling (only applies when `windowless_frame_rate = 0`) |
195 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |195 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |
196 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |196 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |
197+| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 | Mouse wheel scroll sensitivity multiplier |
198+| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 | Touchpad/smooth 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 |
201+| `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |
197 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |202 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
198 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |203 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
199 204
M docs/reference/configuration.md +5 -0
@@ -64,6 +64,11 @@
6464 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
6565 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
6666 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |
67+| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 |
68+| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 |
69+| `engine.cef.input.scroll_horizontal_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 |
6772 | `default_ui_scale` | float | `1.0` | > 0 |
6873 | `default_webpage_zoom` | float | `1.2` | > 0 |
6974 | `workspace.new_pane_url` | string | `about:blank` | |
@@ -64,6 +64,11 @@
64 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |64 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
65 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |65 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
66 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |66 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |
67+| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 |
68+| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 |
69+| `engine.cef.input.scroll_horizontal_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 |
67 | `default_ui_scale` | float | `1.0` | > 0 |72 | `default_ui_scale` | float | `1.0` | > 0 |
68 | `default_webpage_zoom` | float | `1.2` | > 0 |73 | `default_webpage_zoom` | float | `1.2` | > 0 |
69 | `workspace.new_pane_url` | string | `about:blank` | |74 | `workspace.new_pane_url` | string | `about:blank` | |
M go.mod +1 -1
@@ -7,7 +7,7 @@ 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.5.6
10+ github.com/bnema/purego-cef2gtk v0.6.0
1111 github.com/bnema/purego-pipewire v0.1.4
1212 github.com/bnema/purego-sqlite v0.1.3
1313 github.com/bnema/puregotk v0.5.4
@@ -7,7 +7,7 @@ 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.5.610+ github.com/bnema/purego-cef2gtk v0.6.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.5.4
M go.sum +2 -2 generated file: collapsed
M internal/bootstrap/engine.go +10 -3
@@ -84,9 +84,16 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
8484 AdaptiveWindowlessFrameRate: cfg.Engine.CEF.CEFAdaptiveWindowlessFrameRate(),
8585 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,
8686 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),
87- EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
88- TraceHandlers: cfg.Engine.CEF.TraceHandlers,
89- ApplicationScale: cfg.DefaultUIScale,
87+ 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,
93+ },
94+ EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
95+ TraceHandlers: cfg.Engine.CEF.TraceHandlers,
96+ ApplicationScale: cfg.DefaultUIScale,
9097 }
9198 deps := cef.EngineDependencies{
9299 RegisterHandlers: handlers.RegisterAll,
@@ -84,9 +84,16 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
84 AdaptiveWindowlessFrameRate: cfg.Engine.CEF.CEFAdaptiveWindowlessFrameRate(),84 AdaptiveWindowlessFrameRate: cfg.Engine.CEF.CEFAdaptiveWindowlessFrameRate(),
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- EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,87+ Input: cef.RuntimeInputConfig{
88- TraceHandlers: cfg.Engine.CEF.TraceHandlers,88+ ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,
89- ApplicationScale: cfg.DefaultUIScale,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+ },
94+ EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
95+ TraceHandlers: cfg.Engine.CEF.TraceHandlers,
96+ ApplicationScale: cfg.DefaultUIScale,
90 }97 }
91 deps := cef.EngineDependencies{98 deps := cef.EngineDependencies{
92 RegisterHandlers: handlers.RegisterAll,99 RegisterHandlers: handlers.RegisterAll,
M internal/infrastructure/cef/engine_init.go +1 -0
@@ -104,6 +104,7 @@ func NewEngine(
104104 adaptiveWindowlessFrameRate: cfg.AdaptiveWindowlessFrameRate,
105105 windowlessFrameRate: windowlessFrameRate,
106106 windowlessFrameRateMax: cfg.WindowlessFrameRateMax,
107+ inputConfig: cfg.Input,
107108 audioOutputFactory: audioFactory,
108109 },
109110 logger,
@@ -104,6 +104,7 @@ func NewEngine(
104 adaptiveWindowlessFrameRate: cfg.AdaptiveWindowlessFrameRate,104 adaptiveWindowlessFrameRate: cfg.AdaptiveWindowlessFrameRate,
105 windowlessFrameRate: windowlessFrameRate,105 windowlessFrameRate: windowlessFrameRate,
106 windowlessFrameRateMax: cfg.WindowlessFrameRateMax,106 windowlessFrameRateMax: cfg.WindowlessFrameRateMax,
107+ inputConfig: cfg.Input,
107 audioOutputFactory: audioFactory,108 audioOutputFactory: audioFactory,
108 },109 },
109 logger,110 logger,
M internal/infrastructure/cef/factory.go +4 -0
@@ -25,6 +25,7 @@ type WebViewFactory struct {
2525 adaptiveWindowlessFrameRate bool
2626 windowlessFrameRate int32
2727 windowlessFrameRateMax int32
28+ inputConfig RuntimeInputConfig
2829 bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor
2930 audioOutputFactory port.AudioOutputFactory
3031 }
@@ -33,6 +34,7 @@ type webViewFactoryOptions struct {
3334 adaptiveWindowlessFrameRate bool
3435 windowlessFrameRate int32
3536 windowlessFrameRateMax int32
37+ inputConfig RuntimeInputConfig
3638 audioOutputFactory port.AudioOutputFactory
3739 }
3840
@@ -61,6 +63,7 @@ func newWebViewFactory(engine *Engine, opts webViewFactoryOptions) *WebViewFacto
6163 adaptiveWindowlessFrameRate: opts.adaptiveWindowlessFrameRate,
6264 windowlessFrameRate: opts.windowlessFrameRate,
6365 windowlessFrameRateMax: opts.windowlessFrameRateMax,
66+ inputConfig: opts.inputConfig,
6467 audioOutputFactory: opts.audioOutputFactory,
6568 }
6669 }
@@ -122,6 +125,7 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
122125 adaptiveWindowlessFrameRate: f.adaptiveWindowlessFrameRate,
123126 windowlessFrameRate: f.windowlessFrameRate,
124127 windowlessFrameRateMax: f.windowlessFrameRateMax,
128+ inputConfig: f.inputConfig,
125129 backgroundColor: f.bgColor.Load(),
126130 }
127131 wv.runOnGTKSync(func() {
@@ -25,6 +25,7 @@ type WebViewFactory struct {
25 adaptiveWindowlessFrameRate bool25 adaptiveWindowlessFrameRate bool
26 windowlessFrameRate int3226 windowlessFrameRate int32
27 windowlessFrameRateMax int3227 windowlessFrameRateMax int32
28+ inputConfig RuntimeInputConfig
28 bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor29 bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor
29 audioOutputFactory port.AudioOutputFactory30 audioOutputFactory port.AudioOutputFactory
30 }31 }
@@ -33,6 +34,7 @@ type webViewFactoryOptions struct {
33 adaptiveWindowlessFrameRate bool34 adaptiveWindowlessFrameRate bool
34 windowlessFrameRate int3235 windowlessFrameRate int32
35 windowlessFrameRateMax int3236 windowlessFrameRateMax int32
37+ inputConfig RuntimeInputConfig
36 audioOutputFactory port.AudioOutputFactory38 audioOutputFactory port.AudioOutputFactory
37 }39 }
38 40
@@ -61,6 +63,7 @@ func newWebViewFactory(engine *Engine, opts webViewFactoryOptions) *WebViewFacto
61 adaptiveWindowlessFrameRate: opts.adaptiveWindowlessFrameRate,63 adaptiveWindowlessFrameRate: opts.adaptiveWindowlessFrameRate,
62 windowlessFrameRate: opts.windowlessFrameRate,64 windowlessFrameRate: opts.windowlessFrameRate,
63 windowlessFrameRateMax: opts.windowlessFrameRateMax,65 windowlessFrameRateMax: opts.windowlessFrameRateMax,
66+ inputConfig: opts.inputConfig,
64 audioOutputFactory: opts.audioOutputFactory,67 audioOutputFactory: opts.audioOutputFactory,
65 }68 }
66 }69 }
@@ -122,6 +125,7 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
122 adaptiveWindowlessFrameRate: f.adaptiveWindowlessFrameRate,125 adaptiveWindowlessFrameRate: f.adaptiveWindowlessFrameRate,
123 windowlessFrameRate: f.windowlessFrameRate,126 windowlessFrameRate: f.windowlessFrameRate,
124 windowlessFrameRateMax: f.windowlessFrameRateMax,127 windowlessFrameRateMax: f.windowlessFrameRateMax,
128+ inputConfig: f.inputConfig,
125 backgroundColor: f.bgColor.Load(),129 backgroundColor: f.bgColor.Load(),
126 }130 }
127 wv.runOnGTKSync(func() {131 wv.runOnGTKSync(func() {
M internal/infrastructure/cef/runtime_config.go +9 -0
@@ -22,11 +22,20 @@ type RuntimeConfig struct {
2222 AdaptiveWindowlessFrameRate bool
2323 WindowlessFrameRate int32
2424 WindowlessFrameRateMax int32
25+ Input RuntimeInputConfig
2526 EnableAudioHandler bool
2627 TraceHandlers bool
2728 ApplicationScale float64
2829 }
2930
31+type RuntimeInputConfig struct {
32+ ScrollWheelMultiplier float64
33+ ScrollTouchpadMultiplier float64
34+ ScrollHorizontalMultiplier float64
35+ ScrollVerticalMultiplier float64
36+ ScrollMaxDelta int32
37+}
38+
3039 type HandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error
3140
3241 type AccentHandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.AccentKeyHandler) error
@@ -22,11 +22,20 @@ type RuntimeConfig struct {
22 AdaptiveWindowlessFrameRate bool22 AdaptiveWindowlessFrameRate bool
23 WindowlessFrameRate int3223 WindowlessFrameRate int32
24 WindowlessFrameRateMax int3224 WindowlessFrameRateMax int32
25+ Input RuntimeInputConfig
25 EnableAudioHandler bool26 EnableAudioHandler bool
26 TraceHandlers bool27 TraceHandlers bool
27 ApplicationScale float6428 ApplicationScale float64
28 }29 }
29 30
31+type RuntimeInputConfig struct {
32+ ScrollWheelMultiplier float64
33+ ScrollTouchpadMultiplier float64
34+ ScrollHorizontalMultiplier float64
35+ ScrollVerticalMultiplier float64
36+ ScrollMaxDelta int32
37+}
38+
30 type HandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error39 type HandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error
31 40
32 type AccentHandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.AccentKeyHandler) error41 type AccentHandlerRegistrar func(context.Context, port.WebUIHandlerRouter, port.AccentKeyHandler) error
M internal/infrastructure/cef/webview.go +8 -0
@@ -204,6 +204,7 @@ type WebView struct {
204204 // can apply the same settings in OnBeforePopup.
205205 windowlessFrameRate int32
206206 backgroundColor uint32
207+ inputConfig RuntimeInputConfig
207208
208209 // Audio output factory and active stream.
209210 audioOutputFactory port.AudioOutputFactory
@@ -1428,6 +1429,13 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14281429 }
14291430 return cef2gtk.InputOptions{
14301431 Scale: 0,
1432+ Scroll: cef2gtk.ScrollOptions{
1433+ WheelMultiplier: wv.inputConfig.ScrollWheelMultiplier,
1434+ TouchpadMultiplier: wv.inputConfig.ScrollTouchpadMultiplier,
1435+ HorizontalMultiplier: wv.inputConfig.ScrollHorizontalMultiplier,
1436+ VerticalMultiplier: wv.inputConfig.ScrollVerticalMultiplier,
1437+ MaxDelta: wv.inputConfig.ScrollMaxDelta,
1438+ },
14311439 OnMiddleClick: func(_, _ float64) bool {
14321440 return wv.handleMiddleClickFromBridge()
14331441 },
@@ -204,6 +204,7 @@ type WebView struct {
204 // can apply the same settings in OnBeforePopup.204 // can apply the same settings in OnBeforePopup.
205 windowlessFrameRate int32205 windowlessFrameRate int32
206 backgroundColor uint32206 backgroundColor uint32
207+ inputConfig RuntimeInputConfig
207 208
208 // Audio output factory and active stream.209 // Audio output factory and active stream.
209 audioOutputFactory port.AudioOutputFactory210 audioOutputFactory port.AudioOutputFactory
@@ -1428,6 +1429,13 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1428 }1429 }
1429 return cef2gtk.InputOptions{1430 return cef2gtk.InputOptions{
1430 Scale: 0,1431 Scale: 0,
1432+ Scroll: cef2gtk.ScrollOptions{
1433+ WheelMultiplier: wv.inputConfig.ScrollWheelMultiplier,
1434+ TouchpadMultiplier: wv.inputConfig.ScrollTouchpadMultiplier,
1435+ HorizontalMultiplier: wv.inputConfig.ScrollHorizontalMultiplier,
1436+ VerticalMultiplier: wv.inputConfig.ScrollVerticalMultiplier,
1437+ MaxDelta: wv.inputConfig.ScrollMaxDelta,
1438+ },
1431 OnMiddleClick: func(_, _ float64) bool {1439 OnMiddleClick: func(_, _ float64) bool {
1432 return wv.handleMiddleClickFromBridge()1440 return wv.handleMiddleClickFromBridge()
1433 },1441 },
M internal/infrastructure/cef/webview_input_test.go +15 -1
@@ -9,11 +9,25 @@ import (
99
1010 func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) {
1111 nativeWidget := &gtk.Widget{}
12- wv := &WebView{nativeWidget: nativeWidget}
12+ wv := &WebView{
13+ nativeWidget: nativeWidget,
14+ inputConfig: RuntimeInputConfig{
15+ ScrollWheelMultiplier: 1.25,
16+ ScrollTouchpadMultiplier: 0.35,
17+ ScrollHorizontalMultiplier: 0.75,
18+ ScrollVerticalMultiplier: 1.5,
19+ ScrollMaxDelta: 120,
20+ },
21+ }
1322
1423 opts := wv.bridgeInputOptions()
1524
1625 require.Zero(t, opts.Scale)
26+ require.InDelta(t, 1.25, opts.Scroll.WheelMultiplier, 0.001)
27+ require.InDelta(t, 0.35, opts.Scroll.TouchpadMultiplier, 0.001)
28+ require.InDelta(t, 0.75, opts.Scroll.HorizontalMultiplier, 0.001)
29+ require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)
30+ require.Equal(t, int32(120), opts.Scroll.MaxDelta)
1731 require.NotNil(t, opts.OnMiddleClick)
1832 require.NotNil(t, opts.SelectionText)
1933 require.NotNil(t, opts.OnClipboardShortcut)
@@ -9,11 +9,25 @@ import (
9 9
10 func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) {10 func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) {
11 nativeWidget := &gtk.Widget{}11 nativeWidget := &gtk.Widget{}
12- wv := &WebView{nativeWidget: nativeWidget}12+ wv := &WebView{
13+ nativeWidget: nativeWidget,
14+ inputConfig: RuntimeInputConfig{
15+ ScrollWheelMultiplier: 1.25,
16+ ScrollTouchpadMultiplier: 0.35,
17+ ScrollHorizontalMultiplier: 0.75,
18+ ScrollVerticalMultiplier: 1.5,
19+ ScrollMaxDelta: 120,
20+ },
21+ }
13 22
14 opts := wv.bridgeInputOptions()23 opts := wv.bridgeInputOptions()
15 24
16 require.Zero(t, opts.Scale)25 require.Zero(t, opts.Scale)
26+ require.InDelta(t, 1.25, opts.Scroll.WheelMultiplier, 0.001)
27+ require.InDelta(t, 0.35, opts.Scroll.TouchpadMultiplier, 0.001)
28+ require.InDelta(t, 0.75, opts.Scroll.HorizontalMultiplier, 0.001)
29+ require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)
30+ require.Equal(t, int32(120), opts.Scroll.MaxDelta)
17 require.NotNil(t, opts.OnMiddleClick)31 require.NotNil(t, opts.OnMiddleClick)
18 require.NotNil(t, opts.SelectionText)32 require.NotNil(t, opts.SelectionText)
19 require.NotNil(t, opts.OnClipboardShortcut)33 require.NotNil(t, opts.OnClipboardShortcut)
M internal/infrastructure/config/defaults.go +14 -4
@@ -67,10 +67,13 @@ const (
6767 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)
6868
6969 // Performance defaults
70- defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
71- defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72- defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
73- defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
70+ defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
71+ defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72+ defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
73+ defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
74+ defaultCEFScrollMultiplier = 1.0
75+ defaultCEFScrollTouchpadMultiplier = 0.35
76+ defaultCEFScrollMaxDelta = 0
7477
7578 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
7679 defaultSkiaCPUPaintingThreads = 0
@@ -178,6 +181,13 @@ func DefaultConfig() *Config {
178181 WindowlessFrameRateMax: defaultCEFWindowlessFrameRateMax,
179182 LogFile: "",
180183 EnableAudioHandler: true,
184+ Input: CEFInputConfig{
185+ ScrollWheelMultiplier: defaultCEFScrollMultiplier,
186+ ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,
187+ ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
188+ ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
189+ ScrollMaxDelta: defaultCEFScrollMaxDelta,
190+ },
181191 },
182192 WebKit: WebKitEngineConfig{
183193 ITPEnabled: true,
@@ -67,10 +67,13 @@ const (
67 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)67 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)
68 68
69 // Performance defaults69 // Performance defaults
70- defaultZoomCacheSize = 256 // domains to cache (~20KB memory)70+ defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
71- defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup71+ defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72- defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled72+ defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
73- defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap73+ defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
74+ defaultCEFScrollMultiplier = 1.0
75+ defaultCEFScrollTouchpadMultiplier = 0.35
76+ defaultCEFScrollMaxDelta = 0
74 77
75 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)78 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
76 defaultSkiaCPUPaintingThreads = 079 defaultSkiaCPUPaintingThreads = 0
@@ -178,6 +181,13 @@ func DefaultConfig() *Config {
178 WindowlessFrameRateMax: defaultCEFWindowlessFrameRateMax,181 WindowlessFrameRateMax: defaultCEFWindowlessFrameRateMax,
179 LogFile: "",182 LogFile: "",
180 EnableAudioHandler: true,183 EnableAudioHandler: true,
184+ Input: CEFInputConfig{
185+ ScrollWheelMultiplier: defaultCEFScrollMultiplier,
186+ ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,
187+ ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
188+ ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
189+ ScrollMaxDelta: defaultCEFScrollMaxDelta,
190+ },
181 },191 },
182 WebKit: WebKitEngineConfig{192 WebKit: WebKitEngineConfig{
183 ITPEnabled: true,193 ITPEnabled: true,
M internal/infrastructure/config/defaults_test.go +5 -0
@@ -25,6 +25,11 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
2525 assert.Empty(t, cfg.Engine.CEF.LogFile)
2626 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)
2727 assert.False(t, cfg.Engine.CEF.TraceHandlers)
28+ assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollWheelMultiplier, 0.001)
29+ assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, cfg.Engine.CEF.Input.ScrollTouchpadMultiplier, 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)
32+ assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
2833 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
2934
3035 // System view shortcuts are first-class global actions.
@@ -25,6 +25,11 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
25 assert.Empty(t, cfg.Engine.CEF.LogFile)25 assert.Empty(t, cfg.Engine.CEF.LogFile)
26 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)26 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)
27 assert.False(t, cfg.Engine.CEF.TraceHandlers)27 assert.False(t, cfg.Engine.CEF.TraceHandlers)
28+ assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollWheelMultiplier, 0.001)
29+ assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, cfg.Engine.CEF.Input.ScrollTouchpadMultiplier, 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)
32+ assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
28 assert.True(t, cfg.Engine.WebKit.ITPEnabled)33 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
29 34
30 // System view shortcuts are first-class global actions.35 // System view shortcuts are first-class global actions.
M internal/infrastructure/config/engine.go +11 -0
@@ -110,12 +110,23 @@ type CEFEngineConfig struct {
110110 WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` //nolint:lll // struct tags exceed lll limit
111111 // WindowlessFrameRateMax is the hard cap for adaptive monitor refresh rates.
112112 WindowlessFrameRateMax int32 `mapstructure:"windowless_frame_rate_max" toml:"windowless_frame_rate_max" yaml:"windowless_frame_rate_max"` //nolint:lll // struct tags exceed lll limit
113+ // Input controls GTK input translation before events are forwarded to CEF.
114+ Input CEFInputConfig `mapstructure:"input" toml:"input" yaml:"input"`
113115 // EnableAudioHandler opts into the experimental CEF AudioHandler bridge.
114116 EnableAudioHandler bool `mapstructure:"enable_audio_handler" toml:"enable_audio_handler" yaml:"enable_audio_handler"`
115117 // TraceHandlers enables purego-cef handler/refcount tracing for diagnostics.
116118 TraceHandlers bool `mapstructure:"trace_handlers" toml:"trace_handlers" yaml:"trace_handlers"`
117119 }
118120
121+// CEFInputConfig holds CEF-specific input translation options.
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 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+}
129+
119130 // CEFRenderStack returns the effective CEF GPU render stack.
120131 func (c CEFEngineConfig) CEFRenderStack() CEFRenderStack {
121132 if c.RenderStack != "" {
@@ -110,12 +110,23 @@ type CEFEngineConfig struct {
110 WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` //nolint:lll // struct tags exceed lll limit110 WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` //nolint:lll // struct tags exceed lll limit
111 // WindowlessFrameRateMax is the hard cap for adaptive monitor refresh rates.111 // WindowlessFrameRateMax is the hard cap for adaptive monitor refresh rates.
112 WindowlessFrameRateMax int32 `mapstructure:"windowless_frame_rate_max" toml:"windowless_frame_rate_max" yaml:"windowless_frame_rate_max"` //nolint:lll // struct tags exceed lll limit112 WindowlessFrameRateMax int32 `mapstructure:"windowless_frame_rate_max" toml:"windowless_frame_rate_max" yaml:"windowless_frame_rate_max"` //nolint:lll // struct tags exceed lll limit
113+ // Input controls GTK input translation before events are forwarded to CEF.
114+ Input CEFInputConfig `mapstructure:"input" toml:"input" yaml:"input"`
113 // EnableAudioHandler opts into the experimental CEF AudioHandler bridge.115 // EnableAudioHandler opts into the experimental CEF AudioHandler bridge.
114 EnableAudioHandler bool `mapstructure:"enable_audio_handler" toml:"enable_audio_handler" yaml:"enable_audio_handler"`116 EnableAudioHandler bool `mapstructure:"enable_audio_handler" toml:"enable_audio_handler" yaml:"enable_audio_handler"`
115 // TraceHandlers enables purego-cef handler/refcount tracing for diagnostics.117 // TraceHandlers enables purego-cef handler/refcount tracing for diagnostics.
116 TraceHandlers bool `mapstructure:"trace_handlers" toml:"trace_handlers" yaml:"trace_handlers"`118 TraceHandlers bool `mapstructure:"trace_handlers" toml:"trace_handlers" yaml:"trace_handlers"`
117 }119 }
118 120
121+// CEFInputConfig holds CEF-specific input translation options.
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 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+}
129+
119 // CEFRenderStack returns the effective CEF GPU render stack.130 // CEFRenderStack returns the effective CEF GPU render stack.
120 func (c CEFEngineConfig) CEFRenderStack() CEFRenderStack {131 func (c CEFEngineConfig) CEFRenderStack() CEFRenderStack {
121 if c.RenderStack != "" {132 if c.RenderStack != "" {
M internal/infrastructure/config/loader.go +5 -0
@@ -746,6 +746,11 @@ 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)
749754
750755 wk := e.WebKit
751756 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)
@@ -746,6 +746,11 @@ 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)
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 754
750 wk := e.WebKit755 wk := e.WebKit
751 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)756 m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled)
M internal/infrastructure/config/loader_test.go +5 -0
@@ -17,6 +17,11 @@ func TestSetEngineDefaults(t *testing.T) {
1717 assert.Equal(t, "always", mgr.viper.GetString("engine.cookie_policy"))
1818 assert.Equal(t, 4, mgr.viper.GetInt("engine.pool_prewarm_count"))
1919 assert.Equal(t, 256, mgr.viper.GetInt("engine.zoom_cache_size"))
20+ assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_wheel_multiplier"), 0.001)
21+ assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_touchpad_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)
24+ assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))
2025
2126 // WebKit-specific engine fields
2227 assert.True(t, mgr.viper.GetBool("engine.webkit.itp_enabled"))
@@ -17,6 +17,11 @@ func TestSetEngineDefaults(t *testing.T) {
17 assert.Equal(t, "always", mgr.viper.GetString("engine.cookie_policy"))17 assert.Equal(t, "always", mgr.viper.GetString("engine.cookie_policy"))
18 assert.Equal(t, 4, mgr.viper.GetInt("engine.pool_prewarm_count"))18 assert.Equal(t, 4, mgr.viper.GetInt("engine.pool_prewarm_count"))
19 assert.Equal(t, 256, mgr.viper.GetInt("engine.zoom_cache_size"))19 assert.Equal(t, 256, mgr.viper.GetInt("engine.zoom_cache_size"))
20+ assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_wheel_multiplier"), 0.001)
21+ assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_touchpad_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)
24+ assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))
20 25
21 // WebKit-specific engine fields26 // WebKit-specific engine fields
22 assert.True(t, mgr.viper.GetBool("engine.webkit.itp_enabled"))27 assert.True(t, mgr.viper.GetBool("engine.webkit.itp_enabled"))
M internal/infrastructure/config/schema_provider.go +40 -0
@@ -962,6 +962,46 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
962962 Range: ">=0",
963963 Section: SectionPerformance,
964964 },
965+ {
966+ Key: "engine.cef.input.scroll_wheel_multiplier",
967+ Type: "float64",
968+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollWheelMultiplier),
969+ Description: "CEF mouse wheel scroll delta multiplier",
970+ Range: ">0",
971+ Section: SectionPerformance,
972+ },
973+ {
974+ Key: "engine.cef.input.scroll_touchpad_multiplier",
975+ Type: "float64",
976+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollTouchpadMultiplier),
977+ Description: "CEF touchpad/smooth scroll delta multiplier",
978+ Range: ">0",
979+ Section: SectionPerformance,
980+ },
981+ {
982+ Key: "engine.cef.input.scroll_horizontal_multiplier",
983+ Type: "float64",
984+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollHorizontalMultiplier),
985+ Description: "CEF horizontal scroll delta multiplier",
986+ Range: ">0",
987+ Section: SectionPerformance,
988+ },
989+ {
990+ Key: "engine.cef.input.scroll_vertical_multiplier",
991+ Type: "float64",
992+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollVerticalMultiplier),
993+ Description: "CEF vertical scroll delta multiplier",
994+ Range: ">0",
995+ Section: SectionPerformance,
996+ },
997+ {
998+ Key: "engine.cef.input.scroll_max_delta",
999+ Type: "int32",
1000+ Default: fmt.Sprintf("%d", defaults.Engine.CEF.Input.ScrollMaxDelta),
1001+ Description: "CEF maximum absolute scroll delta after scaling; 0 disables clamping",
1002+ Range: ">=0",
1003+ Section: SectionPerformance,
1004+ },
9651005 {
9661006 Key: "engine.webkit.skia_cpu_painting_threads",
9671007 Type: "int",
@@ -962,6 +962,46 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
962 Range: ">=0",962 Range: ">=0",
963 Section: SectionPerformance,963 Section: SectionPerformance,
964 },964 },
965+ {
966+ Key: "engine.cef.input.scroll_wheel_multiplier",
967+ Type: "float64",
968+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollWheelMultiplier),
969+ Description: "CEF mouse wheel scroll delta multiplier",
970+ Range: ">0",
971+ Section: SectionPerformance,
972+ },
973+ {
974+ Key: "engine.cef.input.scroll_touchpad_multiplier",
975+ Type: "float64",
976+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollTouchpadMultiplier),
977+ Description: "CEF touchpad/smooth scroll delta multiplier",
978+ Range: ">0",
979+ Section: SectionPerformance,
980+ },
981+ {
982+ Key: "engine.cef.input.scroll_horizontal_multiplier",
983+ Type: "float64",
984+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollHorizontalMultiplier),
985+ Description: "CEF horizontal scroll delta multiplier",
986+ Range: ">0",
987+ Section: SectionPerformance,
988+ },
989+ {
990+ Key: "engine.cef.input.scroll_vertical_multiplier",
991+ Type: "float64",
992+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollVerticalMultiplier),
993+ Description: "CEF vertical scroll delta multiplier",
994+ Range: ">0",
995+ Section: SectionPerformance,
996+ },
997+ {
998+ Key: "engine.cef.input.scroll_max_delta",
999+ Type: "int32",
1000+ Default: fmt.Sprintf("%d", defaults.Engine.CEF.Input.ScrollMaxDelta),
1001+ Description: "CEF maximum absolute scroll delta after scaling; 0 disables clamping",
1002+ Range: ">=0",
1003+ Section: SectionPerformance,
1004+ },
965 {1005 {
966 Key: "engine.webkit.skia_cpu_painting_threads",1006 Key: "engine.webkit.skia_cpu_painting_threads",
967 Type: "int",1007 Type: "int",
M internal/infrastructure/config/validation.go +22 -0
@@ -2,6 +2,7 @@ package config
22
33 import (
44 "fmt"
5+ "math"
56 "net/url"
67 "sort"
78 "strings"
@@ -362,9 +363,30 @@ func validateCEF(config *Config) []string {
362363 validationErrors = append(validationErrors, "engine.cef.windowless_frame_rate_max must be >= 0")
363364 }
364365
366+ input := config.Engine.CEF.Input
367+ if invalidPositiveFloat(input.ScrollWheelMultiplier) {
368+ validationErrors = append(validationErrors, "engine.cef.input.scroll_wheel_multiplier must be > 0")
369+ }
370+ if invalidPositiveFloat(input.ScrollTouchpadMultiplier) {
371+ validationErrors = append(validationErrors, "engine.cef.input.scroll_touchpad_multiplier must be > 0")
372+ }
373+ if invalidPositiveFloat(input.ScrollHorizontalMultiplier) {
374+ validationErrors = append(validationErrors, "engine.cef.input.scroll_horizontal_multiplier must be > 0")
375+ }
376+ if invalidPositiveFloat(input.ScrollVerticalMultiplier) {
377+ validationErrors = append(validationErrors, "engine.cef.input.scroll_vertical_multiplier must be > 0")
378+ }
379+ if input.ScrollMaxDelta < 0 {
380+ validationErrors = append(validationErrors, "engine.cef.input.scroll_max_delta must be >= 0")
381+ }
382+
365383 return validationErrors
366384 }
367385
386+func invalidPositiveFloat(value float64) bool {
387+ return value <= 0 || math.IsNaN(value) || math.IsInf(value, 0)
388+}
389+
368390 func validateWorkspaceNewPaneURL(config *Config) []string {
369391 var validationErrors []string
370392
@@ -2,6 +2,7 @@ package config
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "math"
5 "net/url"6 "net/url"
6 "sort"7 "sort"
7 "strings"8 "strings"
@@ -362,9 +363,30 @@ func validateCEF(config *Config) []string {
362 validationErrors = append(validationErrors, "engine.cef.windowless_frame_rate_max must be >= 0")363 validationErrors = append(validationErrors, "engine.cef.windowless_frame_rate_max must be >= 0")
363 }364 }
364 365
366+ input := config.Engine.CEF.Input
367+ if invalidPositiveFloat(input.ScrollWheelMultiplier) {
368+ validationErrors = append(validationErrors, "engine.cef.input.scroll_wheel_multiplier must be > 0")
369+ }
370+ if invalidPositiveFloat(input.ScrollTouchpadMultiplier) {
371+ validationErrors = append(validationErrors, "engine.cef.input.scroll_touchpad_multiplier must be > 0")
372+ }
373+ if invalidPositiveFloat(input.ScrollHorizontalMultiplier) {
374+ validationErrors = append(validationErrors, "engine.cef.input.scroll_horizontal_multiplier must be > 0")
375+ }
376+ if invalidPositiveFloat(input.ScrollVerticalMultiplier) {
377+ validationErrors = append(validationErrors, "engine.cef.input.scroll_vertical_multiplier must be > 0")
378+ }
379+ if input.ScrollMaxDelta < 0 {
380+ validationErrors = append(validationErrors, "engine.cef.input.scroll_max_delta must be >= 0")
381+ }
382+
365 return validationErrors383 return validationErrors
366 }384 }
367 385
386+func invalidPositiveFloat(value float64) bool {
387+ return value <= 0 || math.IsNaN(value) || math.IsInf(value, 0)
388+}
389+
368 func validateWorkspaceNewPaneURL(config *Config) []string {390 func validateWorkspaceNewPaneURL(config *Config) []string {
369 var validationErrors []string391 var validationErrors []string
370 392
M internal/infrastructure/config/validation_test.go +64 -0
@@ -1,6 +1,7 @@
11 package config
22
33 import (
4+ "math"
45 "testing"
56
67 "github.com/stretchr/testify/assert"
@@ -71,6 +72,69 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
7172 wantErr: true,
7273 wantText: "engine.cef.windowless_frame_rate",
7374 },
75+ {
76+ name: "zero wheel multiplier",
77+ mutate: func(cfg *Config) {
78+ cfg.Engine.CEF.Input.ScrollWheelMultiplier = 0
79+ },
80+ wantErr: true,
81+ wantText: "engine.cef.input.scroll_wheel_multiplier",
82+ },
83+ {
84+ name: "negative touchpad multiplier",
85+ mutate: func(cfg *Config) {
86+ cfg.Engine.CEF.Input.ScrollTouchpadMultiplier = -0.1
87+ },
88+ wantErr: true,
89+ wantText: "engine.cef.input.scroll_touchpad_multiplier",
90+ },
91+ {
92+ name: "zero horizontal multiplier",
93+ mutate: func(cfg *Config) {
94+ cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = 0
95+ },
96+ wantErr: true,
97+ wantText: "engine.cef.input.scroll_horizontal_multiplier",
98+ },
99+ {
100+ name: "negative vertical multiplier",
101+ mutate: func(cfg *Config) {
102+ cfg.Engine.CEF.Input.ScrollVerticalMultiplier = -0.1
103+ },
104+ wantErr: true,
105+ wantText: "engine.cef.input.scroll_vertical_multiplier",
106+ },
107+ {
108+ name: "nan horizontal multiplier",
109+ mutate: func(cfg *Config) {
110+ cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = math.NaN()
111+ },
112+ wantErr: true,
113+ wantText: "engine.cef.input.scroll_horizontal_multiplier",
114+ },
115+ {
116+ name: "infinite vertical multiplier",
117+ mutate: func(cfg *Config) {
118+ cfg.Engine.CEF.Input.ScrollVerticalMultiplier = math.Inf(1)
119+ },
120+ wantErr: true,
121+ wantText: "engine.cef.input.scroll_vertical_multiplier",
122+ },
123+ {
124+ name: "zero max delta disables clamp",
125+ mutate: func(cfg *Config) {
126+ cfg.Engine.CEF.Input.ScrollMaxDelta = 0
127+ },
128+ wantErr: false,
129+ },
130+ {
131+ name: "negative max delta",
132+ mutate: func(cfg *Config) {
133+ cfg.Engine.CEF.Input.ScrollMaxDelta = -1
134+ },
135+ wantErr: true,
136+ wantText: "engine.cef.input.scroll_max_delta",
137+ },
74138 }
75139
76140 for _, tt := range tests {
@@ -1,6 +1,7 @@
1 package config1 package config
2 2
3 import (3 import (
4+ "math"
4 "testing"5 "testing"
5 6
6 "github.com/stretchr/testify/assert"7 "github.com/stretchr/testify/assert"
@@ -71,6 +72,69 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
71 wantErr: true,72 wantErr: true,
72 wantText: "engine.cef.windowless_frame_rate",73 wantText: "engine.cef.windowless_frame_rate",
73 },74 },
75+ {
76+ name: "zero wheel multiplier",
77+ mutate: func(cfg *Config) {
78+ cfg.Engine.CEF.Input.ScrollWheelMultiplier = 0
79+ },
80+ wantErr: true,
81+ wantText: "engine.cef.input.scroll_wheel_multiplier",
82+ },
83+ {
84+ name: "negative touchpad multiplier",
85+ mutate: func(cfg *Config) {
86+ cfg.Engine.CEF.Input.ScrollTouchpadMultiplier = -0.1
87+ },
88+ wantErr: true,
89+ wantText: "engine.cef.input.scroll_touchpad_multiplier",
90+ },
91+ {
92+ name: "zero horizontal multiplier",
93+ mutate: func(cfg *Config) {
94+ cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = 0
95+ },
96+ wantErr: true,
97+ wantText: "engine.cef.input.scroll_horizontal_multiplier",
98+ },
99+ {
100+ name: "negative vertical multiplier",
101+ mutate: func(cfg *Config) {
102+ cfg.Engine.CEF.Input.ScrollVerticalMultiplier = -0.1
103+ },
104+ wantErr: true,
105+ wantText: "engine.cef.input.scroll_vertical_multiplier",
106+ },
107+ {
108+ name: "nan horizontal multiplier",
109+ mutate: func(cfg *Config) {
110+ cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = math.NaN()
111+ },
112+ wantErr: true,
113+ wantText: "engine.cef.input.scroll_horizontal_multiplier",
114+ },
115+ {
116+ name: "infinite vertical multiplier",
117+ mutate: func(cfg *Config) {
118+ cfg.Engine.CEF.Input.ScrollVerticalMultiplier = math.Inf(1)
119+ },
120+ wantErr: true,
121+ wantText: "engine.cef.input.scroll_vertical_multiplier",
122+ },
123+ {
124+ name: "zero max delta disables clamp",
125+ mutate: func(cfg *Config) {
126+ cfg.Engine.CEF.Input.ScrollMaxDelta = 0
127+ },
128+ wantErr: false,
129+ },
130+ {
131+ name: "negative max delta",
132+ mutate: func(cfg *Config) {
133+ cfg.Engine.CEF.Input.ScrollMaxDelta = -1
134+ },
135+ wantErr: true,
136+ wantText: "engine.cef.input.scroll_max_delta",
137+ },
74 }138 }
75 139
76 for _, tt := range tests {140 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