fix(ui): fix omnibox pane switching, Ctrl+L handling, and overlay dimensions
- commit
- d0dbbe7226e6fcbd581c3753644fdf52a9e01afb
- author
- bnema · · 2025-12-22 21:06 UTC
- parent
- 4b644f1
- signature
- unsigned
7 files · +78 -23
M dev/check-docs.go +3 -0
| @@ -1,3 +1,6 @@ | |||
| 1 | + | //go:build devtools | |
| 2 | + | // +build devtools | |
| 3 | + | ||
| 1 | 4 | package main | |
| 2 | 5 | ||
| 3 | 6 | import ( | |
| @@ -1,3 +1,6 @@ | |||||
| 1 | + | //go:build devtools | |||
| 2 | + | // +build devtools | |||
| 3 | + | ||||
| 1 | package main | 4 | package main | ||
| 2 | 5 | ||||
| 3 | import ( | 6 | import ( | ||
M go.mod +4 -7
| @@ -3,12 +3,12 @@ module github.com/bnema/dumber | |||
| 3 | 3 | go 1.25.3 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | - | github.com/bnema/puregotk-webkit v0.0.0-00010101000000-000000000000 | |
| 6 | + | github.com/bnema/puregotk-webkit v0.0.7 | |
| 7 | 7 | github.com/charmbracelet/bubbles v0.21.0 | |
| 8 | 8 | github.com/charmbracelet/bubbletea v1.3.10 | |
| 9 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | |
| 10 | 10 | github.com/fsnotify/fsnotify v1.9.0 | |
| 11 | - | github.com/jwijenbergh/puregotk v0.0.0-20251022075221-eae1610c7d83 | |
| 11 | + | github.com/jwijenbergh/puregotk v0.0.0-20251222174431-73930527faa3 | |
| 12 | 12 | github.com/ncruces/go-sqlite3 v0.30.3 | |
| 13 | 13 | github.com/pressly/goose/v3 v3.26.0 | |
| 14 | 14 | github.com/rs/zerolog v1.33.0 | |
| @@ -17,6 +17,8 @@ require ( | |||
| 17 | 17 | github.com/stretchr/testify v1.11.1 | |
| 18 | 18 | ) | |
| 19 | 19 | ||
| 20 | + | replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20251222174431-73930527faa3 | |
| 21 | + | ||
| 20 | 22 | require ( | |
| 21 | 23 | github.com/atotto/clipboard v0.1.4 // indirect | |
| 22 | 24 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | |
| @@ -60,8 +62,3 @@ require ( | |||
| 60 | 62 | golang.org/x/text v0.31.0 // indirect | |
| 61 | 63 | gopkg.in/yaml.v3 v3.0.1 // indirect | |
| 62 | 64 | ) | |
| 63 | - | ||
| 64 | - | replace ( | |
| 65 | - | github.com/bnema/puregotk-webkit => ../puregotk-webkit | |
| 66 | - | github.com/jwijenbergh/puregotk => ../puregotk | |
| 67 | - | ) | |
| @@ -3,12 +3,12 @@ module github.com/bnema/dumber | |||||
| 3 | go 1.25.3 | 3 | go 1.25.3 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | - | github.com/bnema/puregotk-webkit v0.0.0-00010101000000-000000000000 | 6 | + | github.com/bnema/puregotk-webkit v0.0.7 |
| 7 | github.com/charmbracelet/bubbles v0.21.0 | 7 | github.com/charmbracelet/bubbles v0.21.0 | ||
| 8 | github.com/charmbracelet/bubbletea v1.3.10 | 8 | github.com/charmbracelet/bubbletea v1.3.10 | ||
| 9 | github.com/charmbracelet/lipgloss v1.1.0 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | ||
| 10 | github.com/fsnotify/fsnotify v1.9.0 | 10 | github.com/fsnotify/fsnotify v1.9.0 | ||
| 11 | - | github.com/jwijenbergh/puregotk v0.0.0-20251022075221-eae1610c7d83 | 11 | + | github.com/jwijenbergh/puregotk v0.0.0-20251222174431-73930527faa3 |
| 12 | github.com/ncruces/go-sqlite3 v0.30.3 | 12 | github.com/ncruces/go-sqlite3 v0.30.3 | ||
| 13 | github.com/pressly/goose/v3 v3.26.0 | 13 | github.com/pressly/goose/v3 v3.26.0 | ||
| 14 | github.com/rs/zerolog v1.33.0 | 14 | github.com/rs/zerolog v1.33.0 | ||
| @@ -17,6 +17,8 @@ require ( | |||||
| 17 | github.com/stretchr/testify v1.11.1 | 17 | github.com/stretchr/testify v1.11.1 | ||
| 18 | ) | 18 | ) | ||
| 19 | 19 | ||||
| 20 | + | replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20251222174431-73930527faa3 | |||
| 21 | + | ||||
| 20 | require ( | 22 | require ( | ||
| 21 | github.com/atotto/clipboard v0.1.4 // indirect | 23 | github.com/atotto/clipboard v0.1.4 // indirect | ||
| 22 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | 24 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | ||
| @@ -60,8 +62,3 @@ require ( | |||||
| 60 | golang.org/x/text v0.31.0 // indirect | 62 | golang.org/x/text v0.31.0 // indirect | ||
| 61 | gopkg.in/yaml.v3 v3.0.1 // indirect | 63 | gopkg.in/yaml.v3 v3.0.1 // indirect | ||
| 62 | ) | 64 | ) | ||
| 63 | - | ||||
| 64 | - | replace ( | |||
| 65 | - | github.com/bnema/puregotk-webkit => ../puregotk-webkit | |||
| 66 | - | github.com/jwijenbergh/puregotk => ../puregotk | |||
| 67 | - | ) | |||
M go.sum +4 -0 generated file: collapsed
M internal/ui/component/omnibox.go +40 -5
| @@ -107,6 +107,12 @@ type Omnibox struct { | |||
| 107 | 107 | singleRow int // single ListBoxRow natural height | |
| 108 | 108 | valid bool // whether cache is valid | |
| 109 | 109 | } | |
| 110 | + | ||
| 111 | + | // Callback retention: keep GTK signal callbacks reachable by Go GC. | |
| 112 | + | retainedCallbacks []interface{} | |
| 113 | + | ||
| 114 | + | // Used to rate-limit allocation-related logs. | |
| 115 | + | loggedInvalidOverlayDims bool | |
| 110 | 116 | } | |
| 111 | 117 | ||
| 112 | 118 | // OmniboxConfig holds configuration for creating an Omnibox. | |
| @@ -194,11 +200,27 @@ func (o *Omnibox) resizeAndCenter(rowCount int) { | |||
| 194 | 200 | parentHeight := o.parentOverlay.GetAllocatedHeight() | |
| 195 | 201 | ||
| 196 | 202 | if parentWidth <= 0 || parentHeight <= 0 { | |
| 197 | - | log := logging.FromContext(o.ctx) | |
| 198 | - | log.Error(). | |
| 199 | - | Int("parent_width", parentWidth). | |
| 200 | - | Int("parent_height", parentHeight). | |
| 201 | - | Msg("omnibox: invalid parent overlay dimensions") | |
| 203 | + | // Overlay may not be allocated yet (e.g., right after adding/switching panes). | |
| 204 | + | // Retry once in the next main-loop tick and avoid spamming logs. | |
| 205 | + | if !o.loggedInvalidOverlayDims { | |
| 206 | + | log := logging.FromContext(o.ctx) | |
| 207 | + | log.Error(). | |
| 208 | + | Int("parent_width", parentWidth). | |
| 209 | + | Int("parent_height", parentHeight). | |
| 210 | + | Msg("omnibox: invalid parent overlay dimensions") | |
| 211 | + | o.loggedInvalidOverlayDims = true | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | var cb glib.SourceFunc = func(uintptr) bool { | |
| 215 | + | o.mu.RLock() | |
| 216 | + | visible := o.visible | |
| 217 | + | o.mu.RUnlock() | |
| 218 | + | if visible { | |
| 219 | + | o.resizeAndCenter(rowCount) | |
| 220 | + | } | |
| 221 | + | return false | |
| 222 | + | } | |
| 223 | + | glib.IdleAdd(&cb, 0) | |
| 202 | 224 | return | |
| 203 | 225 | } | |
| 204 | 226 | ||
| @@ -311,6 +333,7 @@ func (o *Omnibox) setupKeyboardHandling() { | |||
| 311 | 333 | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | |
| 312 | 334 | return o.handleKeyPress(keyval, state) | |
| 313 | 335 | } | |
| 336 | + | o.retainedCallbacks = append(o.retainedCallbacks, keyPressedCb) | |
| 314 | 337 | controller.ConnectKeyPressed(&keyPressedCb) | |
| 315 | 338 | ||
| 316 | 339 | o.outerBox.AddController(&controller.EventController) | |
| @@ -363,11 +386,13 @@ func (o *Omnibox) initHeader() error { | |||
| 363 | 386 | historyClickCb := func(_ gtk.Button) { | |
| 364 | 387 | o.setViewMode(ViewModeHistory) | |
| 365 | 388 | } | |
| 389 | + | o.retainedCallbacks = append(o.retainedCallbacks, historyClickCb) | |
| 366 | 390 | o.historyBtn.ConnectClicked(&historyClickCb) | |
| 367 | 391 | ||
| 368 | 392 | favoritesClickCb := func(_ gtk.Button) { | |
| 369 | 393 | o.setViewMode(ViewModeFavorites) | |
| 370 | 394 | } | |
| 395 | + | o.retainedCallbacks = append(o.retainedCallbacks, favoritesClickCb) | |
| 371 | 396 | o.favoritesBtn.ConnectClicked(&favoritesClickCb) | |
| 372 | 397 | ||
| 373 | 398 | o.zoomLabel = gtk.NewLabel(nil) | |
| @@ -424,6 +449,7 @@ func (o *Omnibox) initList() error { | |||
| 424 | 449 | o.mu.Unlock() | |
| 425 | 450 | } | |
| 426 | 451 | } | |
| 452 | + | o.retainedCallbacks = append(o.retainedCallbacks, rowSelectedCb) | |
| 427 | 453 | o.listBox.ConnectRowSelected(&rowSelectedCb) | |
| 428 | 454 | return nil | |
| 429 | 455 | } | |
| @@ -448,6 +474,7 @@ func (o *Omnibox) setupEntryChanged() { | |||
| 448 | 474 | changedCb := func(_ gtk.SearchEntry) { | |
| 449 | 475 | o.onEntryChanged() | |
| 450 | 476 | } | |
| 477 | + | o.retainedCallbacks = append(o.retainedCallbacks, changedCb) | |
| 451 | 478 | o.entry.ConnectSearchChanged(&changedCb) | |
| 452 | 479 | } | |
| 453 | 480 | ||
| @@ -1231,6 +1258,7 @@ func (o *Omnibox) Show(ctx context.Context, query string) { | |||
| 1231 | 1258 | o.outerBox.SetMarginTop(marginTop) | |
| 1232 | 1259 | ||
| 1233 | 1260 | // Show the omnibox | |
| 1261 | + | o.loggedInvalidOverlayDims = false | |
| 1234 | 1262 | o.outerBox.SetVisible(true) | |
| 1235 | 1263 | ||
| 1236 | 1264 | // Trigger initial resize (will be updated when results arrive) | |
| @@ -1359,6 +1387,13 @@ func (o *Omnibox) Destroy() { | |||
| 1359 | 1387 | } | |
| 1360 | 1388 | o.debounceMu.Unlock() | |
| 1361 | 1389 | ||
| 1390 | + | o.mu.Lock() | |
| 1391 | + | o.visible = false | |
| 1392 | + | o.mu.Unlock() | |
| 1393 | + | ||
| 1394 | + | o.parentOverlay = nil | |
| 1395 | + | o.retainedCallbacks = nil | |
| 1396 | + | ||
| 1362 | 1397 | if o.outerBox != nil { | |
| 1363 | 1398 | o.outerBox.Unparent() | |
| 1364 | 1399 | o.outerBox = nil | |
| @@ -107,6 +107,12 @@ type Omnibox struct { | |||||
| 107 | singleRow int // single ListBoxRow natural height | 107 | singleRow int // single ListBoxRow natural height | ||
| 108 | valid bool // whether cache is valid | 108 | valid bool // whether cache is valid | ||
| 109 | } | 109 | } | ||
| 110 | + | ||||
| 111 | + | // Callback retention: keep GTK signal callbacks reachable by Go GC. | |||
| 112 | + | retainedCallbacks []interface{} | |||
| 113 | + | ||||
| 114 | + | // Used to rate-limit allocation-related logs. | |||
| 115 | + | loggedInvalidOverlayDims bool | |||
| 110 | } | 116 | } | ||
| 111 | 117 | ||||
| 112 | // OmniboxConfig holds configuration for creating an Omnibox. | 118 | // OmniboxConfig holds configuration for creating an Omnibox. | ||
| @@ -194,11 +200,27 @@ func (o *Omnibox) resizeAndCenter(rowCount int) { | |||||
| 194 | parentHeight := o.parentOverlay.GetAllocatedHeight() | 200 | parentHeight := o.parentOverlay.GetAllocatedHeight() | ||
| 195 | 201 | ||||
| 196 | if parentWidth <= 0 || parentHeight <= 0 { | 202 | if parentWidth <= 0 || parentHeight <= 0 { | ||
| 197 | - | log := logging.FromContext(o.ctx) | 203 | + | // Overlay may not be allocated yet (e.g., right after adding/switching panes). |
| 198 | - | log.Error(). | 204 | + | // Retry once in the next main-loop tick and avoid spamming logs. |
| 199 | - | Int("parent_width", parentWidth). | 205 | + | if !o.loggedInvalidOverlayDims { |
| 200 | - | Int("parent_height", parentHeight). | 206 | + | log := logging.FromContext(o.ctx) |
| 201 | - | Msg("omnibox: invalid parent overlay dimensions") | 207 | + | log.Error(). |
| 208 | + | Int("parent_width", parentWidth). | |||
| 209 | + | Int("parent_height", parentHeight). | |||
| 210 | + | Msg("omnibox: invalid parent overlay dimensions") | |||
| 211 | + | o.loggedInvalidOverlayDims = true | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | var cb glib.SourceFunc = func(uintptr) bool { | |||
| 215 | + | o.mu.RLock() | |||
| 216 | + | visible := o.visible | |||
| 217 | + | o.mu.RUnlock() | |||
| 218 | + | if visible { | |||
| 219 | + | o.resizeAndCenter(rowCount) | |||
| 220 | + | } | |||
| 221 | + | return false | |||
| 222 | + | } | |||
| 223 | + | glib.IdleAdd(&cb, 0) | |||
| 202 | return | 224 | return | ||
| 203 | } | 225 | } | ||
| 204 | 226 | ||||
| @@ -311,6 +333,7 @@ func (o *Omnibox) setupKeyboardHandling() { | |||||
| 311 | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | 333 | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | ||
| 312 | return o.handleKeyPress(keyval, state) | 334 | return o.handleKeyPress(keyval, state) | ||
| 313 | } | 335 | } | ||
| 336 | + | o.retainedCallbacks = append(o.retainedCallbacks, keyPressedCb) | |||
| 314 | controller.ConnectKeyPressed(&keyPressedCb) | 337 | controller.ConnectKeyPressed(&keyPressedCb) | ||
| 315 | 338 | ||||
| 316 | o.outerBox.AddController(&controller.EventController) | 339 | o.outerBox.AddController(&controller.EventController) | ||
| @@ -363,11 +386,13 @@ func (o *Omnibox) initHeader() error { | |||||
| 363 | historyClickCb := func(_ gtk.Button) { | 386 | historyClickCb := func(_ gtk.Button) { | ||
| 364 | o.setViewMode(ViewModeHistory) | 387 | o.setViewMode(ViewModeHistory) | ||
| 365 | } | 388 | } | ||
| 389 | + | o.retainedCallbacks = append(o.retainedCallbacks, historyClickCb) | |||
| 366 | o.historyBtn.ConnectClicked(&historyClickCb) | 390 | o.historyBtn.ConnectClicked(&historyClickCb) | ||
| 367 | 391 | ||||
| 368 | favoritesClickCb := func(_ gtk.Button) { | 392 | favoritesClickCb := func(_ gtk.Button) { | ||
| 369 | o.setViewMode(ViewModeFavorites) | 393 | o.setViewMode(ViewModeFavorites) | ||
| 370 | } | 394 | } | ||
| 395 | + | o.retainedCallbacks = append(o.retainedCallbacks, favoritesClickCb) | |||
| 371 | o.favoritesBtn.ConnectClicked(&favoritesClickCb) | 396 | o.favoritesBtn.ConnectClicked(&favoritesClickCb) | ||
| 372 | 397 | ||||
| 373 | o.zoomLabel = gtk.NewLabel(nil) | 398 | o.zoomLabel = gtk.NewLabel(nil) | ||
| @@ -424,6 +449,7 @@ func (o *Omnibox) initList() error { | |||||
| 424 | o.mu.Unlock() | 449 | o.mu.Unlock() | ||
| 425 | } | 450 | } | ||
| 426 | } | 451 | } | ||
| 452 | + | o.retainedCallbacks = append(o.retainedCallbacks, rowSelectedCb) | |||
| 427 | o.listBox.ConnectRowSelected(&rowSelectedCb) | 453 | o.listBox.ConnectRowSelected(&rowSelectedCb) | ||
| 428 | return nil | 454 | return nil | ||
| 429 | } | 455 | } | ||
| @@ -448,6 +474,7 @@ func (o *Omnibox) setupEntryChanged() { | |||||
| 448 | changedCb := func(_ gtk.SearchEntry) { | 474 | changedCb := func(_ gtk.SearchEntry) { | ||
| 449 | o.onEntryChanged() | 475 | o.onEntryChanged() | ||
| 450 | } | 476 | } | ||
| 477 | + | o.retainedCallbacks = append(o.retainedCallbacks, changedCb) | |||
| 451 | o.entry.ConnectSearchChanged(&changedCb) | 478 | o.entry.ConnectSearchChanged(&changedCb) | ||
| 452 | } | 479 | } | ||
| 453 | 480 | ||||
| @@ -1231,6 +1258,7 @@ func (o *Omnibox) Show(ctx context.Context, query string) { | |||||
| 1231 | o.outerBox.SetMarginTop(marginTop) | 1258 | o.outerBox.SetMarginTop(marginTop) | ||
| 1232 | 1259 | ||||
| 1233 | // Show the omnibox | 1260 | // Show the omnibox | ||
| 1261 | + | o.loggedInvalidOverlayDims = false | |||
| 1234 | o.outerBox.SetVisible(true) | 1262 | o.outerBox.SetVisible(true) | ||
| 1235 | 1263 | ||||
| 1236 | // Trigger initial resize (will be updated when results arrive) | 1264 | // Trigger initial resize (will be updated when results arrive) | ||
| @@ -1359,6 +1387,13 @@ func (o *Omnibox) Destroy() { | |||||
| 1359 | } | 1387 | } | ||
| 1360 | o.debounceMu.Unlock() | 1388 | o.debounceMu.Unlock() | ||
| 1361 | 1389 | ||||
| 1390 | + | o.mu.Lock() | |||
| 1391 | + | o.visible = false | |||
| 1392 | + | o.mu.Unlock() | |||
| 1393 | + | ||||
| 1394 | + | o.parentOverlay = nil | |||
| 1395 | + | o.retainedCallbacks = nil | |||
| 1396 | + | ||||
| 1362 | if o.outerBox != nil { | 1397 | if o.outerBox != nil { | ||
| 1363 | o.outerBox.Unparent() | 1398 | o.outerBox.Unparent() | ||
| 1364 | o.outerBox = nil | 1399 | o.outerBox = nil | ||
M internal/ui/input/gesture.go +7 -3
| @@ -19,6 +19,9 @@ const ( | |||
| 19 | 19 | type GestureHandler struct { | |
| 20 | 20 | clickGesture *gtk.GestureClick | |
| 21 | 21 | ||
| 22 | + | // Callback retention: must stay reachable by Go GC. | |
| 23 | + | pressedCb func(gtk.GestureClick, int, float64, float64) | |
| 24 | + | ||
| 22 | 25 | // Action handler callback | |
| 23 | 26 | onAction ActionHandler | |
| 24 | 27 | ||
| @@ -63,11 +66,11 @@ func (h *GestureHandler) AttachTo(widget *gtk.Widget) { | |||
| 63 | 66 | // Listen to all mouse buttons (not just primary) | |
| 64 | 67 | h.clickGesture.SetButton(0) | |
| 65 | 68 | ||
| 66 | - | // Connect pressed handler | |
| 67 | - | pressedCb := func(_ gtk.GestureClick, nPress int, _ float64, _ float64) { | |
| 69 | + | // Connect pressed handler (retain callback to prevent GC) | |
| 70 | + | h.pressedCb = func(_ gtk.GestureClick, nPress int, _ float64, _ float64) { | |
| 68 | 71 | h.handlePressed(nPress) | |
| 69 | 72 | } | |
| 70 | - | h.clickGesture.ConnectPressed(&pressedCb) | |
| 73 | + | h.clickGesture.ConnectPressed(&h.pressedCb) | |
| 71 | 74 | ||
| 72 | 75 | // Add controller to widget | |
| 73 | 76 | widget.AddController(&h.clickGesture.EventController) | |
| @@ -118,4 +121,5 @@ func (h *GestureHandler) handlePressed(nPress int) { | |||
| 118 | 121 | // but we clear our reference here. | |
| 119 | 122 | func (h *GestureHandler) Detach() { | |
| 120 | 123 | h.clickGesture = nil | |
| 124 | + | h.pressedCb = nil | |
| 121 | 125 | } | |
| @@ -19,6 +19,9 @@ const ( | |||||
| 19 | type GestureHandler struct { | 19 | type GestureHandler struct { | ||
| 20 | clickGesture *gtk.GestureClick | 20 | clickGesture *gtk.GestureClick | ||
| 21 | 21 | ||||
| 22 | + | // Callback retention: must stay reachable by Go GC. | |||
| 23 | + | pressedCb func(gtk.GestureClick, int, float64, float64) | |||
| 24 | + | ||||
| 22 | // Action handler callback | 25 | // Action handler callback | ||
| 23 | onAction ActionHandler | 26 | onAction ActionHandler | ||
| 24 | 27 | ||||
| @@ -63,11 +66,11 @@ func (h *GestureHandler) AttachTo(widget *gtk.Widget) { | |||||
| 63 | // Listen to all mouse buttons (not just primary) | 66 | // Listen to all mouse buttons (not just primary) | ||
| 64 | h.clickGesture.SetButton(0) | 67 | h.clickGesture.SetButton(0) | ||
| 65 | 68 | ||||
| 66 | - | // Connect pressed handler | 69 | + | // Connect pressed handler (retain callback to prevent GC) |
| 67 | - | pressedCb := func(_ gtk.GestureClick, nPress int, _ float64, _ float64) { | 70 | + | h.pressedCb = func(_ gtk.GestureClick, nPress int, _ float64, _ float64) { |
| 68 | h.handlePressed(nPress) | 71 | h.handlePressed(nPress) | ||
| 69 | } | 72 | } | ||
| 70 | - | h.clickGesture.ConnectPressed(&pressedCb) | 73 | + | h.clickGesture.ConnectPressed(&h.pressedCb) |
| 71 | 74 | ||||
| 72 | // Add controller to widget | 75 | // Add controller to widget | ||
| 73 | widget.AddController(&h.clickGesture.EventController) | 76 | widget.AddController(&h.clickGesture.EventController) | ||
| @@ -118,4 +121,5 @@ func (h *GestureHandler) handlePressed(nPress int) { | |||||
| 118 | // but we clear our reference here. | 121 | // but we clear our reference here. | ||
| 119 | func (h *GestureHandler) Detach() { | 122 | func (h *GestureHandler) Detach() { | ||
| 120 | h.clickGesture = nil | 123 | h.clickGesture = nil | ||
| 124 | + | h.pressedCb = nil | |||
| 121 | } | 125 | } | ||
M internal/ui/input/hover.go +11 -5
| @@ -25,6 +25,10 @@ type HoverHandler struct { | |||
| 25 | 25 | paneID entity.PaneID | |
| 26 | 26 | onEnter HoverCallback | |
| 27 | 27 | ||
| 28 | + | // Callback retention: must stay reachable by Go GC. | |
| 29 | + | enterCb func(gtk.EventControllerMotion, float64, float64) | |
| 30 | + | leaveCb func(gtk.EventControllerMotion) | |
| 31 | + | ||
| 28 | 32 | timer *time.Timer | |
| 29 | 33 | timerMu sync.Mutex | |
| 30 | 34 | canceled bool | |
| @@ -63,17 +67,17 @@ func (h *HoverHandler) AttachTo(widget *gtk.Widget) { | |||
| 63 | 67 | return | |
| 64 | 68 | } | |
| 65 | 69 | ||
| 66 | - | // Connect enter handler with debounce | |
| 67 | - | enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) { | |
| 70 | + | // Connect enter handler with debounce (retain callbacks to prevent GC) | |
| 71 | + | h.enterCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) { | |
| 68 | 72 | h.handleEnter() | |
| 69 | 73 | } | |
| 70 | - | h.motionCtrl.ConnectEnter(&enterCb) | |
| 74 | + | h.motionCtrl.ConnectEnter(&h.enterCb) | |
| 71 | 75 | ||
| 72 | 76 | // Connect leave handler to cancel pending focus | |
| 73 | - | leaveCb := func(_ gtk.EventControllerMotion) { | |
| 77 | + | h.leaveCb = func(_ gtk.EventControllerMotion) { | |
| 74 | 78 | h.handleLeave() | |
| 75 | 79 | } | |
| 76 | - | h.motionCtrl.ConnectLeave(&leaveCb) | |
| 80 | + | h.motionCtrl.ConnectLeave(&h.leaveCb) | |
| 77 | 81 | ||
| 78 | 82 | // Add controller to widget | |
| 79 | 83 | widget.AddController(&h.motionCtrl.EventController) | |
| @@ -131,4 +135,6 @@ func (h *HoverHandler) Detach() { | |||
| 131 | 135 | h.timerMu.Unlock() | |
| 132 | 136 | ||
| 133 | 137 | h.motionCtrl = nil | |
| 138 | + | h.enterCb = nil | |
| 139 | + | h.leaveCb = nil | |
| 134 | 140 | } | |
| @@ -25,6 +25,10 @@ type HoverHandler struct { | |||||
| 25 | paneID entity.PaneID | 25 | paneID entity.PaneID | ||
| 26 | onEnter HoverCallback | 26 | onEnter HoverCallback | ||
| 27 | 27 | ||||
| 28 | + | // Callback retention: must stay reachable by Go GC. | |||
| 29 | + | enterCb func(gtk.EventControllerMotion, float64, float64) | |||
| 30 | + | leaveCb func(gtk.EventControllerMotion) | |||
| 31 | + | ||||
| 28 | timer *time.Timer | 32 | timer *time.Timer | ||
| 29 | timerMu sync.Mutex | 33 | timerMu sync.Mutex | ||
| 30 | canceled bool | 34 | canceled bool | ||
| @@ -63,17 +67,17 @@ func (h *HoverHandler) AttachTo(widget *gtk.Widget) { | |||||
| 63 | return | 67 | return | ||
| 64 | } | 68 | } | ||
| 65 | 69 | ||||
| 66 | - | // Connect enter handler with debounce | 70 | + | // Connect enter handler with debounce (retain callbacks to prevent GC) |
| 67 | - | enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) { | 71 | + | h.enterCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) { |
| 68 | h.handleEnter() | 72 | h.handleEnter() | ||
| 69 | } | 73 | } | ||
| 70 | - | h.motionCtrl.ConnectEnter(&enterCb) | 74 | + | h.motionCtrl.ConnectEnter(&h.enterCb) |
| 71 | 75 | ||||
| 72 | // Connect leave handler to cancel pending focus | 76 | // Connect leave handler to cancel pending focus | ||
| 73 | - | leaveCb := func(_ gtk.EventControllerMotion) { | 77 | + | h.leaveCb = func(_ gtk.EventControllerMotion) { |
| 74 | h.handleLeave() | 78 | h.handleLeave() | ||
| 75 | } | 79 | } | ||
| 76 | - | h.motionCtrl.ConnectLeave(&leaveCb) | 80 | + | h.motionCtrl.ConnectLeave(&h.leaveCb) |
| 77 | 81 | ||||
| 78 | // Add controller to widget | 82 | // Add controller to widget | ||
| 79 | widget.AddController(&h.motionCtrl.EventController) | 83 | widget.AddController(&h.motionCtrl.EventController) | ||
| @@ -131,4 +135,6 @@ func (h *HoverHandler) Detach() { | |||||
| 131 | h.timerMu.Unlock() | 135 | h.timerMu.Unlock() | ||
| 132 | 136 | ||||
| 133 | h.motionCtrl = nil | 137 | h.motionCtrl = nil | ||
| 138 | + | h.enterCb = nil | |||
| 139 | + | h.leaveCb = nil | |||
| 134 | } | 140 | } | ||
M internal/ui/input/keyboard.go +9 -3
| @@ -31,6 +31,9 @@ type KeyboardHandler struct { | |||
| 31 | 31 | // GTK controller (nil until attached) | |
| 32 | 32 | controller *gtk.EventControllerKey | |
| 33 | 33 | ||
| 34 | + | // Callback retention: must stay reachable by Go GC. | |
| 35 | + | keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool | |
| 36 | + | ||
| 34 | 37 | ctx context.Context | |
| 35 | 38 | mu sync.RWMutex | |
| 36 | 39 | } | |
| @@ -98,11 +101,11 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||
| 98 | 101 | // Set capture phase to intercept events before WebView gets them | |
| 99 | 102 | h.controller.SetPropagationPhase(gtk.PhaseCaptureValue) | |
| 100 | 103 | ||
| 101 | - | // Connect key pressed handler | |
| 102 | - | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | |
| 104 | + | // Connect key pressed handler (retain callback to prevent GC). | |
| 105 | + | h.keyPressedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | |
| 103 | 106 | return h.handleKeyPress(keyval, state) | |
| 104 | 107 | } | |
| 105 | - | h.controller.ConnectKeyPressed(&keyPressedCb) | |
| 108 | + | h.controller.ConnectKeyPressed(&h.keyPressedCb) | |
| 106 | 109 | ||
| 107 | 110 | // Add controller to window | |
| 108 | 111 | window.AddController(&h.controller.EventController) | |
| @@ -115,6 +118,7 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||
| 115 | 118 | // but we clear our reference here. | |
| 116 | 119 | func (h *KeyboardHandler) Detach() { | |
| 117 | 120 | h.controller = nil | |
| 121 | + | h.keyPressedCb = nil | |
| 118 | 122 | } | |
| 119 | 123 | ||
| 120 | 124 | // handleKeyPress processes a key press event. | |
| @@ -124,6 +128,8 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, state gdk.ModifierType) bo | |||
| 124 | 128 | shouldBypass := h.shouldBypass | |
| 125 | 129 | h.mu.RUnlock() | |
| 126 | 130 | if shouldBypass != nil && shouldBypass() { | |
| 131 | + | log := logging.FromContext(h.ctx) | |
| 132 | + | log.Debug().Uint("keyval", keyval).Uint("state", uint(state)).Msg("keyboard handler bypassed") | |
| 127 | 133 | return false | |
| 128 | 134 | } | |
| 129 | 135 | ||
| @@ -31,6 +31,9 @@ type KeyboardHandler struct { | |||||
| 31 | // GTK controller (nil until attached) | 31 | // GTK controller (nil until attached) | ||
| 32 | controller *gtk.EventControllerKey | 32 | controller *gtk.EventControllerKey | ||
| 33 | 33 | ||||
| 34 | + | // Callback retention: must stay reachable by Go GC. | |||
| 35 | + | keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool | |||
| 36 | + | ||||
| 34 | ctx context.Context | 37 | ctx context.Context | ||
| 35 | mu sync.RWMutex | 38 | mu sync.RWMutex | ||
| 36 | } | 39 | } | ||
| @@ -98,11 +101,11 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||||
| 98 | // Set capture phase to intercept events before WebView gets them | 101 | // Set capture phase to intercept events before WebView gets them | ||
| 99 | h.controller.SetPropagationPhase(gtk.PhaseCaptureValue) | 102 | h.controller.SetPropagationPhase(gtk.PhaseCaptureValue) | ||
| 100 | 103 | ||||
| 101 | - | // Connect key pressed handler | 104 | + | // Connect key pressed handler (retain callback to prevent GC). |
| 102 | - | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | 105 | + | h.keyPressedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { |
| 103 | return h.handleKeyPress(keyval, state) | 106 | return h.handleKeyPress(keyval, state) | ||
| 104 | } | 107 | } | ||
| 105 | - | h.controller.ConnectKeyPressed(&keyPressedCb) | 108 | + | h.controller.ConnectKeyPressed(&h.keyPressedCb) |
| 106 | 109 | ||||
| 107 | // Add controller to window | 110 | // Add controller to window | ||
| 108 | window.AddController(&h.controller.EventController) | 111 | window.AddController(&h.controller.EventController) | ||
| @@ -115,6 +118,7 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||||
| 115 | // but we clear our reference here. | 118 | // but we clear our reference here. | ||
| 116 | func (h *KeyboardHandler) Detach() { | 119 | func (h *KeyboardHandler) Detach() { | ||
| 117 | h.controller = nil | 120 | h.controller = nil | ||
| 121 | + | h.keyPressedCb = nil | |||
| 118 | } | 122 | } | ||
| 119 | 123 | ||||
| 120 | // handleKeyPress processes a key press event. | 124 | // handleKeyPress processes a key press event. | ||
| @@ -124,6 +128,8 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, state gdk.ModifierType) bo | |||||
| 124 | shouldBypass := h.shouldBypass | 128 | shouldBypass := h.shouldBypass | ||
| 125 | h.mu.RUnlock() | 129 | h.mu.RUnlock() | ||
| 126 | if shouldBypass != nil && shouldBypass() { | 130 | if shouldBypass != nil && shouldBypass() { | ||
| 131 | + | log := logging.FromContext(h.ctx) | |||
| 132 | + | log.Debug().Uint("keyval", keyval).Uint("state", uint(state)).Msg("keyboard handler bypassed") | |||
| 127 | return false | 133 | return false | ||
| 128 | } | 134 | } | ||
| 129 | 135 | ||||