1

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.

Alexis Dumas
  • 1,299
  • 11
  • 30
  • I don't know the correct answer to this either, but I have had some success using both [`mpsc`](https://doc.rust-lang.org/std/sync/mpsc/) and [`glib::MainContext::channel()`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.channel) depending on what was needed. – Herohtar Feb 09 '22 at 20:47
  • @Herohtar I've seen the latter used in the context of opening a dialog box (which I also need to do) but outside of that I'm not precisely sure how it relates to this. I'll investigate, thank you! – Alexis Dumas Feb 09 '22 at 20:54

2 Answers2

0

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.

Alexis Dumas
  • 1,299
  • 11
  • 30
  • 1
    `Rc> like a proper multithreaded pointer, so that multiple threads can access it:` That does not allow multiple threads to access the data, but only a single thread. You'd need to use `Arc` and `Mutex` instead for multiple threads to work. – Sebastian Dröge Feb 16 '22 at 08:16
  • @SebastianDröge I meant so that Rust would let me access it from multiple threads (it does, all GTK event callbacks are launched in a new thread), not so that it would be thread safe in the way Arc or Mutex is, so let me change that. – Alexis Dumas Feb 16 '22 at 21:27
  • All GTK signals are emitted from the same thread and it's not allowed (and also not possible) to use any GTK from other threads than the main thread. – Sebastian Dröge Feb 17 '22 at 07:14
0

You can clone self itself for the closure:

fn setup_callbacks(&self) {
    self.imp().open_button
        .connect_clicked(clone!(@weak self as this => move |_| {
            this.imp().state.borrot_mut().project_path = Some("nice".to_string());
            this.imp().headerbar.set_subtitle(Some("nice"));
        }));
}
Paulo Lieuthier
  • 426
  • 7
  • 14