Gtk-rs, ref-counting cycles, and `connect_destroy`

I have an open question about architecture/style in gtk-rs programs. Programming a UI application with gtk-rs involves connecting callbacks to events which occur on widgets. Most of the tutorials and examples provided by the gtk-rs project use an architecture in which a rather large main() function sets up the event callbacks, and discuss logistics around clone() of ref-counted GTK objects into closures, even providing a clone!() macro to collapse e.g.

let widget_a = widget_a.clone();
let widget_b = widget_b.clone();
let closure = move || { widget_a.foo(); widget_b.bar(); };

into

let closure = clone!(@strong widget_a, @strong widget_b => move || { widget_a.foo(); widget_b.bar(); });

This gets unwieldy once there are enough callbacks to connect and widgets to clone. In my case, there are about 20 widgets in my main application window MainWindow which exist for the lifetime of the window. Here's one example of a callback in my application which is unwieldy to connect, even with the clone!() macro:

unfrob_button.connect_clicked(clone!(@weak foo_list, @weak bar_list, @weak bar_model, @weak frob_unfrob_stack, @weak frob_button, @weak bar_data, @weak baz_data => move |_| {
  bar_data.clear();
  frob_unfrob_stack.set_visible_child(&frob_button);
  repopulate_selected_foo_model(&foo_list, &bar_list, &bar_model, &bar_data, &baz_data);
}));

Imagine setting up a couple dozen of these callbacks. It becomes tedious to enumerate all the widgets used in each callback, difficult to refactor, and hard to read. On top of that, if a line break is inserted anywhere within the clone!() invocation before the start of the closure, GNU Emacs likes to add at least two levels of indentation to the closure body. It would be nice instead if references to all of the window's widgets and data could be contained within a single ref-counted MainWindow struct which could be cloned and accessible from within the closures. However, ref-counting cycles make this a bit tricky.

If the ref-counted MainWindow struct holds strong references to widgets, and callback closures for the widgets hold strong references to the struct, a RC cycle is born. The way to break RC cycles is to introduce Weak references. If the struct holds weak references to widgets, unfortunately, many unwrap() calls ensue. If the callback closures hold weak references to the MainWindow struct, then there is no natural place to keep at least one strong reference to keep the MainWindow struct from being dropped.

The latter was actually the solution I landed on, except that I keep a strong reference to the MainWindow struct inside a no-op callback to the destroy signal on the window, whose documentation does seem to indicate that this is indeed its intended use case:

impl MainWindow {
  pub fn init(self: Rc<Self>) {
    // ...
    let self_ref = Cell::new(Some(self));
    window.connect_destroy(move |_| drop(self_ref.take()));
  }
}

Put together, the result looks something like this:

pub struct MainWindow {
  window:            gtk::ApplicationWindow,
  foo_list:          gtk::TreeView,
  bar_list:          gtk::TreeView,
  bar_model:         gtk::TreeStore,
  frob_unfrob_stack: gtk::Stack,
  frob_button:       gtk::Button,
  unfrob_button:     gtk:Button,
  bar_data:          Rc<Bar>,
  baz_data:          Rc<Baz>,
  // ...
}

impl MainWindow {
  pub fn init(self: Rc<Self>) {
    // ...
    self.repopulate_selected_foo_model();
    self.unfrob_button.connect_clicked(clone!(@weak self as self_ => move |_unfrob_button| self_.unfrob_button_clicked()));
    // ...

    let window = self.window.clone();
    let last_self = Cell::new(Some(self));
    window.connect_destroy(move |_| drop(last_self.take()));
  }

  fn unfrob_button_clicked(&self) {
    self.bar_data.clear();
    self.frob_unfrob_stack.set_visible_child(&self.frob_button);
    self.repopulate_selected_foo_model();
  }
}

Is this a sane architecture for a gtk-rs application? What are some good alternatives?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.