Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,9 @@ func (a *App) onAppActivate(ctx context.Context) {
gutil.Connect(a.win.MainWindow, a.win.MainWindow.ConnectCloseRequest, func(gtk.Window) bool {
win := a.win
a.win = nil
// The window is destroyed after this returns; release the
// window's Go-side references once the main loop is idle.
gutil.IdleAdd(win.Destroy)
// The window is destroyed after this returns; drop ours once
// the main loop is idle.
gutil.IdleAdd(win.Widget().Unref)
return false
})

Expand Down
37 changes: 21 additions & 16 deletions internal/ui/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ package ui
import (
"testing"

"deedles.dev/trayscale/internal/gutil"
"codeberg.org/puregotk/puregotk/v4/adw"
"codeberg.org/puregotk/puregotk/v4/gio"
"deedles.dev/trayscale/internal/tsutil"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
)

// TestFillFromBuilder builds every .ui file into its Go struct so that
// a type missing from gutil's registry fails here instead of panicking
// at runtime.
func TestFillFromBuilder(t *testing.T) {
// TestTemplates builds every template class through its constructor,
// so a template child whose type doesn't match its field fails here
// instead of panicking at runtime.
func TestTemplates(t *testing.T) {
requireGTK(t)

fill := func(t *testing.T, build func()) {
build := func(t *testing.T, build func()) {
t.Helper()
defer func() {
if r := recover(); r != nil {
Expand All @@ -25,29 +26,33 @@ func TestFillFromBuilder(t *testing.T) {
build()
}

running := &tsutil.IPNStatus{State: ipn.Running, Prefs: ipn.NewPrefs().View()}

t.Run("MainWindow", func(t *testing.T) {
var win MainWindow
fill(t, func() { gutil.FillFromUI(&win, menuXML, mainWindowXML); gutil.UnrefFields(&win) })
app := &App{app: adw.NewApplication("dev.deedles.Trayscale.test.templates", gio.GApplicationNonUniqueValue)}
defer app.app.Unref()
build(t, func() {
win := NewMainWindow(app)
win.MainWindow.Destroy()
win.Widget().Unref()
})
})
t.Run("OfflinePage", func(t *testing.T) {
fill(t, func() { offlinePageClass.New().Destroy() })
build(t, func() { offlinePageClass.New().Widget().Unref() })
})
t.Run("PeerPage", func(t *testing.T) {
peer := layoutPeer(t, "peer", "peer", true, false, false)
status := layoutStatus([]tailcfg.NodeView{peer})
status.Prefs = ipn.NewPrefs().View()
fill(t, func() { NewPeerPage(&App{}, status, peer).Destroy() })
build(t, func() { NewPeerPage(&App{}, status, peer).Widget().Unref() })
})
t.Run("SelfPage", func(t *testing.T) {
status := &tsutil.IPNStatus{State: ipn.Running, Prefs: ipn.NewPrefs().View()}
fill(t, func() { NewSelfPage(&App{}, status).Destroy() })
build(t, func() { NewSelfPage(&App{}, running).Widget().Unref() })
})
t.Run("MullvadPage", func(t *testing.T) {
status := &tsutil.IPNStatus{State: ipn.Running, Prefs: ipn.NewPrefs().View()}
fill(t, func() { NewMullvadPage(&App{}, status).Destroy() })
build(t, func() { NewMullvadPage(&App{}, running).Widget().Unref() })
})
t.Run("PreferencesDialog", func(t *testing.T) {
var dialog PreferencesDialog
fill(t, func() { gutil.FillFromUI(&dialog, preferencesXML); gutil.UnrefFields(&dialog) })
build(t, func() { NewPreferencesDialog().Widget().Unref() })
})
}
6 changes: 3 additions & 3 deletions internal/ui/leak_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestPeerPageLeaks(t *testing.T) {
}
stack.Remove(page.Widget())
vp.Unref()
page.Destroy()
page.Widget().Unref()
}

const warmup, n, objs = 5, 200, 6
Expand Down Expand Up @@ -102,7 +102,7 @@ func TestSelfPageLeaks(t *testing.T) {
}
stack.Remove(page.Widget())
vp.Unref()
page.Destroy()
page.Widget().Unref()
}

const warmup, n, objs = 5, 200, 5
Expand Down Expand Up @@ -171,7 +171,7 @@ func TestMullvadPageLeaks(t *testing.T) {

stack.Remove(page.Widget())
vp.Unref()
page.Destroy()
page.Widget().Unref()
}

const warmup, n = 5, 100
Expand Down
39 changes: 24 additions & 15 deletions internal/ui/mainwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"codeberg.org/puregotk/puregotk/v4/gio"
"codeberg.org/puregotk/puregotk/v4/glib"
"codeberg.org/puregotk/puregotk/v4/gtk"
"deedles.dev/trayscale/internal/gobj"
"deedles.dev/trayscale/internal/gutil"
"deedles.dev/trayscale/internal/listmodels"
"deedles.dev/trayscale/internal/locale"
Expand All @@ -33,10 +34,20 @@ var (
menuXML string
)

// The main window's template carries menu.ui's menus, which its menu
// buttons refer to by id.
var mainWindowClass = gobj.Widget[MainWindow](gobj.WidgetSpec{
Name: "TrayscaleMainWindow",
Parent: adw.ApplicationWindowGLibType,
Template: strings.Replace(mainWindowXML, "</interface>", menuXML[strings.Index(menuXML, "<menu "):strings.LastIndex(menuXML, "</interface>")]+"</interface>", 1),
})

type MainWindow struct {
gobj.Base

app *App

MainWindow *adw.ApplicationWindow
MainWindow *adw.ApplicationWindow `gtk:"-"`
ToastOverlay *adw.ToastOverlay
SplitView *adw.NavigationSplitView
StatusSwitch *gtk.Switch
Expand Down Expand Up @@ -64,16 +75,15 @@ type MainWindow struct {
}

func NewMainWindow(app *App) *MainWindow {
win := MainWindow{
app: app,
pages: make(map[string]Page),
stackPages: make(map[string]*adw.ViewStackPage),
showOffline: true,
}
win := mainWindowClass.New()
win.MainWindow = adw.ApplicationWindowNewFromInternalPtr(win.GoPointer())
win.app = app
win.pages = make(map[string]Page)
win.stackPages = make(map[string]*adw.ViewStackPage)
win.showOffline = true
if app.settings != nil {
win.showOffline = app.settings.GetBoolean("show-offline-peers")
}
gutil.FillFromUI(&win, menuXML, mainWindowXML)

win.MainWindow.SetApplication(&app.app.Application)

Expand Down Expand Up @@ -155,7 +165,7 @@ func NewMainWindow(app *App) *MainWindow {

win.initPeerSearch()

return &win
return win
}

func (win *MainWindow) initPeerSearch() {
Expand Down Expand Up @@ -229,7 +239,7 @@ func (win *MainWindow) removePage(name string, page Page) {
vp.Unref()
delete(win.stackPages, name)
}
page.Destroy()
page.Widget().Unref()

if !reselect {
return
Expand All @@ -241,11 +251,11 @@ func (win *MainWindow) removePage(name string, page Page) {
win.PeersStack.SetVisibleChildName(remaining[0].name)
}

// Destroy releases the window's pages and Go-side references after the
// window has been closed.
func (win *MainWindow) Destroy() {
// Dispose releases the window's pages and Go-side references when the
// window is destroyed.
func (win *MainWindow) Dispose() {
for _, page := range win.pages {
page.Destroy()
page.Widget().Unref()
}
clear(win.pages)
for _, vp := range win.stackPages {
Expand All @@ -254,7 +264,6 @@ func (win *MainWindow) Destroy() {
clear(win.stackPages)
win.profileModel.Unref()
win.profileSortModel.Unref()
gutil.UnrefFields(win)
}

func (win *MainWindow) Update(status tsutil.Status) {
Expand Down
6 changes: 3 additions & 3 deletions internal/ui/mainwindow.ui
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.96.1 -->
<interface>
<interface domain="trayscale">
<!-- interface-name mainwindow.ui -->
<requires lib="gtk" version="4.18"/>
<requires lib="libadwaita" version="1.9"/>
<object class="AdwApplicationWindow" id="MainWindow">
<template class="TrayscaleMainWindow" parent="AdwApplicationWindow">
<property name="content">ToastOverlay</property>
<property name="default-height">600</property>
<property name="default-width">800</property>
Expand Down Expand Up @@ -114,5 +114,5 @@
<setter object="PeersSidebar" property="mode">page</setter>
</object>
</child>
</object>
</template>
</interface>
3 changes: 1 addition & 2 deletions internal/ui/migration_repro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ func (p *stackStub) Widget() *gtk.Widget { return p.w }
func (p *stackStub) Actions() *gio.SimpleActionGroup { return nil }
func (p *stackStub) Bind(vp *adw.ViewStackPage) { p.page = vp }
func (p *stackStub) Update(tsutil.Status) bool { p.page.SetTitle("still-bound"); return true }
func (p *stackStub) Destroy() { p.w.Unref() }

func TestRestackReleasesReplacedStackPage(t *testing.T) {
requireGTK(t)
Expand Down Expand Up @@ -107,7 +106,7 @@ func TestRestackReleasesReplacedStackPage(t *testing.T) {
t.Fatalf("replaced stack page finalized %d times, want 1", replaced)
}

win.Destroy()
win.Dispose()
if kept != 1 {
t.Fatalf("filtered stack page finalized %d times after Destroy, want 1", kept)
}
Expand Down
4 changes: 0 additions & 4 deletions internal/ui/mullvadpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,6 @@ func (page *MullvadPage) Actions() *gio.SimpleActionGroup {
return nil
}

func (page *MullvadPage) Destroy() {
page.Widget().Unref()
}

func (page *MullvadPage) Dispose() {
for _, node := range page.exitNodes {
node.row.Unref()
Expand Down
4 changes: 0 additions & 4 deletions internal/ui/offlinepage.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ func (page *OfflinePage) Bind(stackPage *adw.ViewStackPage) {
stackPage.SetIconName(page.Page.GetIconName())
}

func (page *OfflinePage) Destroy() {
page.Widget().Unref()
}

func (page *OfflinePage) Update(status tsutil.Status) bool {
if status, ok := status.(*tsutil.IPNStatus); ok {
page.NeedsAuthGroup.SetVisible(status.NeedsAuth())
Expand Down
4 changes: 0 additions & 4 deletions internal/ui/peerpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,6 @@ func (page *PeerPage) Actions() *gio.SimpleActionGroup {
return page.actions
}

func (page *PeerPage) Destroy() {
page.Widget().Unref()
}

func (page *PeerPage) Dispose() {
page.addrModel.Unref()
page.routeModel.Unref()
Expand Down
18 changes: 13 additions & 5 deletions internal/ui/preferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ import (

"codeberg.org/puregotk/puregotk/v4/adw"
"codeberg.org/puregotk/puregotk/v4/gtk"
"deedles.dev/trayscale/internal/gutil"
"deedles.dev/trayscale/internal/gobj"
)

//go:embed preferences.ui
var preferencesXML string

var preferencesDialogClass = gobj.Widget[PreferencesDialog](gobj.WidgetSpec{
Name: "TrayscalePreferencesDialog",
Parent: adw.PreferencesDialogGLibType,
Template: preferencesXML,
})

type PreferencesDialog struct {
PreferencesDialog *adw.PreferencesDialog
gobj.Base

PreferencesDialog *adw.PreferencesDialog `gtk:"-"`
UseTrayIconRow *adw.SwitchRow
PollingIntervalRow *adw.SpinRow
PollingIntervalAdjustment *gtk.Adjustment
Expand All @@ -21,7 +29,7 @@ type PreferencesDialog struct {
}

func NewPreferencesDialog() *PreferencesDialog {
var win PreferencesDialog
gutil.FillFromUI(&win, preferencesXML)
return &win
win := preferencesDialogClass.New()
win.PreferencesDialog = adw.PreferencesDialogNewFromInternalPtr(win.GoPointer())
return win
}
6 changes: 3 additions & 3 deletions internal/ui/preferences.ui
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 1.0.3 -->
<interface>
<interface domain="trayscale">
<!-- interface-name preferences.ui -->
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.6"/>
<object class="AdwPreferencesDialog" id="PreferencesDialog">
<template class="TrayscalePreferencesDialog" parent="AdwPreferencesDialog">
<property name="visible-page">PreferencesPage</property>
<child>
<object class="AdwPreferencesPage" id="PreferencesPage">
Expand Down Expand Up @@ -54,5 +54,5 @@
</child>
</object>
</child>
</object>
</template>
</interface>
4 changes: 0 additions & 4 deletions internal/ui/selfpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,6 @@ func (page *SelfPage) Actions() *gio.SimpleActionGroup {
return page.actions
}

func (page *SelfPage) Destroy() {
page.Widget().Unref()
}

func (page *SelfPage) Dispose() {
page.setLatencies(nil)
page.addrModel.Unref()
Expand Down
6 changes: 1 addition & 5 deletions internal/ui/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,8 @@ func (a *App) showPreferences() {
})
})

// The presented dialog keeps itself alive; drop the fields'
// references once it's closed.
gutil.Connect(dialog.PreferencesDialog, dialog.PreferencesDialog.ConnectClosed, func(adw.Dialog) {
gutil.IdleAdd(func() { gutil.UnrefFields(dialog) })
})
dialog.PreferencesDialog.Present(a.parent())
dialog.Widget().Unref() // Presented dialogs keep themselves alive.
}

// showAbout shows the app's about dialog.
Expand Down
6 changes: 3 additions & 3 deletions internal/ui/slots_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ func slotsInUse(t *testing.T, n, m int) (used int) {
}
for _, p := range peers {
stack.Remove(p.Widget())
p.Destroy()
p.Widget().Unref()
}
if mullvad != nil {
stack.Remove(mullvad.Widget())
mullvad.Destroy()
mullvad.Widget().Unref()
}
// Shared callbacks made on first use stay, so wait for the
// main loop to go idle rather than for the count to return.
Expand Down Expand Up @@ -146,7 +146,7 @@ func BenchmarkMullvadPage(b *testing.B) {
}
page.applySearch()
stack.Remove(page.Widget())
page.Destroy()
page.Widget().Unref()
}
}

Expand Down
7 changes: 2 additions & 5 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ func optBoolIcon(v opt.Bool) *gio.ThemedIcon {
// Page represents the UI for a single page of the app. This usually
// corresponds to information about a specific peer in the tailnet.
type Page interface {
// Widget returns the page's widget, borrowed from the page.
// Widget returns the page's widget, borrowed from the page. Whoever
// holds the page owns one reference to it.
Widget() *gtk.Widget
// Actions returns the page's action group, borrowed from the page,
// or nil.
Expand All @@ -68,8 +69,4 @@ type Page interface {
// in the constructor, not here.
Bind(*adw.ViewStackPage)
Update(tsutil.Status) bool

// Destroy releases the page's references. It is called once, after
// the page's widget has been removed from the stack.
Destroy()
}
Loading