Merge pull request #5 from bnema/dev
fix: navigation gestures, WebView cleanup, and CLI help
8 files · +179 -32
M CHANGELOG.md +5 -0
| @@ -16,8 +16,13 @@ All notable changes to this project will be documented in this file. | |||
| 16 | 16 | - Simplified omnibox favicon handling: uses database favicon_url only | |
| 17 | 17 | ||
| 18 | 18 | ### Fixed | |
| 19 | + | - Mouse button navigation: Added support for hardware mouse buttons 8/9 (back/forward) | |
| 20 | + | - Touchpad gestures: Two-finger horizontal swipe for back/forward navigation | |
| 21 | + | - Web app shortcuts: Ctrl+C, Ctrl+V, Ctrl+A now work in Gmail, VSCode web, etc. | |
| 19 | 22 | - Dark mode flash on startup: Set GTK dark theme preference and WebView background color before window/content creation | |
| 20 | 23 | - Cmd+Q and window close button now trigger graceful shutdown instead of killing the browser process | |
| 24 | + | - WebView cleanup: Properly destroy WebViews to prevent memory leaks and duplicate handler registrations when closing stacked panes | |
| 25 | + | - CLI help: Running `dumber` without arguments now shows available commands (use `--gui` or `browse` to launch GUI) | |
| 21 | 26 | ||
| 22 | 27 | ## [0.12.0] - 2025-10-16 | |
| 23 | 28 | ||
| @@ -16,8 +16,13 @@ All notable changes to this project will be documented in this file. | |||||
| 16 | - Simplified omnibox favicon handling: uses database favicon_url only | 16 | - Simplified omnibox favicon handling: uses database favicon_url only | ||
| 17 | 17 | ||||
| 18 | ### Fixed | 18 | ### Fixed | ||
| 19 | + | - Mouse button navigation: Added support for hardware mouse buttons 8/9 (back/forward) | |||
| 20 | + | - Touchpad gestures: Two-finger horizontal swipe for back/forward navigation | |||
| 21 | + | - Web app shortcuts: Ctrl+C, Ctrl+V, Ctrl+A now work in Gmail, VSCode web, etc. | |||
| 19 | - Dark mode flash on startup: Set GTK dark theme preference and WebView background color before window/content creation | 22 | - Dark mode flash on startup: Set GTK dark theme preference and WebView background color before window/content creation | ||
| 20 | - Cmd+Q and window close button now trigger graceful shutdown instead of killing the browser process | 23 | - Cmd+Q and window close button now trigger graceful shutdown instead of killing the browser process | ||
| 24 | + | - WebView cleanup: Properly destroy WebViews to prevent memory leaks and duplicate handler registrations when closing stacked panes | |||
| 25 | + | - CLI help: Running `dumber` without arguments now shows available commands (use `--gui` or `browse` to launch GUI) | |||
| 21 | 26 | ||||
| 22 | ## [0.12.0] - 2025-10-16 | 27 | ## [0.12.0] - 2025-10-16 | ||
| 23 | 28 | ||||
M README.md +2 -26
| @@ -172,16 +172,6 @@ Dumber features a Zellij-inspired pane management system that allows you to spli | |||
| 172 | 172 | 3. **Navigate**: Use `Alt+Arrow Keys` to move focus between split panes, or `Alt+Up/Down` to navigate within stacked panes | |
| 173 | 173 | 4. **Close**: Press `X` in pane mode to close the current pane | |
| 174 | 174 | ||
| 175 | - | ### Features | |
| 176 | - | - **Binary tree layout**: Panes are organized in a binary tree structure for optimal space usage | |
| 177 | - | - **Stacked panes**: Zellij-style stacking with collapsed title bars showing "Page Title - domain" for inactive panes | |
| 178 | - | - **Focus management**: Visual borders, hover-to-focus, and enhanced focus throttling (100ms) for intuitive navigation | |
| 179 | - | - **Independent sessions**: Each pane maintains its own browsing session, history, and zoom level | |
| 180 | - | - **Advanced popup handling**: Intelligent popup vs tab detection, OAuth auto-close, and popup deduplication with SHA256 fingerprinting | |
| 181 | - | - **Cross-WebView communication**: localStorage-based parent-popup bridge enabling proper `window.opener` functionality | |
| 182 | - | - **WebView identity tracking**: Unique WebView ID system for precise popup targeting and enhanced debugging | |
| 183 | - | - **Keyboard-driven**: All operations accessible via keyboard shortcuts with modal pane mode | |
| 184 | - | ||
| 185 | 175 | ### Configuration | |
| 186 | 176 | The pane system is fully configurable via `config.json`: | |
| 187 | 177 | ```json | |
| @@ -373,22 +363,8 @@ WebKitGTK uses GStreamer for media playback. Dumber includes automatic hardware | |||
| 373 | 363 | - `sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-pipewire` | |
| 374 | 364 | - Hardware accel: `gstreamer1.0-vaapi va-driver-all` (covers most GPUs) | |
| 375 | 365 | ||
| 376 | - | ## Theme and Runtime Updates | |
| 377 | - | - The homepage and pages respect your system’s light/dark preference. | |
| 378 | - | - The app updates color‑scheme live when your GTK theme changes (no relaunch needed). | |
| 379 | - | ||
| 380 | - | ## Project Structure | |
| 381 | - | - `main.go`: Entry point with CLI/GUI routing | |
| 382 | - | - `internal/app/cli`: CLI application logic and Cobra commands | |
| 383 | - | - `internal/app/browser`: Browser application logic and WebKit integration | |
| 384 | - | - `internal/cli`: Cobra commands (`browse`, `dmenu`, `version`) | |
| 385 | - | - `internal/config`: Viper‑backed config, XDG helpers, defaults | |
| 386 | - | - `internal/db`: SQLite schema init and sqlc‑generated queries | |
| 387 | - | - `internal/logging`: Structured logging system with file rotation | |
| 388 | - | - `services`: Application services (parser/history/config/browser) | |
| 389 | - | - `pkg/webkit`: WebKit2GTK bindings (CGO and stub implementations) | |
| 390 | - | - `pkg/clipboard`: Cross-platform clipboard operations | |
| 391 | - | - `frontend`: TypeScript homepage UI bundled into the binary | |
| 366 | + | ## Contribute | |
| 367 | + | I will gladly accept contributions, especially related to UI/UX enhancements. Feel free to open an issue and let's discuss it! | |
| 392 | 368 | ||
| 393 | 369 | ## Security & Privacy | |
| 394 | 370 | - History and zoom settings are stored locally in SQLite under XDG state. | |
| @@ -172,16 +172,6 @@ Dumber features a Zellij-inspired pane management system that allows you to spli | |||||
| 172 | 3. **Navigate**: Use `Alt+Arrow Keys` to move focus between split panes, or `Alt+Up/Down` to navigate within stacked panes | 172 | 3. **Navigate**: Use `Alt+Arrow Keys` to move focus between split panes, or `Alt+Up/Down` to navigate within stacked panes | ||
| 173 | 4. **Close**: Press `X` in pane mode to close the current pane | 173 | 4. **Close**: Press `X` in pane mode to close the current pane | ||
| 174 | 174 | ||||
| 175 | - | ### Features | |||
| 176 | - | - **Binary tree layout**: Panes are organized in a binary tree structure for optimal space usage | |||
| 177 | - | - **Stacked panes**: Zellij-style stacking with collapsed title bars showing "Page Title - domain" for inactive panes | |||
| 178 | - | - **Focus management**: Visual borders, hover-to-focus, and enhanced focus throttling (100ms) for intuitive navigation | |||
| 179 | - | - **Independent sessions**: Each pane maintains its own browsing session, history, and zoom level | |||
| 180 | - | - **Advanced popup handling**: Intelligent popup vs tab detection, OAuth auto-close, and popup deduplication with SHA256 fingerprinting | |||
| 181 | - | - **Cross-WebView communication**: localStorage-based parent-popup bridge enabling proper `window.opener` functionality | |||
| 182 | - | - **WebView identity tracking**: Unique WebView ID system for precise popup targeting and enhanced debugging | |||
| 183 | - | - **Keyboard-driven**: All operations accessible via keyboard shortcuts with modal pane mode | |||
| 184 | - | ||||
| 185 | ### Configuration | 175 | ### Configuration | ||
| 186 | The pane system is fully configurable via `config.json`: | 176 | The pane system is fully configurable via `config.json`: | ||
| 187 | ```json | 177 | ```json | ||
| @@ -373,22 +363,8 @@ WebKitGTK uses GStreamer for media playback. Dumber includes automatic hardware | |||||
| 373 | - `sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-pipewire` | 363 | - `sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-pipewire` | ||
| 374 | - Hardware accel: `gstreamer1.0-vaapi va-driver-all` (covers most GPUs) | 364 | - Hardware accel: `gstreamer1.0-vaapi va-driver-all` (covers most GPUs) | ||
| 375 | 365 | ||||
| 376 | - | ## Theme and Runtime Updates | 366 | + | ## Contribute |
| 377 | - | - The homepage and pages respect your system’s light/dark preference. | 367 | + | I will gladly accept contributions, especially related to UI/UX enhancements. Feel free to open an issue and let's discuss it! |
| 378 | - | - The app updates color‑scheme live when your GTK theme changes (no relaunch needed). | |||
| 379 | - | ||||
| 380 | - | ## Project Structure | |||
| 381 | - | - `main.go`: Entry point with CLI/GUI routing | |||
| 382 | - | - `internal/app/cli`: CLI application logic and Cobra commands | |||
| 383 | - | - `internal/app/browser`: Browser application logic and WebKit integration | |||
| 384 | - | - `internal/cli`: Cobra commands (`browse`, `dmenu`, `version`) | |||
| 385 | - | - `internal/config`: Viper‑backed config, XDG helpers, defaults | |||
| 386 | - | - `internal/db`: SQLite schema init and sqlc‑generated queries | |||
| 387 | - | - `internal/logging`: Structured logging system with file rotation | |||
| 388 | - | - `services`: Application services (parser/history/config/browser) | |||
| 389 | - | - `pkg/webkit`: WebKit2GTK bindings (CGO and stub implementations) | |||
| 390 | - | - `pkg/clipboard`: Cross-platform clipboard operations | |||
| 391 | - | - `frontend`: TypeScript homepage UI bundled into the binary | |||
| 392 | 368 | ||||
| 393 | ## Security & Privacy | 369 | ## Security & Privacy | ||
| 394 | - History and zoom settings are stored locally in SQLite under XDG state. | 370 | - History and zoom settings are stored locally in SQLite under XDG state. | ||
M internal/app/browser/pane.go +9 -0
| @@ -121,6 +121,15 @@ func (p *BrowserPane) Cleanup() { | |||
| 121 | 121 | } | |
| 122 | 122 | } | |
| 123 | 123 | ||
| 124 | + | // Destroy WebView to unregister from global registry and release resources | |
| 125 | + | if p.webView != nil && !p.webView.IsDestroyed() { | |
| 126 | + | if err := p.webView.Destroy(); err != nil { | |
| 127 | + | log.Printf("[pane-%s] failed to destroy webview: %v", p.id, err) | |
| 128 | + | } else { | |
| 129 | + | log.Printf("[pane-%s] successfully destroyed webview", p.id) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | ||
| 124 | 133 | // Clear tracking | |
| 125 | 134 | p.guiComponents = nil | |
| 126 | 135 | p.hasGUI = false | |
| @@ -121,6 +121,15 @@ func (p *BrowserPane) Cleanup() { | |||||
| 121 | } | 121 | } | ||
| 122 | } | 122 | } | ||
| 123 | 123 | ||||
| 124 | + | // Destroy WebView to unregister from global registry and release resources | |||
| 125 | + | if p.webView != nil && !p.webView.IsDestroyed() { | |||
| 126 | + | if err := p.webView.Destroy(); err != nil { | |||
| 127 | + | log.Printf("[pane-%s] failed to destroy webview: %v", p.id, err) | |||
| 128 | + | } else { | |||
| 129 | + | log.Printf("[pane-%s] successfully destroyed webview", p.id) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | ||||
| 124 | // Clear tracking | 133 | // Clear tracking | ||
| 125 | p.guiComponents = nil | 134 | p.guiComponents = nil | ||
| 126 | p.hasGUI = false | 135 | p.hasGUI = false | ||
M main.go +2 -2
| @@ -62,9 +62,9 @@ func main() { | |||
| 62 | 62 | ||
| 63 | 63 | // shouldRunCLI determines if we should run in CLI mode based on arguments | |
| 64 | 64 | func shouldRunCLI() bool { | |
| 65 | - | // No args = GUI landing page mode | |
| 65 | + | // No args = show CLI help with available commands | |
| 66 | 66 | if len(os.Args) <= 1 { | |
| 67 | - | return false | |
| 67 | + | return true | |
| 68 | 68 | } | |
| 69 | 69 | ||
| 70 | 70 | // Check for GUI-specific flags | |
| @@ -62,9 +62,9 @@ func main() { | |||||
| 62 | 62 | ||||
| 63 | // shouldRunCLI determines if we should run in CLI mode based on arguments | 63 | // shouldRunCLI determines if we should run in CLI mode based on arguments | ||
| 64 | func shouldRunCLI() bool { | 64 | func shouldRunCLI() bool { | ||
| 65 | - | // No args = GUI landing page mode | 65 | + | // No args = show CLI help with available commands |
| 66 | if len(os.Args) <= 1 { | 66 | if len(os.Args) <= 1 { | ||
| 67 | - | return false | 67 | + | return true |
| 68 | } | 68 | } | ||
| 69 | 69 | ||||
| 70 | // Check for GUI-specific flags | 70 | // Check for GUI-specific flags | ||
M pkg/webkit/keyboard_bridge.go +2 -4
| @@ -180,10 +180,8 @@ func (w *WebView) AttachKeyboardBridge() { | |||
| 180 | 180 | case '0': | |
| 181 | 181 | keyName = keyZero | |
| 182 | 182 | default: | |
| 183 | - | // Only forward shortcuts with modifiers | |
| 184 | - | if !ctrl && !alt && !shift { | |
| 185 | - | return false | |
| 186 | - | } | |
| 183 | + | // For unknown keys, let WebView handle them | |
| 184 | + | // This allows web apps to receive Ctrl+C, Ctrl+V, Ctrl+A, etc. | |
| 187 | 185 | return false | |
| 188 | 186 | } | |
| 189 | 187 | ||
| @@ -180,10 +180,8 @@ func (w *WebView) AttachKeyboardBridge() { | |||||
| 180 | case '0': | 180 | case '0': | ||
| 181 | keyName = keyZero | 181 | keyName = keyZero | ||
| 182 | default: | 182 | default: | ||
| 183 | - | // Only forward shortcuts with modifiers | 183 | + | // For unknown keys, let WebView handle them |
| 184 | - | if !ctrl && !alt && !shift { | 184 | + | // This allows web apps to receive Ctrl+C, Ctrl+V, Ctrl+A, etc. |
| 185 | - | return false | |||
| 186 | - | } | |||
| 187 | return false | 185 | return false | ||
| 188 | } | 186 | } | ||
| 189 | 187 | ||||
A pkg/webkit/mouse_gestures.go +71 -0
| @@ -0,0 +1,71 @@ | |||
| 1 | + | package webkit | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "log" | |
| 5 | + | ||
| 6 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Mouse button constants for navigation | |
| 10 | + | // Standard button numbers (consistent across Wayland and GTK) | |
| 11 | + | const ( | |
| 12 | + | mouseButtonBack = 8 // Side button (back) | |
| 13 | + | mouseButtonForward = 9 // Side button (forward) | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // AttachMouseGestures attaches mouse button gesture handling to the WebView | |
| 17 | + | // This enables hardware mouse button navigation (back/forward buttons) | |
| 18 | + | func (w *WebView) AttachMouseGestures() { | |
| 19 | + | if w.view == nil { | |
| 20 | + | log.Printf("[mouse-gestures] Cannot attach: WebView is nil") | |
| 21 | + | return | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Create gesture controller for mouse clicks | |
| 25 | + | gestureClick := gtk.NewGestureClick() | |
| 26 | + | ||
| 27 | + | // Listen to all mouse buttons (0 = any button) | |
| 28 | + | gestureClick.SetButton(0) | |
| 29 | + | ||
| 30 | + | // Set to capture phase to intercept before WebView processes | |
| 31 | + | gestureClick.SetPropagationPhase(gtk.PhaseCapture) | |
| 32 | + | ||
| 33 | + | // Connect to pressed signal | |
| 34 | + | gestureClick.ConnectPressed(func(nPress int, x, y float64) { | |
| 35 | + | // Get which button was pressed | |
| 36 | + | button := gestureClick.CurrentButton() | |
| 37 | + | ||
| 38 | + | // Only handle single clicks (not double/triple clicks) | |
| 39 | + | if nPress != 1 { | |
| 40 | + | return | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | switch button { | |
| 44 | + | case mouseButtonBack: | |
| 45 | + | // Mouse back button - navigate backward | |
| 46 | + | if w.view.CanGoBack() { | |
| 47 | + | log.Printf("[mouse-gestures] Back button clicked - navigating backward") | |
| 48 | + | w.view.GoBack() | |
| 49 | + | } else { | |
| 50 | + | log.Printf("[mouse-gestures] Back button clicked but cannot go back") | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | case mouseButtonForward: | |
| 54 | + | // Mouse forward button - navigate forward | |
| 55 | + | if w.view.CanGoForward() { | |
| 56 | + | log.Printf("[mouse-gestures] Forward button clicked - navigating forward") | |
| 57 | + | w.view.GoForward() | |
| 58 | + | } else { | |
| 59 | + | log.Printf("[mouse-gestures] Forward button clicked but cannot go forward") | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | default: | |
| 63 | + | // Don't handle other buttons (let WebView handle normally) | |
| 64 | + | return | |
| 65 | + | } | |
| 66 | + | }) | |
| 67 | + | ||
| 68 | + | // Attach controller to WebView | |
| 69 | + | w.view.AddController(gestureClick) | |
| 70 | + | log.Printf("[mouse-gestures] Mouse gesture controller attached to WebView ID %d", w.id) | |
| 71 | + | } | |
| @@ -0,0 +1,71 @@ | |||||
| 1 | + | package webkit | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "log" | |||
| 5 | + | ||||
| 6 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Mouse button constants for navigation | |||
| 10 | + | // Standard button numbers (consistent across Wayland and GTK) | |||
| 11 | + | const ( | |||
| 12 | + | mouseButtonBack = 8 // Side button (back) | |||
| 13 | + | mouseButtonForward = 9 // Side button (forward) | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // AttachMouseGestures attaches mouse button gesture handling to the WebView | |||
| 17 | + | // This enables hardware mouse button navigation (back/forward buttons) | |||
| 18 | + | func (w *WebView) AttachMouseGestures() { | |||
| 19 | + | if w.view == nil { | |||
| 20 | + | log.Printf("[mouse-gestures] Cannot attach: WebView is nil") | |||
| 21 | + | return | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // Create gesture controller for mouse clicks | |||
| 25 | + | gestureClick := gtk.NewGestureClick() | |||
| 26 | + | ||||
| 27 | + | // Listen to all mouse buttons (0 = any button) | |||
| 28 | + | gestureClick.SetButton(0) | |||
| 29 | + | ||||
| 30 | + | // Set to capture phase to intercept before WebView processes | |||
| 31 | + | gestureClick.SetPropagationPhase(gtk.PhaseCapture) | |||
| 32 | + | ||||
| 33 | + | // Connect to pressed signal | |||
| 34 | + | gestureClick.ConnectPressed(func(nPress int, x, y float64) { | |||
| 35 | + | // Get which button was pressed | |||
| 36 | + | button := gestureClick.CurrentButton() | |||
| 37 | + | ||||
| 38 | + | // Only handle single clicks (not double/triple clicks) | |||
| 39 | + | if nPress != 1 { | |||
| 40 | + | return | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | switch button { | |||
| 44 | + | case mouseButtonBack: | |||
| 45 | + | // Mouse back button - navigate backward | |||
| 46 | + | if w.view.CanGoBack() { | |||
| 47 | + | log.Printf("[mouse-gestures] Back button clicked - navigating backward") | |||
| 48 | + | w.view.GoBack() | |||
| 49 | + | } else { | |||
| 50 | + | log.Printf("[mouse-gestures] Back button clicked but cannot go back") | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | case mouseButtonForward: | |||
| 54 | + | // Mouse forward button - navigate forward | |||
| 55 | + | if w.view.CanGoForward() { | |||
| 56 | + | log.Printf("[mouse-gestures] Forward button clicked - navigating forward") | |||
| 57 | + | w.view.GoForward() | |||
| 58 | + | } else { | |||
| 59 | + | log.Printf("[mouse-gestures] Forward button clicked but cannot go forward") | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | default: | |||
| 63 | + | // Don't handle other buttons (let WebView handle normally) | |||
| 64 | + | return | |||
| 65 | + | } | |||
| 66 | + | }) | |||
| 67 | + | ||||
| 68 | + | // Attach controller to WebView | |||
| 69 | + | w.view.AddController(gestureClick) | |||
| 70 | + | log.Printf("[mouse-gestures] Mouse gesture controller attached to WebView ID %d", w.id) | |||
| 71 | + | } | |||
A pkg/webkit/touchpad_gestures.go +76 -0
| @@ -0,0 +1,76 @@ | |||
| 1 | + | package webkit | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "log" | |
| 5 | + | "math" | |
| 6 | + | ||
| 7 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Touchpad swipe velocity threshold (pixels per second) | |
| 11 | + | // Lower values make gestures more sensitive, higher values require faster swipes | |
| 12 | + | const ( | |
| 13 | + | swipeVelocityThreshold = 500.0 // Reasonable threshold for intentional swipes | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // AttachTouchpadGestures attaches touchpad swipe gesture handling to the WebView | |
| 17 | + | // This enables two-finger swipe navigation (left/right for back/forward) | |
| 18 | + | func (w *WebView) AttachTouchpadGestures() { | |
| 19 | + | if w.view == nil { | |
| 20 | + | log.Printf("[touchpad-gestures] Cannot attach: WebView is nil") | |
| 21 | + | return | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Create gesture controller for swipe detection | |
| 25 | + | gestureSwipe := gtk.NewGestureSwipe() | |
| 26 | + | ||
| 27 | + | // Set to capture phase to intercept before WebView processes | |
| 28 | + | gestureSwipe.SetPropagationPhase(gtk.PhaseCapture) | |
| 29 | + | ||
| 30 | + | // Set to require touchpad (2 fingers minimum for touchpad gestures) | |
| 31 | + | // This prevents single-finger drags from triggering navigation | |
| 32 | + | gestureSwipe.SetTouchOnly(false) // Allow both touch and touchpad | |
| 33 | + | ||
| 34 | + | // Connect to swipe signal | |
| 35 | + | gestureSwipe.ConnectSwipe(func(velocityX, velocityY float64) { | |
| 36 | + | // Only process horizontal swipes (ignore mostly vertical swipes) | |
| 37 | + | // Use absolute values to compare magnitudes | |
| 38 | + | absVelocityX := math.Abs(velocityX) | |
| 39 | + | absVelocityY := math.Abs(velocityY) | |
| 40 | + | ||
| 41 | + | // Require horizontal velocity to be dominant | |
| 42 | + | if absVelocityY > absVelocityX { | |
| 43 | + | log.Printf("[touchpad-gestures] Swipe is more vertical (X:%.1f Y:%.1f), ignoring", velocityX, velocityY) | |
| 44 | + | return | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Check if swipe velocity meets threshold | |
| 48 | + | if absVelocityX < swipeVelocityThreshold { | |
| 49 | + | log.Printf("[touchpad-gestures] Swipe velocity too low (%.1f < %.1f), ignoring", absVelocityX, swipeVelocityThreshold) | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // Determine swipe direction and navigate | |
| 54 | + | if velocityX > 0 { | |
| 55 | + | // Swipe right (positive velocity) - go back | |
| 56 | + | if w.view.CanGoBack() { | |
| 57 | + | log.Printf("[touchpad-gestures] Swipe right detected (velocity: %.1f) - navigating backward", velocityX) | |
| 58 | + | w.view.GoBack() | |
| 59 | + | } else { | |
| 60 | + | log.Printf("[touchpad-gestures] Swipe right detected but cannot go back") | |
| 61 | + | } | |
| 62 | + | } else { | |
| 63 | + | // Swipe left (negative velocity) - go forward | |
| 64 | + | if w.view.CanGoForward() { | |
| 65 | + | log.Printf("[touchpad-gestures] Swipe left detected (velocity: %.1f) - navigating forward", velocityX) | |
| 66 | + | w.view.GoForward() | |
| 67 | + | } else { | |
| 68 | + | log.Printf("[touchpad-gestures] Swipe left detected but cannot go forward") | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | }) | |
| 72 | + | ||
| 73 | + | // Attach controller to WebView | |
| 74 | + | w.view.AddController(gestureSwipe) | |
| 75 | + | log.Printf("[touchpad-gestures] Touchpad gesture controller attached to WebView ID %d", w.id) | |
| 76 | + | } | |
| @@ -0,0 +1,76 @@ | |||||
| 1 | + | package webkit | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "log" | |||
| 5 | + | "math" | |||
| 6 | + | ||||
| 7 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Touchpad swipe velocity threshold (pixels per second) | |||
| 11 | + | // Lower values make gestures more sensitive, higher values require faster swipes | |||
| 12 | + | const ( | |||
| 13 | + | swipeVelocityThreshold = 500.0 // Reasonable threshold for intentional swipes | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // AttachTouchpadGestures attaches touchpad swipe gesture handling to the WebView | |||
| 17 | + | // This enables two-finger swipe navigation (left/right for back/forward) | |||
| 18 | + | func (w *WebView) AttachTouchpadGestures() { | |||
| 19 | + | if w.view == nil { | |||
| 20 | + | log.Printf("[touchpad-gestures] Cannot attach: WebView is nil") | |||
| 21 | + | return | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // Create gesture controller for swipe detection | |||
| 25 | + | gestureSwipe := gtk.NewGestureSwipe() | |||
| 26 | + | ||||
| 27 | + | // Set to capture phase to intercept before WebView processes | |||
| 28 | + | gestureSwipe.SetPropagationPhase(gtk.PhaseCapture) | |||
| 29 | + | ||||
| 30 | + | // Set to require touchpad (2 fingers minimum for touchpad gestures) | |||
| 31 | + | // This prevents single-finger drags from triggering navigation | |||
| 32 | + | gestureSwipe.SetTouchOnly(false) // Allow both touch and touchpad | |||
| 33 | + | ||||
| 34 | + | // Connect to swipe signal | |||
| 35 | + | gestureSwipe.ConnectSwipe(func(velocityX, velocityY float64) { | |||
| 36 | + | // Only process horizontal swipes (ignore mostly vertical swipes) | |||
| 37 | + | // Use absolute values to compare magnitudes | |||
| 38 | + | absVelocityX := math.Abs(velocityX) | |||
| 39 | + | absVelocityY := math.Abs(velocityY) | |||
| 40 | + | ||||
| 41 | + | // Require horizontal velocity to be dominant | |||
| 42 | + | if absVelocityY > absVelocityX { | |||
| 43 | + | log.Printf("[touchpad-gestures] Swipe is more vertical (X:%.1f Y:%.1f), ignoring", velocityX, velocityY) | |||
| 44 | + | return | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // Check if swipe velocity meets threshold | |||
| 48 | + | if absVelocityX < swipeVelocityThreshold { | |||
| 49 | + | log.Printf("[touchpad-gestures] Swipe velocity too low (%.1f < %.1f), ignoring", absVelocityX, swipeVelocityThreshold) | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // Determine swipe direction and navigate | |||
| 54 | + | if velocityX > 0 { | |||
| 55 | + | // Swipe right (positive velocity) - go back | |||
| 56 | + | if w.view.CanGoBack() { | |||
| 57 | + | log.Printf("[touchpad-gestures] Swipe right detected (velocity: %.1f) - navigating backward", velocityX) | |||
| 58 | + | w.view.GoBack() | |||
| 59 | + | } else { | |||
| 60 | + | log.Printf("[touchpad-gestures] Swipe right detected but cannot go back") | |||
| 61 | + | } | |||
| 62 | + | } else { | |||
| 63 | + | // Swipe left (negative velocity) - go forward | |||
| 64 | + | if w.view.CanGoForward() { | |||
| 65 | + | log.Printf("[touchpad-gestures] Swipe left detected (velocity: %.1f) - navigating forward", velocityX) | |||
| 66 | + | w.view.GoForward() | |||
| 67 | + | } else { | |||
| 68 | + | log.Printf("[touchpad-gestures] Swipe left detected but cannot go forward") | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | }) | |||
| 72 | + | ||||
| 73 | + | // Attach controller to WebView | |||
| 74 | + | w.view.AddController(gestureSwipe) | |||
| 75 | + | log.Printf("[touchpad-gestures] Touchpad gesture controller attached to WebView ID %d", w.id) | |||
| 76 | + | } | |||
M pkg/webkit/webview.go +12 -0
| @@ -145,6 +145,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 145 | 145 | // Attach keyboard bridge to forward keyboard events to JavaScript | |
| 146 | 146 | wv.AttachKeyboardBridge() | |
| 147 | 147 | ||
| 148 | + | // Attach mouse gesture controls for hardware navigation buttons | |
| 149 | + | wv.AttachMouseGestures() | |
| 150 | + | ||
| 151 | + | // Attach touchpad gesture controls for swipe navigation | |
| 152 | + | wv.AttachTouchpadGestures() | |
| 153 | + | ||
| 148 | 154 | // Only create window if requested (standalone WebViews) | |
| 149 | 155 | // Workspace panes will set CreateWindow=false and manage the container themselves | |
| 150 | 156 | if cfg.CreateWindow { | |
| @@ -922,6 +928,12 @@ func (w *WebView) InitializeFromBare(cfg *Config) error { | |||
| 922 | 928 | // Attach keyboard bridge | |
| 923 | 929 | w.AttachKeyboardBridge() | |
| 924 | 930 | ||
| 931 | + | // Attach mouse gesture controls | |
| 932 | + | w.AttachMouseGestures() | |
| 933 | + | ||
| 934 | + | // Attach touchpad gesture controls | |
| 935 | + | w.AttachTouchpadGestures() | |
| 936 | + | ||
| 925 | 937 | log.Printf("[webkit] Completed initialization for WebView ID %d", w.id) | |
| 926 | 938 | ||
| 927 | 939 | return nil | |
| @@ -145,6 +145,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 145 | // Attach keyboard bridge to forward keyboard events to JavaScript | 145 | // Attach keyboard bridge to forward keyboard events to JavaScript | ||
| 146 | wv.AttachKeyboardBridge() | 146 | wv.AttachKeyboardBridge() | ||
| 147 | 147 | ||||
| 148 | + | // Attach mouse gesture controls for hardware navigation buttons | |||
| 149 | + | wv.AttachMouseGestures() | |||
| 150 | + | ||||
| 151 | + | // Attach touchpad gesture controls for swipe navigation | |||
| 152 | + | wv.AttachTouchpadGestures() | |||
| 153 | + | ||||
| 148 | // Only create window if requested (standalone WebViews) | 154 | // Only create window if requested (standalone WebViews) | ||
| 149 | // Workspace panes will set CreateWindow=false and manage the container themselves | 155 | // Workspace panes will set CreateWindow=false and manage the container themselves | ||
| 150 | if cfg.CreateWindow { | 156 | if cfg.CreateWindow { | ||
| @@ -922,6 +928,12 @@ func (w *WebView) InitializeFromBare(cfg *Config) error { | |||||
| 922 | // Attach keyboard bridge | 928 | // Attach keyboard bridge | ||
| 923 | w.AttachKeyboardBridge() | 929 | w.AttachKeyboardBridge() | ||
| 924 | 930 | ||||
| 931 | + | // Attach mouse gesture controls | |||
| 932 | + | w.AttachMouseGestures() | |||
| 933 | + | ||||
| 934 | + | // Attach touchpad gesture controls | |||
| 935 | + | w.AttachTouchpadGestures() | |||
| 936 | + | ||||
| 925 | log.Printf("[webkit] Completed initialization for WebView ID %d", w.id) | 937 | log.Printf("[webkit] Completed initialization for WebView ID %d", w.id) | ||
| 926 | 938 | ||||
| 927 | return nil | 939 | return nil | ||