detached 7c3858b

fix(ticks): release split raw callback slots

commit
7c3858b0c868bdc71d38a851521d02e9ba7c147e
author
brice · · 2026-07-14 07:11 UTC
committer
brice · · 2026-07-14 09:32 UTC
parent
67cc805
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +49 -7
M internal/ui/layout/gtk_widgets.go +49 -7
@@ -1,7 +1,10 @@
11 package layout
22
33 import (
4+ "sync"
5+
46 "github.com/bnema/puregotk/v4/gdk"
7+ "github.com/bnema/puregotk/v4/glib"
58 "github.com/bnema/puregotk/v4/gobject"
69 "github.com/bnema/puregotk/v4/graphene"
710 "github.com/bnema/puregotk/v4/gtk"
@@ -82,8 +85,14 @@ func (w *gtkWidget) ComputePoint(target Widget) (x, y float64, ok bool) {
8285 // gtkPaned wraps gtk.Paned to implement PanedWidget.
8386 type gtkPaned struct {
8487 inner *gtk.Paned
88+
89+ tickMu sync.Mutex
90+ tickCallbacks map[uint]*gtk.TickCallback
8591 }
8692
93+// unrefTickCallback is the canonical puregotk/purego callback-slot release.
94+var unrefTickCallback = glib.UnrefCallback
95+
8796 func (p *gtkPaned) Show() { p.inner.Show() }
8897 func (p *gtkPaned) Hide() { p.inner.Hide() }
8998 func (p *gtkPaned) SetVisible(visible bool) { p.inner.SetVisible(visible) }
@@ -176,14 +185,47 @@ func (p *gtkPaned) ConnectNotifyPosition(callback func()) uint {
176185 }
177186
178187 func (p *gtkPaned) AddTickCallback(callback func() bool) uint {
179- cb := gtk.TickCallback(func(widget uintptr, frameClock uintptr, userData uintptr) bool {
180- return callback()
181- })
182- return p.inner.AddTickCallback(&cb, 0, nil)
188+ cb := new(gtk.TickCallback)
189+ var id uint
190+ *cb = func(_ uintptr, _ uintptr, _ uintptr) bool {
191+ keepRunning := callback()
192+ if !keepRunning {
193+ // GTK removes a false-returning source itself; release its matching
194+ // purego slot exactly once rather than merely dropping the Go pointer.
195+ p.releaseTickCallback(id)
196+ }
197+ return keepRunning
198+ }
199+ id = p.inner.AddTickCallback(cb, 0, nil)
200+ if id != 0 {
201+ p.tickMu.Lock()
202+ if p.tickCallbacks == nil {
203+ p.tickCallbacks = make(map[uint]*gtk.TickCallback)
204+ }
205+ p.tickCallbacks[id] = cb
206+ p.tickMu.Unlock()
207+ }
208+ return id
209+}
210+
211+func (p *gtkPaned) RemoveTickCallback(id uint) {
212+ p.inner.RemoveTickCallback(id)
213+ p.releaseTickCallback(id)
214+}
215+
216+func (p *gtkPaned) releaseTickCallback(id uint) {
217+ if id == 0 {
218+ return
219+ }
220+ p.tickMu.Lock()
221+ callback := p.tickCallbacks[id]
222+ delete(p.tickCallbacks, id)
223+ p.tickMu.Unlock()
224+ if callback != nil {
225+ _ = unrefTickCallback(callback)
226+ }
183227 }
184228
185-func (p *gtkPaned) RemoveTickCallback(id uint) { p.inner.RemoveTickCallback(id) }
186-
187229 func (p *gtkPaned) GetAllocatedWidth() int { return p.inner.GetAllocatedWidth() }
188230 func (p *gtkPaned) GetAllocatedHeight() int { return p.inner.GetAllocatedHeight() }
189231 func (p *gtkPaned) AddController(c *gtk.EventController) { p.inner.AddController(c) }
@@ -781,7 +823,7 @@ func (f *GtkWidgetFactory) NewPaned(orientation Orientation) PanedWidget {
781823 paned := gtk.NewPaned(orientation)
782824 paned.SetHexpand(true)
783825 paned.SetVexpand(true)
784- return &gtkPaned{inner: paned}
826+ return &gtkPaned{inner: paned, tickCallbacks: make(map[uint]*gtk.TickCallback)}
785827 }
786828
787829 func (f *GtkWidgetFactory) NewBox(orientation Orientation, spacing int) BoxWidget {
@@ -1,7 +1,10 @@
1 package layout1 package layout
2 2
3 import (3 import (
4+ "sync"
5+
4 "github.com/bnema/puregotk/v4/gdk"6 "github.com/bnema/puregotk/v4/gdk"
7+ "github.com/bnema/puregotk/v4/glib"
5 "github.com/bnema/puregotk/v4/gobject"8 "github.com/bnema/puregotk/v4/gobject"
6 "github.com/bnema/puregotk/v4/graphene"9 "github.com/bnema/puregotk/v4/graphene"
7 "github.com/bnema/puregotk/v4/gtk"10 "github.com/bnema/puregotk/v4/gtk"
@@ -82,8 +85,14 @@ func (w *gtkWidget) ComputePoint(target Widget) (x, y float64, ok bool) {
82 // gtkPaned wraps gtk.Paned to implement PanedWidget.85 // gtkPaned wraps gtk.Paned to implement PanedWidget.
83 type gtkPaned struct {86 type gtkPaned struct {
84 inner *gtk.Paned87 inner *gtk.Paned
88+
89+ tickMu sync.Mutex
90+ tickCallbacks map[uint]*gtk.TickCallback
85 }91 }
86 92
93+// unrefTickCallback is the canonical puregotk/purego callback-slot release.
94+var unrefTickCallback = glib.UnrefCallback
95+
87 func (p *gtkPaned) Show() { p.inner.Show() }96 func (p *gtkPaned) Show() { p.inner.Show() }
88 func (p *gtkPaned) Hide() { p.inner.Hide() }97 func (p *gtkPaned) Hide() { p.inner.Hide() }
89 func (p *gtkPaned) SetVisible(visible bool) { p.inner.SetVisible(visible) }98 func (p *gtkPaned) SetVisible(visible bool) { p.inner.SetVisible(visible) }
@@ -176,14 +185,47 @@ func (p *gtkPaned) ConnectNotifyPosition(callback func()) uint {
176 }185 }
177 186
178 func (p *gtkPaned) AddTickCallback(callback func() bool) uint {187 func (p *gtkPaned) AddTickCallback(callback func() bool) uint {
179- cb := gtk.TickCallback(func(widget uintptr, frameClock uintptr, userData uintptr) bool {188+ cb := new(gtk.TickCallback)
180- return callback()189+ var id uint
181- })190+ *cb = func(_ uintptr, _ uintptr, _ uintptr) bool {
182- return p.inner.AddTickCallback(&cb, 0, nil)191+ keepRunning := callback()
192+ if !keepRunning {
193+ // GTK removes a false-returning source itself; release its matching
194+ // purego slot exactly once rather than merely dropping the Go pointer.
195+ p.releaseTickCallback(id)
196+ }
197+ return keepRunning
198+ }
199+ id = p.inner.AddTickCallback(cb, 0, nil)
200+ if id != 0 {
201+ p.tickMu.Lock()
202+ if p.tickCallbacks == nil {
203+ p.tickCallbacks = make(map[uint]*gtk.TickCallback)
204+ }
205+ p.tickCallbacks[id] = cb
206+ p.tickMu.Unlock()
207+ }
208+ return id
209+}
210+
211+func (p *gtkPaned) RemoveTickCallback(id uint) {
212+ p.inner.RemoveTickCallback(id)
213+ p.releaseTickCallback(id)
214+}
215+
216+func (p *gtkPaned) releaseTickCallback(id uint) {
217+ if id == 0 {
218+ return
219+ }
220+ p.tickMu.Lock()
221+ callback := p.tickCallbacks[id]
222+ delete(p.tickCallbacks, id)
223+ p.tickMu.Unlock()
224+ if callback != nil {
225+ _ = unrefTickCallback(callback)
226+ }
183 }227 }
184 228
185-func (p *gtkPaned) RemoveTickCallback(id uint) { p.inner.RemoveTickCallback(id) }
186-
187 func (p *gtkPaned) GetAllocatedWidth() int { return p.inner.GetAllocatedWidth() }229 func (p *gtkPaned) GetAllocatedWidth() int { return p.inner.GetAllocatedWidth() }
188 func (p *gtkPaned) GetAllocatedHeight() int { return p.inner.GetAllocatedHeight() }230 func (p *gtkPaned) GetAllocatedHeight() int { return p.inner.GetAllocatedHeight() }
189 func (p *gtkPaned) AddController(c *gtk.EventController) { p.inner.AddController(c) }231 func (p *gtkPaned) AddController(c *gtk.EventController) { p.inner.AddController(c) }
@@ -781,7 +823,7 @@ func (f *GtkWidgetFactory) NewPaned(orientation Orientation) PanedWidget {
781 paned := gtk.NewPaned(orientation)823 paned := gtk.NewPaned(orientation)
782 paned.SetHexpand(true)824 paned.SetHexpand(true)
783 paned.SetVexpand(true)825 paned.SetVexpand(true)
784- return &gtkPaned{inner: paned}826+ return &gtkPaned{inner: paned, tickCallbacks: make(map[uint]*gtk.TickCallback)}
785 }827 }
786 828
787 func (f *GtkWidgetFactory) NewBox(orientation Orientation, spacing int) BoxWidget {829 func (f *GtkWidgetFactory) NewBox(orientation Orientation, spacing int) BoxWidget {

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