AdwTabPage does not unreference child after being closed

I recently realized a program of mine has memory leaks. Upon debugging, I narrowed it down to an issue while closing tabs using AdwTabView. I have created a small demo terminal app that can reproduce this issue.

Terminal tabs can be closed in three ways:

  1. User closes the tab directly (clicks on the “X” button on the tab)
  2. User closes the window, then all tabs are closed
  3. The process running in the terminal exits, which causes a chain reaction to close the tab for that terminal
    • Context for this case:
      • There is an intermediary component between AdwTabPage and Terminal: TerminalTab. TerminalTab listens to the child_exited event emitted by Terminal and emits a close_request event of its own when that happens
      • Upon adding a TerminalTab to AdwTabView (with adw_tab_view_add_page), I listen for the close_request event TerminalTab may emit
      • The callback for the close_request event gets the page for the TerminalTab that emitted the event (with adw_tab_view_get_page) and calls adw_tab_view_close_page

Scenarios 1 and 2 work flawlessly. Upon closing a tab or the window, both dispose, and Vala’s “destructor” methods are called.

Scenario 3, however, requires me to call terminal_tab.unparent (line 118) in the close_request callback. Not calling unparent causes a leak; the terminal tab, and terminal, consequently, are never free’d.

Demo Source
class MyTerminal : Vte.Terminal {
    public MyTerminal () {
        Object ();

        this.spawn_async (Vte.PtyFlags.DEFAULT,
                        { "zsh" },

    ~MyTerminal () {
        message ("MyTerminal destroyed");

    public override void dispose () {
        message ("MyTerminal dispose");
        base.dispose ();

class MyTerminalTab : Gtk.Box {
    private MyTerminal terminal;

    public signal void close_request ();

    public MyTerminalTab () {
        Object (orientation: Gtk.Orientation.VERTICAL, spacing: 0);

        this.terminal = new MyTerminal ();
        this.append (this.terminal);

        this.terminal.child_exited.connect (this.on_child_exited);

    private void on_child_exited (int code) {
        message ("Child exited with code %d", code);
        this.close_request.emit ();

    ~MyTerminalTab () {
        message ("MyTerminalTab destroyed");

    public override void dispose () {
        message ("MyTerminalTab dispose");

        //  This causes terminal to emit child_exited. Without this call,
        //  terminal is still disposed and destroyed, but it does not emit the
        //  signal.
        //  this.terminal = null;

        //  This changes nothing (no signal emited nor any error thrown)
        //  this.remove (this.terminal);

        base.dispose ();

class SimpleWindow : Adw.ApplicationWindow {
  private Adw.TabView tab_view;
  private Adw.TabBar tab_bar;

  construct {
    var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);

    var b = new Gtk.Button () {
      icon_name = "list-add-symbolic",
      action_name = "",

    this.tab_view = new Adw.TabView ();

    this.tab_bar = new Adw.TabBar () {
      view = this.tab_view,
      autohide = false,

    var hb = new Adw.HeaderBar () {
      show_end_title_buttons = true,
      show_start_title_buttons = true,

    hb.pack_end (b);
    box.append (hb);
    box.append (this.tab_bar);
    box.append (this.tab_view);

    this.content = box;

    ActionEntry[] entries = {
      { "new-tab", this.add_tab },

    this.add_action_entries (entries, this);

  private void add_tab () {
    var tt = new MyTerminalTab ();

    tt.close_request.connect (this.on_tt_close_request);

    this.tab_view.append (tt);

  private void on_tt_close_request (MyTerminalTab terminal_tab) {
    message ("MyTerminalTab emitted a close request");
    var page = this.tab_view.get_page (terminal_tab);
    if (page != null) {
      message ("Closing page");
      this.tab_view.close_page (page);
      // [QUESTION] why is it that if I don't call unparent here, Adw.TabPage
      // keeps a referece to the terminal tab, causing it never to be free'd?
      terminal_tab.unparent ();

static int main (string[] args) {
  var app = new Adw.Application ("com.raggesilver.Test",

  app.activate.connect ((application) => {
    var w = new SimpleWindow () { application = application as Adw.Application };
    w.present ();

  return (args);
} (not required)
project('vala-memory', ['c', 'vala'],
          version: '0.0.1',
    meson_version: '>= 0.50.0',
  default_options: [ 'warning_level=2',

vala_memory_sources = files(

vala_memory_deps = [
  dependency('gtk4', version: '>= 4.10.0'),
  dependency('libadwaita-1', version: '>= 1.3'),
  dependency('vte-2.91-gtk4', version: '>= 0.71.0'),

executable('adw-tab-test', vala_memory_sources,
  vala_args: '--target-glib=2.50',  dependencies: vala_memory_deps,
  install: true,

Build instructions: valac --pkg=gtk4 --pkg=libadwaita-1 --pkg=vte-2.91-gtk4 adw-tab-test.vala

I have also created a snippet in GitLab with the source and compilation instructions for this demo AdwTabPage closing issue ($5873) · Snippets · Paulo Queiroz / Black Box · GitLab

Am I really supposed to call unparent here? Is this a bug, or am I doing something wrong?


Do you follow this?

The [Adw.TabView::close-page] handler is expected to call adw_tab_view_close_page_finish() to confirm or reject the closing.

Otherwise, I suspect you are running into the fact that in GTK4 there is no direct way to destroy a widget anymore (no gtk_widget_destroy()) but rather you have to remove it from its parent to release the reference the parent holds, allow the refcount to get to zero, and thus release the child. I think gtk_widget_unparent() should (usually?) be sufficient to do this (i.e. to replace the previous, generic gtk_container_remove()), but I’m not 100% sure yet. I’m pondering the same question for gtkmm atm.

AFAIK, I don’t have to specify my own handler. The default handler will “accept” closing any non-pinned tab.

My question is, why does clicking on the close tab button not require me to call unparent but listening to the event and closing it via API does?

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.