Merge pull request #271 from bnema/feat/cef-touchpad-navigation
feat(cef): add touchpad swipe navigation
16 files · +255 -36
M docs/config/index.md +3 -0
| @@ -199,6 +199,9 @@ border = "#363636" | |||
| 199 | 199 | | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier | | |
| 200 | 200 | | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier | | |
| 201 | 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 | 205 | | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) | | |
| 203 | 206 | | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) | | |
| 204 | 207 | ||
| @@ -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 @@ | |||
| 69 | 69 | | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | | |
| 70 | 70 | | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | | |
| 71 | 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 | 75 | | `default_ui_scale` | float | `1.0` | > 0 | | |
| 73 | 76 | | `default_webpage_zoom` | float | `1.2` | > 0 | | |
| 74 | 77 | | `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 ( | |||
| 7 | 7 | github.com/andybalholm/brotli v1.2.1 | |
| 8 | 8 | github.com/bnema/purego v0.11.0-bnema.3 | |
| 9 | 9 | 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 | |
| 11 | 11 | github.com/bnema/purego-pipewire v0.1.4 | |
| 12 | 12 | github.com/bnema/purego-sqlite v0.1.3 | |
| 13 | - | github.com/bnema/puregotk v0.5.4 | |
| 13 | + | github.com/bnema/puregotk v0.6.0 | |
| 14 | 14 | github.com/charmbracelet/bubbles v1.0.0 | |
| 15 | 15 | github.com/charmbracelet/bubbletea v1.3.10 | |
| 16 | 16 | github.com/charmbracelet/lipgloss v1.1.0 | |
| @@ -7,10 +7,10 @@ require ( | |||||
| 7 | github.com/andybalholm/brotli v1.2.1 | 7 | github.com/andybalholm/brotli v1.2.1 | ||
| 8 | github.com/bnema/purego v0.11.0-bnema.3 | 8 | github.com/bnema/purego v0.11.0-bnema.3 | ||
| 9 | github.com/bnema/purego-cef v0.13.2 | 9 | 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 |
| 11 | github.com/bnema/purego-pipewire v0.1.4 | 11 | github.com/bnema/purego-pipewire v0.1.4 | ||
| 12 | github.com/bnema/purego-sqlite v0.1.3 | 12 | github.com/bnema/purego-sqlite v0.1.3 | ||
| 13 | - | github.com/bnema/puregotk v0.5.4 | 13 | + | github.com/bnema/puregotk v0.6.0 |
| 14 | github.com/charmbracelet/bubbles v1.0.0 | 14 | github.com/charmbracelet/bubbles v1.0.0 | ||
| 15 | github.com/charmbracelet/bubbletea v1.3.10 | 15 | github.com/charmbracelet/bubbletea v1.3.10 | ||
| 16 | github.com/charmbracelet/lipgloss v1.1.0 | 16 | github.com/charmbracelet/lipgloss v1.1.0 | ||
M go.sum +4 -4 generated file: collapsed
M internal/bootstrap/engine.go +8 -5
| @@ -85,11 +85,14 @@ func BuildEngine(input EngineInput) (port.Engine, error) { | |||
| 85 | 85 | WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate, | |
| 86 | 86 | WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(), | |
| 87 | 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, | |
| 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, | |
| 93 | 96 | }, | |
| 94 | 97 | EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler, | |
| 95 | 98 | 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 { | |||
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | 31 | 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 | |
| 37 | 40 | } | |
| 38 | 41 | ||
| 39 | 42 | 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 float64 | 32 | + | ScrollWheelMultiplier float64 |
| 33 | - | ScrollTouchpadMultiplier float64 | 33 | + | ScrollTouchpadMultiplier float64 |
| 34 | - | ScrollHorizontalMultiplier float64 | 34 | + | ScrollHorizontalMultiplier float64 |
| 35 | - | ScrollVerticalMultiplier float64 | 35 | + | ScrollVerticalMultiplier float64 |
| 36 | - | ScrollMaxDelta int32 | 36 | + | 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) error | 42 | 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 { | |||
| 205 | 205 | windowlessFrameRate int32 | |
| 206 | 206 | backgroundColor uint32 | |
| 207 | 207 | inputConfig RuntimeInputConfig | |
| 208 | + | touchpadSwipeDX float64 | |
| 209 | + | touchpadSwipeDY float64 | |
| 208 | 210 | ||
| 209 | 211 | // Audio output factory and active stream. | |
| 210 | 212 | audioOutputFactory port.AudioOutputFactory | |
| @@ -1439,6 +1441,12 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions { | |||
| 1439 | 1441 | OnMiddleClick: func(_, _ float64) bool { | |
| 1440 | 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 | 1450 | SelectionText: wv.selectedTextSnapshot, | |
| 1443 | 1451 | OnClipboardShortcut: func(action, text string) { | |
| 1444 | 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 | 1550 | func (wv *WebView) viewBridgeScale() float64 { | |
| 1452 | 1551 | if wv == nil || wv.viewBridge == nil { | |
| 1453 | 1552 | return 1 | |
| @@ -205,6 +205,8 @@ type WebView struct { | |||||
| 205 | windowlessFrameRate int32 | 205 | windowlessFrameRate int32 | ||
| 206 | backgroundColor uint32 | 206 | backgroundColor uint32 | ||
| 207 | inputConfig RuntimeInputConfig | 207 | 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.AudioOutputFactory | 212 | 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 1 | 1552 | return 1 | ||
M internal/infrastructure/cef/webview_input_test.go +46 -5
| @@ -12,11 +12,14 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) | |||
| 12 | 12 | wv := &WebView{ | |
| 13 | 13 | nativeWidget: nativeWidget, | |
| 14 | 14 | 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, | |
| 20 | 23 | }, | |
| 21 | 24 | } | |
| 22 | 25 | ||
| @@ -29,7 +32,45 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) | |||
| 29 | 32 | require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001) | |
| 30 | 33 | require.Equal(t, int32(120), opts.Scroll.MaxDelta) | |
| 31 | 34 | require.NotNil(t, opts.OnMiddleClick) | |
| 35 | + | require.NotNil(t, opts.OnTouchpadSwipe) | |
| 32 | 36 | require.NotNil(t, opts.SelectionText) | |
| 33 | 37 | require.NotNil(t, opts.OnClipboardShortcut) | |
| 34 | 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 | + | } | |
| @@ -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 ( | |||
| 74 | 74 | defaultCEFScrollMultiplier = 1.0 | |
| 75 | 75 | defaultCEFScrollTouchpadMultiplier = 0.35 | |
| 76 | 76 | defaultCEFScrollMaxDelta = 0 | |
| 77 | + | defaultCEFTouchpadNavigation = true | |
| 78 | + | defaultCEFTouchpadNavigationDelta = 80.0 | |
| 79 | + | defaultCEFTouchpadNavigationRatio = 0.5 | |
| 77 | 80 | ||
| 78 | 81 | // Skia threading defaults (0 = unset, -1 = unset for GPU threads) | |
| 79 | 82 | defaultSkiaCPUPaintingThreads = 0 | |
| @@ -182,11 +185,14 @@ func DefaultConfig() *Config { | |||
| 182 | 185 | LogFile: "", | |
| 183 | 186 | EnableAudioHandler: true, | |
| 184 | 187 | 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, | |
| 190 | 196 | }, | |
| 191 | 197 | }, | |
| 192 | 198 | WebKit: WebKitEngineConfig{ | |
| @@ -74,6 +74,9 @@ const ( | |||||
| 74 | defaultCEFScrollMultiplier = 1.0 | 74 | defaultCEFScrollMultiplier = 1.0 | ||
| 75 | defaultCEFScrollTouchpadMultiplier = 0.35 | 75 | defaultCEFScrollTouchpadMultiplier = 0.35 | ||
| 76 | defaultCEFScrollMaxDelta = 0 | 76 | 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 = 0 | 82 | 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) { | |||
| 30 | 30 | assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollHorizontalMultiplier, 0.001) | |
| 31 | 31 | assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollVerticalMultiplier, 0.001) | |
| 32 | 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 | 36 | assert.True(t, cfg.Engine.WebKit.ITPEnabled) | |
| 34 | 37 | ||
| 35 | 38 | // 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 { | |||
| 120 | 120 | ||
| 121 | 121 | // CEFInputConfig holds CEF-specific input translation options. | |
| 122 | 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 | |
| 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 | |
| 128 | 131 | } | |
| 129 | 132 | ||
| 130 | 133 | // 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 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 | 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 | 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 | 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 | 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 | |||
| 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) { | |||
| 746 | 746 | m.viper.SetDefault("engine.cef.windowless_frame_rate_max", ce.CEFWindowlessFrameRateMax()) | |
| 747 | 747 | m.viper.SetDefault("engine.cef.enable_audio_handler", ce.EnableAudioHandler) | |
| 748 | 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 | + | m.setCEFInputDefaults(ce.Input) | |
| 754 | 750 | ||
| 755 | 751 | wk := e.WebKit | |
| 756 | 752 | m.viper.SetDefault("engine.webkit.itp_enabled", wk.ITPEnabled) | |
| @@ -781,6 +777,17 @@ func (m *Manager) setEngineDefaults(defaults *Config) { | |||
| 781 | 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 | 791 | // New returns a new default configuration instance. | |
| 785 | 792 | // This is a convenience function for getting default config without the full manager. | |
| 786 | 793 | 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.WebKit | 751 | 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) { | |||
| 22 | 22 | assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_horizontal_multiplier"), 0.001) | |
| 23 | 23 | assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_vertical_multiplier"), 0.001) | |
| 24 | 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 | 29 | // WebKit-specific engine fields | |
| 27 | 30 | 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 fields | 29 | // 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 | |||
| 1002 | 1002 | Range: ">=0", | |
| 1003 | 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 | 1029 | Key: "engine.webkit.skia_cpu_painting_threads", | |
| 1007 | 1030 | 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 { | |||
| 379 | 379 | if input.ScrollMaxDelta < 0 { | |
| 380 | 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 | 389 | return validationErrors | |
| 384 | 390 | } | |
| @@ -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 validationErrors | 389 | return validationErrors | ||
| 384 | } | 390 | } | ||
M internal/infrastructure/config/validation_test.go +16 -0
| @@ -135,6 +135,22 @@ func TestValidateConfig_CEFConfig(t *testing.T) { | |||
| 135 | 135 | wantErr: true, | |
| 136 | 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 | 156 | 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 { | ||