fix(ui): release mode toaster GTK widgets when the legend is rebuilt or destroyed
puregotk constructors return a reference the caller owns, and removing a widget from its parent does not drop it. Unreference the legend groups, rows and keycaps when the legend is replaced, the column boxes when the layout is redistributed, and every toaster widget and retained callback on destroy. Covered by a GTK test watching widget finalization.
- commit
- 28632c9041acd60b070f4a3f51020a31bfec534b
- author
- brice · · 2026-10-14 07:40 UTC
- parent
- 4576e5c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +108 -1
M internal/ui/mode_toaster_view.go +41 -1
| @@ -82,6 +82,12 @@ type gtkModeToasterView struct { | |||
| 82 | 82 | interactive bool | |
| 83 | 83 | animations bool | |
| 84 | 84 | ||
| 85 | + | // legendWidgets are the widgets built for the current legend. puregotk | |
| 86 | + | // constructors hand back a reference we own, which GTK does not drop when | |
| 87 | + | // the widget is removed from its parent, so they are unreferenced once the | |
| 88 | + | // legend is replaced or the view destroyed. | |
| 89 | + | legendWidgets []*gtk.Widget | |
| 90 | + | ||
| 85 | 91 | groups []legendGroup | |
| 86 | 92 | groupBoxes []*gtk.Box | |
| 87 | 93 | columns [][]*gtk.Box // group boxes per column box, as placed | |
| @@ -441,6 +447,7 @@ func (v *gtkModeToasterView) setBodyLingering(lingering bool) { | |||
| 441 | 447 | // setLegend replaces the legend content; nil empties it. | |
| 442 | 448 | func (v *gtkModeToasterView) setLegend(groups []legendGroup) { | |
| 443 | 449 | v.clearColumns() | |
| 450 | + | v.releaseLegendWidgets() | |
| 444 | 451 | v.groupBoxes = nil | |
| 445 | 452 | v.rows = make(map[string][]*gtk.Widget) | |
| 446 | 453 | v.keycaps = make(map[string][]*gtk.Widget) | |
| @@ -457,11 +464,13 @@ func (v *gtkModeToasterView) setLegend(groups []legendGroup) { | |||
| 457 | 464 | ||
| 458 | 465 | func (v *gtkModeToasterView) buildGroup(group legendGroup) *gtk.Box { | |
| 459 | 466 | box := gtk.NewBox(gtk.OrientationVerticalValue, 0) | |
| 467 | + | v.legendWidgets = append(v.legendWidgets, &box.Widget) | |
| 460 | 468 | box.SetCanFocus(false) | |
| 461 | 469 | box.SetCanTarget(false) | |
| 462 | 470 | box.AddCssClass("mode-legend-group") | |
| 463 | 471 | name := group.Name | |
| 464 | 472 | title := gtk.NewLabel(&name) | |
| 473 | + | v.legendWidgets = append(v.legendWidgets, &title.Widget) | |
| 465 | 474 | title.SetCanFocus(false) | |
| 466 | 475 | title.SetCanTarget(false) | |
| 467 | 476 | title.SetHalign(gtk.AlignStartValue) | |
| @@ -475,6 +484,7 @@ func (v *gtkModeToasterView) buildGroup(group legendGroup) *gtk.Box { | |||
| 475 | 484 | ||
| 476 | 485 | func (v *gtkModeToasterView) buildRow(row legendRow) *gtk.Box { | |
| 477 | 486 | box := gtk.NewBox(gtk.OrientationHorizontalValue, 0) | |
| 487 | + | v.legendWidgets = append(v.legendWidgets, &box.Widget) | |
| 478 | 488 | box.SetCanFocus(false) | |
| 479 | 489 | box.SetCanTarget(false) | |
| 480 | 490 | box.AddCssClass("mode-legend-row") | |
| @@ -482,6 +492,7 @@ func (v *gtkModeToasterView) buildRow(row legendRow) *gtk.Box { | |||
| 482 | 492 | for _, key := range row.Keys { | |
| 483 | 493 | display := displayLegendKey(key) | |
| 484 | 494 | keycap := gtk.NewLabel(&display) | |
| 495 | + | v.legendWidgets = append(v.legendWidgets, &keycap.Widget) | |
| 485 | 496 | keycap.SetCanFocus(false) | |
| 486 | 497 | keycap.SetCanTarget(false) | |
| 487 | 498 | keycap.AddCssClass("mode-legend-keycap") | |
| @@ -490,6 +501,7 @@ func (v *gtkModeToasterView) buildRow(row legendRow) *gtk.Box { | |||
| 490 | 501 | } | |
| 491 | 502 | desc := row.Desc | |
| 492 | 503 | text := gtk.NewLabel(&desc) | |
| 504 | + | v.legendWidgets = append(v.legendWidgets, &text.Widget) | |
| 493 | 505 | text.SetCanFocus(false) | |
| 494 | 506 | text.SetCanTarget(false) | |
| 495 | 507 | text.SetHalign(gtk.AlignStartValue) | |
| @@ -507,10 +519,22 @@ func (v *gtkModeToasterView) clearColumns() { | |||
| 507 | 519 | col.Remove(&group.Widget) | |
| 508 | 520 | } | |
| 509 | 521 | v.columnBox.Remove(&col.Widget) | |
| 522 | + | col.Unref() // drop the reference NewBox gave us | |
| 510 | 523 | } | |
| 511 | 524 | v.columnBoxes, v.columns = nil, nil | |
| 512 | 525 | } | |
| 513 | 526 | ||
| 527 | + | // releaseLegendWidgets drops our reference to every widget of the current | |
| 528 | + | // legend. The group boxes must already be detached from the columns. | |
| 529 | + | func (v *gtkModeToasterView) releaseLegendWidgets() { | |
| 530 | + | for _, widget := range v.legendWidgets { | |
| 531 | + | widget.Unref() | |
| 532 | + | } | |
| 533 | + | v.legendWidgets = nil | |
| 534 | + | v.groupBoxes, v.groups = nil, nil | |
| 535 | + | v.rows, v.keycaps = make(map[string][]*gtk.Widget), make(map[string][]*gtk.Widget) | |
| 536 | + | } | |
| 537 | + | ||
| 514 | 538 | // layoutColumns distributes the groups over count columns. | |
| 515 | 539 | func (v *gtkModeToasterView) layoutColumns(count int) { | |
| 516 | 540 | v.clearColumns() | |
| @@ -702,10 +726,26 @@ func (v *gtkModeToasterView) destroy() { | |||
| 702 | 726 | v.positionHandlerID = 0 | |
| 703 | 727 | } | |
| 704 | 728 | v.placement = nil | |
| 729 | + | v.clearColumns() | |
| 730 | + | v.releaseLegendWidgets() | |
| 705 | 731 | v.overlay.RemoveOverlay(&v.pill.Widget) | |
| 706 | 732 | v.overlay.RemoveOverlay(&v.root.Widget) | |
| 733 | + | ||
| 734 | + | // Widgets are handed to us with a reference that GTK does not release on | |
| 735 | + | // removal: drop it. (Controllers are owned by the widget they were added to, | |
| 736 | + | // which released them on removal.) The callbacks go too, now that nothing | |
| 737 | + | // can invoke them. | |
| 738 | + | for _, widget := range []*gtk.Widget{ | |
| 739 | + | &v.pillLabel.Widget, &v.chevron.Widget, &v.hint.Widget, &v.columnBox.Widget, | |
| 740 | + | &v.scroller.Widget, &v.panel.Widget, &v.pill.Widget, &v.root.Widget, | |
| 741 | + | } { | |
| 742 | + | widget.Unref() | |
| 743 | + | } | |
| 744 | + | v.pillClickCb, v.pillScrollCb, v.motionCb, v.enterCb, v.leaveCb, v.keyCb, v.windowClickC = nil, nil, nil, nil, nil, nil, nil | |
| 707 | 745 | v.overlay, v.window = nil, nil | |
| 708 | - | v.pill, v.root = nil, nil | |
| 746 | + | v.pill, v.pillLabel, v.chevron = nil, nil, nil | |
| 747 | + | v.root, v.panel, v.scroller, v.columnBox, v.hint = nil, nil, nil, nil, nil | |
| 748 | + | v.motion, v.keys, v.windowClick, v.pillClick, v.pillScroll = nil, nil, nil, nil, nil | |
| 709 | 749 | } | |
| 710 | 750 | ||
| 711 | 751 | // newModeToaster builds the mode toaster of a window inside its content | |
| @@ -82,6 +82,12 @@ type gtkModeToasterView struct { | |||||
| 82 | interactive bool | 82 | interactive bool | ||
| 83 | animations bool | 83 | animations bool | ||
| 84 | 84 | ||||
| 85 | + | // legendWidgets are the widgets built for the current legend. puregotk | |||
| 86 | + | // constructors hand back a reference we own, which GTK does not drop when | |||
| 87 | + | // the widget is removed from its parent, so they are unreferenced once the | |||
| 88 | + | // legend is replaced or the view destroyed. | |||
| 89 | + | legendWidgets []*gtk.Widget | |||
| 90 | + | ||||
| 85 | groups []legendGroup | 91 | groups []legendGroup | ||
| 86 | groupBoxes []*gtk.Box | 92 | groupBoxes []*gtk.Box | ||
| 87 | columns [][]*gtk.Box // group boxes per column box, as placed | 93 | columns [][]*gtk.Box // group boxes per column box, as placed | ||
| @@ -441,6 +447,7 @@ func (v *gtkModeToasterView) setBodyLingering(lingering bool) { | |||||
| 441 | // setLegend replaces the legend content; nil empties it. | 447 | // setLegend replaces the legend content; nil empties it. | ||
| 442 | func (v *gtkModeToasterView) setLegend(groups []legendGroup) { | 448 | func (v *gtkModeToasterView) setLegend(groups []legendGroup) { | ||
| 443 | v.clearColumns() | 449 | v.clearColumns() | ||
| 450 | + | v.releaseLegendWidgets() | |||
| 444 | v.groupBoxes = nil | 451 | v.groupBoxes = nil | ||
| 445 | v.rows = make(map[string][]*gtk.Widget) | 452 | v.rows = make(map[string][]*gtk.Widget) | ||
| 446 | v.keycaps = make(map[string][]*gtk.Widget) | 453 | v.keycaps = make(map[string][]*gtk.Widget) | ||
| @@ -457,11 +464,13 @@ func (v *gtkModeToasterView) setLegend(groups []legendGroup) { | |||||
| 457 | 464 | ||||
| 458 | func (v *gtkModeToasterView) buildGroup(group legendGroup) *gtk.Box { | 465 | func (v *gtkModeToasterView) buildGroup(group legendGroup) *gtk.Box { | ||
| 459 | box := gtk.NewBox(gtk.OrientationVerticalValue, 0) | 466 | box := gtk.NewBox(gtk.OrientationVerticalValue, 0) | ||
| 467 | + | v.legendWidgets = append(v.legendWidgets, &box.Widget) | |||
| 460 | box.SetCanFocus(false) | 468 | box.SetCanFocus(false) | ||
| 461 | box.SetCanTarget(false) | 469 | box.SetCanTarget(false) | ||
| 462 | box.AddCssClass("mode-legend-group") | 470 | box.AddCssClass("mode-legend-group") | ||
| 463 | name := group.Name | 471 | name := group.Name | ||
| 464 | title := gtk.NewLabel(&name) | 472 | title := gtk.NewLabel(&name) | ||
| 473 | + | v.legendWidgets = append(v.legendWidgets, &title.Widget) | |||
| 465 | title.SetCanFocus(false) | 474 | title.SetCanFocus(false) | ||
| 466 | title.SetCanTarget(false) | 475 | title.SetCanTarget(false) | ||
| 467 | title.SetHalign(gtk.AlignStartValue) | 476 | title.SetHalign(gtk.AlignStartValue) | ||
| @@ -475,6 +484,7 @@ func (v *gtkModeToasterView) buildGroup(group legendGroup) *gtk.Box { | |||||
| 475 | 484 | ||||
| 476 | func (v *gtkModeToasterView) buildRow(row legendRow) *gtk.Box { | 485 | func (v *gtkModeToasterView) buildRow(row legendRow) *gtk.Box { | ||
| 477 | box := gtk.NewBox(gtk.OrientationHorizontalValue, 0) | 486 | box := gtk.NewBox(gtk.OrientationHorizontalValue, 0) | ||
| 487 | + | v.legendWidgets = append(v.legendWidgets, &box.Widget) | |||
| 478 | box.SetCanFocus(false) | 488 | box.SetCanFocus(false) | ||
| 479 | box.SetCanTarget(false) | 489 | box.SetCanTarget(false) | ||
| 480 | box.AddCssClass("mode-legend-row") | 490 | box.AddCssClass("mode-legend-row") | ||
| @@ -482,6 +492,7 @@ func (v *gtkModeToasterView) buildRow(row legendRow) *gtk.Box { | |||||
| 482 | for _, key := range row.Keys { | 492 | for _, key := range row.Keys { | ||
| 483 | display := displayLegendKey(key) | 493 | display := displayLegendKey(key) | ||
| 484 | keycap := gtk.NewLabel(&display) | 494 | keycap := gtk.NewLabel(&display) | ||
| 495 | + | v.legendWidgets = append(v.legendWidgets, &keycap.Widget) | |||
| 485 | keycap.SetCanFocus(false) | 496 | keycap.SetCanFocus(false) | ||
| 486 | keycap.SetCanTarget(false) | 497 | keycap.SetCanTarget(false) | ||
| 487 | keycap.AddCssClass("mode-legend-keycap") | 498 | keycap.AddCssClass("mode-legend-keycap") | ||
| @@ -490,6 +501,7 @@ func (v *gtkModeToasterView) buildRow(row legendRow) *gtk.Box { | |||||
| 490 | } | 501 | } | ||
| 491 | desc := row.Desc | 502 | desc := row.Desc | ||
| 492 | text := gtk.NewLabel(&desc) | 503 | text := gtk.NewLabel(&desc) | ||
| 504 | + | v.legendWidgets = append(v.legendWidgets, &text.Widget) | |||
| 493 | text.SetCanFocus(false) | 505 | text.SetCanFocus(false) | ||
| 494 | text.SetCanTarget(false) | 506 | text.SetCanTarget(false) | ||
| 495 | text.SetHalign(gtk.AlignStartValue) | 507 | text.SetHalign(gtk.AlignStartValue) | ||
| @@ -507,10 +519,22 @@ func (v *gtkModeToasterView) clearColumns() { | |||||
| 507 | col.Remove(&group.Widget) | 519 | col.Remove(&group.Widget) | ||
| 508 | } | 520 | } | ||
| 509 | v.columnBox.Remove(&col.Widget) | 521 | v.columnBox.Remove(&col.Widget) | ||
| 522 | + | col.Unref() // drop the reference NewBox gave us | |||
| 510 | } | 523 | } | ||
| 511 | v.columnBoxes, v.columns = nil, nil | 524 | v.columnBoxes, v.columns = nil, nil | ||
| 512 | } | 525 | } | ||
| 513 | 526 | ||||
| 527 | + | // releaseLegendWidgets drops our reference to every widget of the current | |||
| 528 | + | // legend. The group boxes must already be detached from the columns. | |||
| 529 | + | func (v *gtkModeToasterView) releaseLegendWidgets() { | |||
| 530 | + | for _, widget := range v.legendWidgets { | |||
| 531 | + | widget.Unref() | |||
| 532 | + | } | |||
| 533 | + | v.legendWidgets = nil | |||
| 534 | + | v.groupBoxes, v.groups = nil, nil | |||
| 535 | + | v.rows, v.keycaps = make(map[string][]*gtk.Widget), make(map[string][]*gtk.Widget) | |||
| 536 | + | } | |||
| 537 | + | ||||
| 514 | // layoutColumns distributes the groups over count columns. | 538 | // layoutColumns distributes the groups over count columns. | ||
| 515 | func (v *gtkModeToasterView) layoutColumns(count int) { | 539 | func (v *gtkModeToasterView) layoutColumns(count int) { | ||
| 516 | v.clearColumns() | 540 | v.clearColumns() | ||
| @@ -702,10 +726,26 @@ func (v *gtkModeToasterView) destroy() { | |||||
| 702 | v.positionHandlerID = 0 | 726 | v.positionHandlerID = 0 | ||
| 703 | } | 727 | } | ||
| 704 | v.placement = nil | 728 | v.placement = nil | ||
| 729 | + | v.clearColumns() | |||
| 730 | + | v.releaseLegendWidgets() | |||
| 705 | v.overlay.RemoveOverlay(&v.pill.Widget) | 731 | v.overlay.RemoveOverlay(&v.pill.Widget) | ||
| 706 | v.overlay.RemoveOverlay(&v.root.Widget) | 732 | v.overlay.RemoveOverlay(&v.root.Widget) | ||
| 733 | + | ||||
| 734 | + | // Widgets are handed to us with a reference that GTK does not release on | |||
| 735 | + | // removal: drop it. (Controllers are owned by the widget they were added to, | |||
| 736 | + | // which released them on removal.) The callbacks go too, now that nothing | |||
| 737 | + | // can invoke them. | |||
| 738 | + | for _, widget := range []*gtk.Widget{ | |||
| 739 | + | &v.pillLabel.Widget, &v.chevron.Widget, &v.hint.Widget, &v.columnBox.Widget, | |||
| 740 | + | &v.scroller.Widget, &v.panel.Widget, &v.pill.Widget, &v.root.Widget, | |||
| 741 | + | } { | |||
| 742 | + | widget.Unref() | |||
| 743 | + | } | |||
| 744 | + | v.pillClickCb, v.pillScrollCb, v.motionCb, v.enterCb, v.leaveCb, v.keyCb, v.windowClickC = nil, nil, nil, nil, nil, nil, nil | |||
| 707 | v.overlay, v.window = nil, nil | 745 | v.overlay, v.window = nil, nil | ||
| 708 | - | v.pill, v.root = nil, nil | 746 | + | v.pill, v.pillLabel, v.chevron = nil, nil, nil |
| 747 | + | v.root, v.panel, v.scroller, v.columnBox, v.hint = nil, nil, nil, nil, nil | |||
| 748 | + | v.motion, v.keys, v.windowClick, v.pillClick, v.pillScroll = nil, nil, nil, nil, nil | |||
| 709 | } | 749 | } | ||
| 710 | 750 | ||||
| 711 | // newModeToaster builds the mode toaster of a window inside its content | 751 | // newModeToaster builds the mode toaster of a window inside its content | ||
A internal/ui/mode_toaster_view_gtk_test.go +67 -0
| @@ -0,0 +1,67 @@ | |||
| 1 | + | package ui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/puregotk/v4/glib" | |
| 7 | + | "github.com/bnema/puregotk/v4/gobject" | |
| 8 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // watchFinalized reports, through the returned flag, when widget is finalized. | |
| 13 | + | func watchFinalized(t *testing.T, widget *gtk.Widget) *bool { | |
| 14 | + | t.Helper() | |
| 15 | + | finalized := new(bool) | |
| 16 | + | notify := gobject.WeakNotify(func(_, _ uintptr) { *finalized = true }) | |
| 17 | + | t.Cleanup(func() { _ = notify }) | |
| 18 | + | widget.WeakRef(¬ify, 0) | |
| 19 | + | return finalized | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func TestGTKModeToasterViewReleasesWidgets(t *testing.T) { | |
| 23 | + | gtkApp := requireGTKDisplayApp(t) | |
| 24 | + | defer gtkApp.Unref() | |
| 25 | + | ||
| 26 | + | var view *gtkModeToasterView | |
| 27 | + | done := make(chan struct{}) | |
| 28 | + | run := glib.SourceFunc(func(uintptr) bool { | |
| 29 | + | defer close(done) | |
| 30 | + | window := gtk.NewApplicationWindow(gtkApp) | |
| 31 | + | overlay := gtk.NewOverlay() | |
| 32 | + | window.SetChild(&overlay.Widget) | |
| 33 | + | view = newGTKModeToasterView(overlay, window, modeToasterEvents{}) | |
| 34 | + | require.NotNil(t, view) | |
| 35 | + | ||
| 36 | + | groups := []legendGroup{{Name: "SCROLL", Rows: []legendRow{{Action: "down", Keys: []string{"j"}, Desc: "Down"}}}} | |
| 37 | + | view.setLegend(groups) | |
| 38 | + | row := watchFinalized(t, view.rows["down"][0]) | |
| 39 | + | keycap := watchFinalized(t, view.keycaps["down"][0]) | |
| 40 | + | group := watchFinalized(t, &view.groupBoxes[0].Widget) | |
| 41 | + | ||
| 42 | + | view.setLegend(nil) | |
| 43 | + | require.True(t, *row, "a replaced legend row is released") | |
| 44 | + | require.True(t, *keycap, "a replaced legend keycap is released") | |
| 45 | + | require.True(t, *group, "a replaced legend group is released") | |
| 46 | + | ||
| 47 | + | view.setLegend(groups) | |
| 48 | + | view.layoutColumns(1) | |
| 49 | + | column := watchFinalized(t, &view.columnBoxes[0].Widget) | |
| 50 | + | view.layoutColumns(2) | |
| 51 | + | require.True(t, *column, "a column dropped by a relayout is released") | |
| 52 | + | ||
| 53 | + | pill := watchFinalized(t, &view.pill.Widget) | |
| 54 | + | body := watchFinalized(t, &view.root.Widget) | |
| 55 | + | label := watchFinalized(t, &view.pillLabel.Widget) | |
| 56 | + | lastRow := watchFinalized(t, view.rows["down"][0]) | |
| 57 | + | view.destroy() | |
| 58 | + | require.True(t, *pill, "destroy releases the pill") | |
| 59 | + | require.True(t, *body, "destroy releases the legend body") | |
| 60 | + | require.True(t, *label, "destroy releases the pill label") | |
| 61 | + | require.True(t, *lastRow, "destroy releases the legend rows") | |
| 62 | + | window.Destroy() | |
| 63 | + | return false | |
| 64 | + | }) | |
| 65 | + | glib.MainContextDefault().Invoke(&run, 0) | |
| 66 | + | <-done | |
| 67 | + | } | |
| @@ -0,0 +1,67 @@ | |||||
| 1 | + | package ui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/puregotk/v4/glib" | |||
| 7 | + | "github.com/bnema/puregotk/v4/gobject" | |||
| 8 | + | "github.com/bnema/puregotk/v4/gtk" | |||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // watchFinalized reports, through the returned flag, when widget is finalized. | |||
| 13 | + | func watchFinalized(t *testing.T, widget *gtk.Widget) *bool { | |||
| 14 | + | t.Helper() | |||
| 15 | + | finalized := new(bool) | |||
| 16 | + | notify := gobject.WeakNotify(func(_, _ uintptr) { *finalized = true }) | |||
| 17 | + | t.Cleanup(func() { _ = notify }) | |||
| 18 | + | widget.WeakRef(¬ify, 0) | |||
| 19 | + | return finalized | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | func TestGTKModeToasterViewReleasesWidgets(t *testing.T) { | |||
| 23 | + | gtkApp := requireGTKDisplayApp(t) | |||
| 24 | + | defer gtkApp.Unref() | |||
| 25 | + | ||||
| 26 | + | var view *gtkModeToasterView | |||
| 27 | + | done := make(chan struct{}) | |||
| 28 | + | run := glib.SourceFunc(func(uintptr) bool { | |||
| 29 | + | defer close(done) | |||
| 30 | + | window := gtk.NewApplicationWindow(gtkApp) | |||
| 31 | + | overlay := gtk.NewOverlay() | |||
| 32 | + | window.SetChild(&overlay.Widget) | |||
| 33 | + | view = newGTKModeToasterView(overlay, window, modeToasterEvents{}) | |||
| 34 | + | require.NotNil(t, view) | |||
| 35 | + | ||||
| 36 | + | groups := []legendGroup{{Name: "SCROLL", Rows: []legendRow{{Action: "down", Keys: []string{"j"}, Desc: "Down"}}}} | |||
| 37 | + | view.setLegend(groups) | |||
| 38 | + | row := watchFinalized(t, view.rows["down"][0]) | |||
| 39 | + | keycap := watchFinalized(t, view.keycaps["down"][0]) | |||
| 40 | + | group := watchFinalized(t, &view.groupBoxes[0].Widget) | |||
| 41 | + | ||||
| 42 | + | view.setLegend(nil) | |||
| 43 | + | require.True(t, *row, "a replaced legend row is released") | |||
| 44 | + | require.True(t, *keycap, "a replaced legend keycap is released") | |||
| 45 | + | require.True(t, *group, "a replaced legend group is released") | |||
| 46 | + | ||||
| 47 | + | view.setLegend(groups) | |||
| 48 | + | view.layoutColumns(1) | |||
| 49 | + | column := watchFinalized(t, &view.columnBoxes[0].Widget) | |||
| 50 | + | view.layoutColumns(2) | |||
| 51 | + | require.True(t, *column, "a column dropped by a relayout is released") | |||
| 52 | + | ||||
| 53 | + | pill := watchFinalized(t, &view.pill.Widget) | |||
| 54 | + | body := watchFinalized(t, &view.root.Widget) | |||
| 55 | + | label := watchFinalized(t, &view.pillLabel.Widget) | |||
| 56 | + | lastRow := watchFinalized(t, view.rows["down"][0]) | |||
| 57 | + | view.destroy() | |||
| 58 | + | require.True(t, *pill, "destroy releases the pill") | |||
| 59 | + | require.True(t, *body, "destroy releases the legend body") | |||
| 60 | + | require.True(t, *label, "destroy releases the pill label") | |||
| 61 | + | require.True(t, *lastRow, "destroy releases the legend rows") | |||
| 62 | + | window.Destroy() | |||
| 63 | + | return false | |||
| 64 | + | }) | |||
| 65 | + | glib.MainContextDefault().Invoke(&run, 0) | |||
| 66 | + | <-done | |||
| 67 | + | } | |||