rustgtkgtk-rs

How to access state and template children in callback in GTK-rs


The tutorials and examples for gtk-rs are honestly really incomplete and spotty, so I'm trying to piece together how to modify the application's state, as well as the state of some of the child elements, inside a button callback. So, in brief, I have:

// ...
mod imp {
    pub struct Window {
        #[template_child]
        pub headerbar: TemplateChild<gtk::HeaderBar>,
        #[template_child]
        pub open_button: TemplateChild<gtk::Button>,

        // Internal state    
        pub state: Rc<RefCell<ScribeDownWindowState>>,
    }

    #[derive(Default)]
    pub struct ScribeDownWindowState {
        pub project_path: Option<String>,
    }
}

In the ObjectImpl for this struct, I have the constructed method, which calls the parent constructed method, then calls setup_callbacks on the parent object, which is the Window type that actually is part of the GTK inheritance hierarchy:

mod imp;
glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap;
}

impl Window {
    pub fn new<P: glib::IsA<gtk::Application>>(app: &P) -> Self {
        glib::Object::new(&[("application", app)]).expect("Failed to create ScribeDownWindow")
    }

    fn setup_callbacks(&self) {
        let state = self.imp().state;
        let headerbar = Rc::new(&self.imp().headerbar);
        self.imp().open_button
            .connect_clicked(clone!(@strong state, @strong headerbar => move |_| {
                let s = state.borrow_mut();
                s.project_path = Some("fuck".to_string());
                headerbar.set_subtitle(Some("fuck"));
            }))
    }
}

I need to access both the state and headerbar properties of the imp::Window struct, and modify the project_path property of state and call set_subtitle on the headerbar. I've tried all sorts of variations of this, using all combinations of variables and Rcs and RefCells and I just cannot seem to get past this error (or some permutation of it):

error[E0759]: `self` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
  --> src/scribedown_window/mod.rs:22:39
   |
20 |     fn setup_callbacks(&self) {
   |                        ----- this data with an anonymous lifetime `'_`...
21 |         let state = self.imp().state;
22 |         let headerbar = Rc::new(&self.imp().headerbar);
   |                                  ---- ^^^
   |                                  |
   |                                  ...is captured here...
23 |         self.imp().open_button.connect_clicked(
   |                                --------------- ...and is required to live as long as `'static` here

There has to be a way to get what I need done done, if you couldn't modify any other interface objects inside a button click callback your UI would be seriously hindered, but I don't see how.


Solution

  • The solution to this problem was to create a struct to hold both the UI state and the application state, like so:

    pub struct App {
        pub window: crate::scribedown_window::Window,
        pub state: State,
        pub document_list_model: Option<document_list::Model>,
    }
    

    With this struct in hand, you can wrap it in an Rc<RefCell<T>> so that other threads and scopes can access it (just not thread-safely/at the same time, you need a Mutex or Arc for that):

    let scribedown = Rc::new(RefCell::new(app::App {
        window: win,
        state: app::State {
            project: None,
            open_files: vec![],
        },
        document_list_model: None,
    }));
    

    Now, you can just pass a reference counted pointer to this central state holder to all the callbacks you want, and the callbacks themselves will keep the state alive and keep access to it, while also enforcing a crash if multiple callbacks try to modify the RefCell at the same time. Note that for this to work, all the methods to set up the application's UI callbacks need to be passed the reference counted state variable, scribedown, so they can't be methods of the App struct taking &self, since that would be useless and borrow it. They can be static methods though:

    app::App::connect_all(scribedown.clone());
    

    Then, to wire up the callbacks, each callback needs its own pointer to the state to use, and on top of that, since you can't move a cloned reference counted pointer out of the enclosing scope, and you don't want to move the original reference counted pointer out, you need to create an outer RCP to then use to clone the actual RCP for the callback. That ends up looking like this:

    // NOTE: the outer pointers to `sd`, formatted like `sd_for_*`, are
    // done in order to prevent the callback from borrowing the original
    // pointer when it creates its own pointer, which we need to keep free
    // to continue making more pointers. This happens because using
    // something inside a `move` callback borrows it.
    
    // Connect open button callback
    let sd_for_button = sd.clone();
    {
        let osd = sd.borrow();
        let button = &osd.window.imp().open_button;
        button.connect_clicked(move |_| {
            // Launch dialog in new thread so it doesn't hang this one
            gtk::glib::MainContext::default()
        .spawn_local(Self::open_project_dialog(Rc::clone(&sd_for_button)));
        });
    }
    

    I'm not sure this is the "official" or idiomatic solution to this, but I looked at the source code for Fractal (a Matrix messenger client written in Rust with GTK-rs), and they seemed to be using a similar solution.