fix(browser): add focus debouncing to prevent rapid oscillation crashes
- Add 150ms focus debouncing to SetActivePane for all sources - Prevent rapid focus changes that cause GTK segmentation faults - Preserve system events priority (bypass debouncing for SourceSystem) - Track last focus time and target to avoid redundant requests - Fixes crashes during rapid mouse movements between panes
- commit
- 1d5172ca7dccc8d212875dd4a94e28e4890e47cc
- author
- Brice · · 2025-09-27 21:47 UTC
- parent
- 0533835
- signature
- unsigned
1 file · +20 -2
M internal/app/browser/workspace_manager.go +20 -2
| @@ -50,6 +50,11 @@ type WorkspaceManager struct { | |||
| 50 | 50 | // Widget operation synchronization to prevent GTK race conditions | |
| 51 | 51 | widgetMutex sync.Mutex | |
| 52 | 52 | widgetRegistry *WidgetRegistry | |
| 53 | + | ||
| 54 | + | // Focus debouncing to prevent rapid oscillation from any source | |
| 55 | + | lastFocusTime time.Time | |
| 56 | + | lastFocusTarget *paneNode | |
| 57 | + | focusDebounce time.Duration | |
| 53 | 58 | } | |
| 54 | 59 | ||
| 55 | 60 | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | |
| @@ -64,6 +69,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 64 | 69 | lastExitMsg: make(map[*webkit.WebView]time.Time), | |
| 65 | 70 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | |
| 66 | 71 | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | |
| 72 | + | focusDebounce: 150 * time.Millisecond, // 150ms focus debouncing for all sources | |
| 67 | 73 | } | |
| 68 | 74 | ||
| 69 | 75 | // Initialize specialized managers | |
| @@ -88,7 +94,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 88 | 94 | } | |
| 89 | 95 | return manager.app.createPaneForView(view) | |
| 90 | 96 | } | |
| 91 | - | manager.ensureStackedPaneStyles() | |
| 97 | + | manager.ensureWorkspaceStyles() | |
| 92 | 98 | ||
| 93 | 99 | rootContainer := rootPane.webView.RootWidget() | |
| 94 | 100 | root := &paneNode{ | |
| @@ -338,8 +344,20 @@ func (wm *WorkspaceManager) GetActiveNode() *paneNode { | |||
| 338 | 344 | return wm.focusStateMachine.GetActivePane() | |
| 339 | 345 | } | |
| 340 | 346 | ||
| 341 | - | // SetActivePane requests focus change through the focus state machine | |
| 347 | + | // SetActivePane requests focus change through the focus state machine with debouncing | |
| 342 | 348 | func (wm *WorkspaceManager) SetActivePane(node *paneNode, source FocusSource) { | |
| 349 | + | now := time.Now() | |
| 350 | + | ||
| 351 | + | // Check if we should debounce this focus request (except for urgent system events) | |
| 352 | + | if source != SourceSystem && now.Sub(wm.lastFocusTime) < wm.focusDebounce && wm.lastFocusTarget == node { | |
| 353 | + | // Too soon and same target - ignore this request to prevent rapid oscillation | |
| 354 | + | return | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | // Update tracking and proceed with focus change | |
| 358 | + | wm.lastFocusTime = now | |
| 359 | + | wm.lastFocusTarget = node | |
| 360 | + | ||
| 343 | 361 | if err := wm.focusStateMachine.RequestFocus(node, source); err != nil { | |
| 344 | 362 | log.Printf("[workspace] Focus request failed: %v", err) | |
| 345 | 363 | } | |
| @@ -50,6 +50,11 @@ type WorkspaceManager struct { | |||||
| 50 | // Widget operation synchronization to prevent GTK race conditions | 50 | // Widget operation synchronization to prevent GTK race conditions | ||
| 51 | widgetMutex sync.Mutex | 51 | widgetMutex sync.Mutex | ||
| 52 | widgetRegistry *WidgetRegistry | 52 | widgetRegistry *WidgetRegistry | ||
| 53 | + | ||||
| 54 | + | // Focus debouncing to prevent rapid oscillation from any source | |||
| 55 | + | lastFocusTime time.Time | |||
| 56 | + | lastFocusTarget *paneNode | |||
| 57 | + | focusDebounce time.Duration | |||
| 53 | } | 58 | } | ||
| 54 | 59 | ||||
| 55 | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | 60 | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | ||
| @@ -64,6 +69,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||||
| 64 | lastExitMsg: make(map[*webkit.WebView]time.Time), | 69 | lastExitMsg: make(map[*webkit.WebView]time.Time), | ||
| 65 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | 70 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | ||
| 66 | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | 71 | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | ||
| 72 | + | focusDebounce: 150 * time.Millisecond, // 150ms focus debouncing for all sources | |||
| 67 | } | 73 | } | ||
| 68 | 74 | ||||
| 69 | // Initialize specialized managers | 75 | // Initialize specialized managers | ||
| @@ -88,7 +94,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||||
| 88 | } | 94 | } | ||
| 89 | return manager.app.createPaneForView(view) | 95 | return manager.app.createPaneForView(view) | ||
| 90 | } | 96 | } | ||
| 91 | - | manager.ensureStackedPaneStyles() | 97 | + | manager.ensureWorkspaceStyles() |
| 92 | 98 | ||||
| 93 | rootContainer := rootPane.webView.RootWidget() | 99 | rootContainer := rootPane.webView.RootWidget() | ||
| 94 | root := &paneNode{ | 100 | root := &paneNode{ | ||
| @@ -338,8 +344,20 @@ func (wm *WorkspaceManager) GetActiveNode() *paneNode { | |||||
| 338 | return wm.focusStateMachine.GetActivePane() | 344 | return wm.focusStateMachine.GetActivePane() | ||
| 339 | } | 345 | } | ||
| 340 | 346 | ||||
| 341 | - | // SetActivePane requests focus change through the focus state machine | 347 | + | // SetActivePane requests focus change through the focus state machine with debouncing |
| 342 | func (wm *WorkspaceManager) SetActivePane(node *paneNode, source FocusSource) { | 348 | func (wm *WorkspaceManager) SetActivePane(node *paneNode, source FocusSource) { | ||
| 349 | + | now := time.Now() | |||
| 350 | + | ||||
| 351 | + | // Check if we should debounce this focus request (except for urgent system events) | |||
| 352 | + | if source != SourceSystem && now.Sub(wm.lastFocusTime) < wm.focusDebounce && wm.lastFocusTarget == node { | |||
| 353 | + | // Too soon and same target - ignore this request to prevent rapid oscillation | |||
| 354 | + | return | |||
| 355 | + | } | |||
| 356 | + | ||||
| 357 | + | // Update tracking and proceed with focus change | |||
| 358 | + | wm.lastFocusTime = now | |||
| 359 | + | wm.lastFocusTarget = node | |||
| 360 | + | ||||
| 343 | if err := wm.focusStateMachine.RequestFocus(node, source); err != nil { | 361 | if err := wm.focusStateMachine.RequestFocus(node, source); err != nil { | ||
| 344 | log.Printf("[workspace] Focus request failed: %v", err) | 362 | log.Printf("[workspace] Focus request failed: %v", err) | ||
| 345 | } | 363 | } | ||