detached 6c6c1d8

fix(ticks): release raw callback slots

commit
6c6c1d8593ee32e5a84a52dd65024f15ba11c568
author
brice · · 2026-07-14 06:23 UTC
committer
brice · · 2026-07-14 07:06 UTC
parent
d80cc8e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +40 -16
M internal/infrastructure/cef/webview.go +16 -6
@@ -2071,12 +2071,14 @@ func (wv *WebView) startBeginFrameLoop() {
20712071 cb := new(gtk.TickCallback)
20722072 *cb = func(_, _, _ uintptr) bool {
20732073 if wv.destroyed.Load() {
2074+ wv.releaseBeginFrameTickCallback()
20742075 return false
20752076 }
20762077 wv.mu.RLock()
20772078 host := wv.host
20782079 wv.mu.RUnlock()
20792080 if host == nil {
2081+ wv.releaseBeginFrameTickCallback()
20802082 return false
20812083 }
20822084 host.SendExternalBeginFrame()
@@ -2099,22 +2101,30 @@ func (wv *WebView) startBeginFrameLoop() {
20992101 }
21002102
21012103 func (wv *WebView) stopBeginFrameLoop() {
2102- if wv.viewBridge == nil || wv.viewBridge.Widget() == nil {
2103- return
2104- }
2105-
21062104 wv.mu.Lock()
21072105 tickID := wv.beginFrameTickID
21082106 wv.beginFrameTickID = 0
21092107 wv.beginFrameTick = nil
2110- widget := wv.viewBridge.Widget()
2108+ bridge := wv.viewBridge
21112109 wv.mu.Unlock()
21122110
2113- if tickID != 0 {
2111+ if tickID == 0 || bridge == nil {
2112+ return
2113+ }
2114+ if widget := bridge.Widget(); widget != nil {
21142115 widget.RemoveTickCallback(tickID)
21152116 }
21162117 }
21172118
2119+// releaseBeginFrameTickCallback releases the Go callback reference after GTK
2120+// removes a source because its callback returned false.
2121+func (wv *WebView) releaseBeginFrameTickCallback() {
2122+ wv.mu.Lock()
2123+ wv.beginFrameTickID = 0
2124+ wv.beginFrameTick = nil
2125+ wv.mu.Unlock()
2126+}
2127+
21182128 func (wv *WebView) runOnGTK(fn func()) {
21192129 if fn == nil {
21202130 return
@@ -2071,12 +2071,14 @@ func (wv *WebView) startBeginFrameLoop() {
2071 cb := new(gtk.TickCallback)2071 cb := new(gtk.TickCallback)
2072 *cb = func(_, _, _ uintptr) bool {2072 *cb = func(_, _, _ uintptr) bool {
2073 if wv.destroyed.Load() {2073 if wv.destroyed.Load() {
2074+ wv.releaseBeginFrameTickCallback()
2074 return false2075 return false
2075 }2076 }
2076 wv.mu.RLock()2077 wv.mu.RLock()
2077 host := wv.host2078 host := wv.host
2078 wv.mu.RUnlock()2079 wv.mu.RUnlock()
2079 if host == nil {2080 if host == nil {
2081+ wv.releaseBeginFrameTickCallback()
2080 return false2082 return false
2081 }2083 }
2082 host.SendExternalBeginFrame()2084 host.SendExternalBeginFrame()
@@ -2099,22 +2101,30 @@ func (wv *WebView) startBeginFrameLoop() {
2099 }2101 }
2100 2102
2101 func (wv *WebView) stopBeginFrameLoop() {2103 func (wv *WebView) stopBeginFrameLoop() {
2102- if wv.viewBridge == nil || wv.viewBridge.Widget() == nil {
2103- return
2104- }
2105-
2106 wv.mu.Lock()2104 wv.mu.Lock()
2107 tickID := wv.beginFrameTickID2105 tickID := wv.beginFrameTickID
2108 wv.beginFrameTickID = 02106 wv.beginFrameTickID = 0
2109 wv.beginFrameTick = nil2107 wv.beginFrameTick = nil
2110- widget := wv.viewBridge.Widget()2108+ bridge := wv.viewBridge
2111 wv.mu.Unlock()2109 wv.mu.Unlock()
2112 2110
2113- if tickID != 0 {2111+ if tickID == 0 || bridge == nil {
2112+ return
2113+ }
2114+ if widget := bridge.Widget(); widget != nil {
2114 widget.RemoveTickCallback(tickID)2115 widget.RemoveTickCallback(tickID)
2115 }2116 }
2116 }2117 }
2117 2118
2119+// releaseBeginFrameTickCallback releases the Go callback reference after GTK
2120+// removes a source because its callback returned false.
2121+func (wv *WebView) releaseBeginFrameTickCallback() {
2122+ wv.mu.Lock()
2123+ wv.beginFrameTickID = 0
2124+ wv.beginFrameTick = nil
2125+ wv.mu.Unlock()
2126+}
2127+
2118 func (wv *WebView) runOnGTK(fn func()) {2128 func (wv *WebView) runOnGTK(fn func()) {
2119 if fn == nil {2129 if fn == nil {
2120 return2130 return
M internal/ui/app.go +24 -10
@@ -167,7 +167,10 @@ type floatingWorkspaceSession struct {
167167 omnibox *component.Omnibox
168168 omniboxWidget layout.Widget
169169 resizeWatcherActive bool
170- resizeTickID uint
170+ // resizeTickCallback keeps the purego callback live until its GTK source is
171+ // removed. It is cleared exactly once with resizeTickID.
172+ resizeTickCallback *gtk.TickCallback
173+ resizeTickID uint
171174 appliedWidth int
172175 appliedHeight int
173176 }
@@ -4594,22 +4597,22 @@ func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) {
45944597
45954598 session.resizeWatcherActive = true
45964599 paneID := session.paneID
4597- tickCallback := gtk.TickCallback(func(_ uintptr, _ uintptr, _ uintptr) bool {
4600+ callback := new(gtk.TickCallback)
4601+ *callback = func(_ uintptr, _ uintptr, _ uintptr) bool {
45984602 liveSession := a.floatingSessionByPaneID(paneID)
45994603 if liveSession == nil {
4600- session.resizeWatcherActive = false
4601- session.resizeTickID = 0
4604+ session.releaseResizeTickCallback()
46024605 return false
46034606 }
46044607
46054608 keepRunning := a.handleFloatingViewportTick(liveSession)
46064609 if !keepRunning {
4607- liveSession.resizeWatcherActive = false
4608- liveSession.resizeTickID = 0
4610+ liveSession.releaseResizeTickCallback()
46094611 }
46104612 return keepRunning
4611- })
4612- session.resizeTickID = overlayWidget.AddTickCallback(&tickCallback, 0, nil)
4613+ }
4614+ session.resizeTickCallback = callback
4615+ session.resizeTickID = overlayWidget.AddTickCallback(callback, 0, nil)
46134616 }
46144617
46154618 func (a *App) stopFloatingResizeWatcher(session *floatingWorkspaceSession) {
@@ -4617,12 +4620,23 @@ func (a *App) stopFloatingResizeWatcher(session *floatingWorkspaceSession) {
46174620 return
46184621 }
46194622
4620- if session.resizeTickID != 0 && session.overlay != nil {
4623+ tickID := session.resizeTickID
4624+ session.releaseResizeTickCallback()
4625+ if tickID != 0 && session.overlay != nil {
46214626 if overlayWidget := session.overlay.GtkWidget(); overlayWidget != nil {
4622- overlayWidget.RemoveTickCallback(session.resizeTickID)
4627+ overlayWidget.RemoveTickCallback(tickID)
46234628 }
46244629 }
4630+}
4631+
4632+// releaseResizeTickCallback releases the raw purego callback slot. It is safe
4633+// to call after GTK has already removed a callback that returned false.
4634+func (session *floatingWorkspaceSession) releaseResizeTickCallback() {
4635+ if session == nil {
4636+ return
4637+ }
46254638 session.resizeTickID = 0
4639+ session.resizeTickCallback = nil
46264640 session.resizeWatcherActive = false
46274641 }
46284642
@@ -167,7 +167,10 @@ type floatingWorkspaceSession struct {
167 omnibox *component.Omnibox167 omnibox *component.Omnibox
168 omniboxWidget layout.Widget168 omniboxWidget layout.Widget
169 resizeWatcherActive bool169 resizeWatcherActive bool
170- resizeTickID uint170+ // resizeTickCallback keeps the purego callback live until its GTK source is
171+ // removed. It is cleared exactly once with resizeTickID.
172+ resizeTickCallback *gtk.TickCallback
173+ resizeTickID uint
171 appliedWidth int174 appliedWidth int
172 appliedHeight int175 appliedHeight int
173 }176 }
@@ -4594,22 +4597,22 @@ func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) {
4594 4597
4595 session.resizeWatcherActive = true4598 session.resizeWatcherActive = true
4596 paneID := session.paneID4599 paneID := session.paneID
4597- tickCallback := gtk.TickCallback(func(_ uintptr, _ uintptr, _ uintptr) bool {4600+ callback := new(gtk.TickCallback)
4601+ *callback = func(_ uintptr, _ uintptr, _ uintptr) bool {
4598 liveSession := a.floatingSessionByPaneID(paneID)4602 liveSession := a.floatingSessionByPaneID(paneID)
4599 if liveSession == nil {4603 if liveSession == nil {
4600- session.resizeWatcherActive = false4604+ session.releaseResizeTickCallback()
4601- session.resizeTickID = 0
4602 return false4605 return false
4603 }4606 }
4604 4607
4605 keepRunning := a.handleFloatingViewportTick(liveSession)4608 keepRunning := a.handleFloatingViewportTick(liveSession)
4606 if !keepRunning {4609 if !keepRunning {
4607- liveSession.resizeWatcherActive = false4610+ liveSession.releaseResizeTickCallback()
4608- liveSession.resizeTickID = 0
4609 }4611 }
4610 return keepRunning4612 return keepRunning
4611- })4613+ }
4612- session.resizeTickID = overlayWidget.AddTickCallback(&tickCallback, 0, nil)4614+ session.resizeTickCallback = callback
4615+ session.resizeTickID = overlayWidget.AddTickCallback(callback, 0, nil)
4613 }4616 }
4614 4617
4615 func (a *App) stopFloatingResizeWatcher(session *floatingWorkspaceSession) {4618 func (a *App) stopFloatingResizeWatcher(session *floatingWorkspaceSession) {
@@ -4617,12 +4620,23 @@ func (a *App) stopFloatingResizeWatcher(session *floatingWorkspaceSession) {
4617 return4620 return
4618 }4621 }
4619 4622
4620- if session.resizeTickID != 0 && session.overlay != nil {4623+ tickID := session.resizeTickID
4624+ session.releaseResizeTickCallback()
4625+ if tickID != 0 && session.overlay != nil {
4621 if overlayWidget := session.overlay.GtkWidget(); overlayWidget != nil {4626 if overlayWidget := session.overlay.GtkWidget(); overlayWidget != nil {
4622- overlayWidget.RemoveTickCallback(session.resizeTickID)4627+ overlayWidget.RemoveTickCallback(tickID)
4623 }4628 }
4624 }4629 }
4630+}
4631+
4632+// releaseResizeTickCallback releases the raw purego callback slot. It is safe
4633+// to call after GTK has already removed a callback that returned false.
4634+func (session *floatingWorkspaceSession) releaseResizeTickCallback() {
4635+ if session == nil {
4636+ return
4637+ }
4625 session.resizeTickID = 04638 session.resizeTickID = 0
4639+ session.resizeTickCallback = nil
4626 session.resizeWatcherActive = false4640 session.resizeWatcherActive = false
4627 }4641 }
4628 4642

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close