fix(ui): release mode frame border reference and tidy native GTK test setup
- commit
- 9a8327648025fde1929b81b8a17debb6e17ab01a
- author
- brice · · 2026-10-14 08:21 UTC
- parent
- 320f6a5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +26 -1
M internal/ui/mode_frame.go +2 -0
| @@ -75,6 +75,8 @@ func (f *modeFrame) destroy() { | |||
| 75 | 75 | f.borderRect = nil | |
| 76 | 76 | if f.overlay != nil && f.border != nil { | |
| 77 | 77 | f.overlay.RemoveOverlay(&f.border.Widget) | |
| 78 | + | // NewBox's reference is not released by the removal. | |
| 79 | + | f.border.Unref() | |
| 78 | 80 | } | |
| 79 | 81 | f.overlay = nil | |
| 80 | 82 | f.border = nil | |
| @@ -75,6 +75,8 @@ func (f *modeFrame) destroy() { | |||||
| 75 | f.borderRect = nil | 75 | f.borderRect = nil | ||
| 76 | if f.overlay != nil && f.border != nil { | 76 | if f.overlay != nil && f.border != nil { | ||
| 77 | f.overlay.RemoveOverlay(&f.border.Widget) | 77 | f.overlay.RemoveOverlay(&f.border.Widget) | ||
| 78 | + | // NewBox's reference is not released by the removal. | |||
| 79 | + | f.border.Unref() | |||
| 78 | } | 80 | } | ||
| 79 | f.overlay = nil | 81 | f.overlay = nil | ||
| 80 | f.border = nil | 82 | f.border = nil | ||
M internal/ui/mode_toaster_view_gtk_test.go +24 -1
| @@ -22,14 +22,21 @@ func watchFinalized(t *testing.T, widget *gtk.Widget) *bool { | |||
| 22 | 22 | func TestGTKModeToasterViewReleasesWidgets(t *testing.T) { | |
| 23 | 23 | gtkApp := requireGTKDisplayApp(t) | |
| 24 | 24 | defer gtkApp.Unref() | |
| 25 | + | // Windows may only be created once the GApplication has started up; | |
| 26 | + | // registering emits the startup signal. | |
| 27 | + | registered, err := gtkApp.Register(nil) | |
| 28 | + | require.NoError(t, err) | |
| 29 | + | require.True(t, registered, "the test application registers") | |
| 25 | 30 | ||
| 26 | 31 | var view *gtkModeToasterView | |
| 27 | 32 | done := make(chan struct{}) | |
| 28 | 33 | run := glib.SourceFunc(func(uintptr) bool { | |
| 29 | 34 | defer close(done) | |
| 30 | 35 | window := gtk.NewApplicationWindow(gtkApp) | |
| 36 | + | require.NotNil(t, window) | |
| 31 | 37 | overlay := gtk.NewOverlay() | |
| 32 | 38 | window.SetChild(&overlay.Widget) | |
| 39 | + | overlayFinalized := watchFinalized(t, &overlay.Widget) | |
| 33 | 40 | view = newGTKModeToasterView(overlay, window, modeToasterEvents{}) | |
| 34 | 41 | require.NotNil(t, view) | |
| 35 | 42 | ||
| @@ -59,7 +66,23 @@ func TestGTKModeToasterViewReleasesWidgets(t *testing.T) { | |||
| 59 | 66 | require.True(t, *body, "destroy releases the legend body") | |
| 60 | 67 | require.True(t, *label, "destroy releases the pill label") | |
| 61 | 68 | require.True(t, *lastRow, "destroy releases the legend rows") | |
| 62 | - | window.Destroy() | |
| 69 | + | ||
| 70 | + | frame := newModeFrame(overlay) | |
| 71 | + | require.NotNil(t, frame) | |
| 72 | + | border := watchFinalized(t, &frame.border.Widget) | |
| 73 | + | frame.destroy() | |
| 74 | + | require.True(t, *border, "destroy releases the mode frame border") | |
| 75 | + | ||
| 76 | + | // The window is never presented: destroying an unrealized window | |
| 77 | + | // crashes on Wayland, so drop the references instead (the same as | |
| 78 | + | // the global shortcut handler test). The overlay's last reference | |
| 79 | + | // goes with the window's child; the window itself stays on GTK's | |
| 80 | + | // toplevel list until destroyed, so its finalization is not asserted. | |
| 81 | + | window.SetChild(nil) | |
| 82 | + | overlay.Unref() | |
| 83 | + | require.True(t, *overlayFinalized, "the test releases its overlay") | |
| 84 | + | gtkApp.RemoveWindow(&window.Window) | |
| 85 | + | window.Unref() | |
| 63 | 86 | return false | |
| 64 | 87 | }) | |
| 65 | 88 | glib.MainContextDefault().Invoke(&run, 0) | |
| @@ -22,14 +22,21 @@ func watchFinalized(t *testing.T, widget *gtk.Widget) *bool { | |||||
| 22 | func TestGTKModeToasterViewReleasesWidgets(t *testing.T) { | 22 | func TestGTKModeToasterViewReleasesWidgets(t *testing.T) { | ||
| 23 | gtkApp := requireGTKDisplayApp(t) | 23 | gtkApp := requireGTKDisplayApp(t) | ||
| 24 | defer gtkApp.Unref() | 24 | defer gtkApp.Unref() | ||
| 25 | + | // Windows may only be created once the GApplication has started up; | |||
| 26 | + | // registering emits the startup signal. | |||
| 27 | + | registered, err := gtkApp.Register(nil) | |||
| 28 | + | require.NoError(t, err) | |||
| 29 | + | require.True(t, registered, "the test application registers") | |||
| 25 | 30 | ||||
| 26 | var view *gtkModeToasterView | 31 | var view *gtkModeToasterView | ||
| 27 | done := make(chan struct{}) | 32 | done := make(chan struct{}) | ||
| 28 | run := glib.SourceFunc(func(uintptr) bool { | 33 | run := glib.SourceFunc(func(uintptr) bool { | ||
| 29 | defer close(done) | 34 | defer close(done) | ||
| 30 | window := gtk.NewApplicationWindow(gtkApp) | 35 | window := gtk.NewApplicationWindow(gtkApp) | ||
| 36 | + | require.NotNil(t, window) | |||
| 31 | overlay := gtk.NewOverlay() | 37 | overlay := gtk.NewOverlay() | ||
| 32 | window.SetChild(&overlay.Widget) | 38 | window.SetChild(&overlay.Widget) | ||
| 39 | + | overlayFinalized := watchFinalized(t, &overlay.Widget) | |||
| 33 | view = newGTKModeToasterView(overlay, window, modeToasterEvents{}) | 40 | view = newGTKModeToasterView(overlay, window, modeToasterEvents{}) | ||
| 34 | require.NotNil(t, view) | 41 | require.NotNil(t, view) | ||
| 35 | 42 | ||||
| @@ -59,7 +66,23 @@ func TestGTKModeToasterViewReleasesWidgets(t *testing.T) { | |||||
| 59 | require.True(t, *body, "destroy releases the legend body") | 66 | require.True(t, *body, "destroy releases the legend body") | ||
| 60 | require.True(t, *label, "destroy releases the pill label") | 67 | require.True(t, *label, "destroy releases the pill label") | ||
| 61 | require.True(t, *lastRow, "destroy releases the legend rows") | 68 | require.True(t, *lastRow, "destroy releases the legend rows") | ||
| 62 | - | window.Destroy() | 69 | + | |
| 70 | + | frame := newModeFrame(overlay) | |||
| 71 | + | require.NotNil(t, frame) | |||
| 72 | + | border := watchFinalized(t, &frame.border.Widget) | |||
| 73 | + | frame.destroy() | |||
| 74 | + | require.True(t, *border, "destroy releases the mode frame border") | |||
| 75 | + | ||||
| 76 | + | // The window is never presented: destroying an unrealized window | |||
| 77 | + | // crashes on Wayland, so drop the references instead (the same as | |||
| 78 | + | // the global shortcut handler test). The overlay's last reference | |||
| 79 | + | // goes with the window's child; the window itself stays on GTK's | |||
| 80 | + | // toplevel list until destroyed, so its finalization is not asserted. | |||
| 81 | + | window.SetChild(nil) | |||
| 82 | + | overlay.Unref() | |||
| 83 | + | require.True(t, *overlayFinalized, "the test releases its overlay") | |||
| 84 | + | gtkApp.RemoveWindow(&window.Window) | |||
| 85 | + | window.Unref() | |||
| 63 | return false | 86 | return false | ||
| 64 | }) | 87 | }) | ||
| 65 | glib.MainContextDefault().Invoke(&run, 0) | 88 | glib.MainContextDefault().Invoke(&run, 0) | ||