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 Rc
s 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.
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.