I have some various idle functions being run that I added with glib::idle_add_local. However, not all of them are required to run at all circumstances. So, how can I make them run only when certain condition is met? For example, only when the user is inside a certain adw::NavigationPage/gtk::StackPage.
Another possible way of making the question could be: how can I add the idle function at a certain moment, and then remove it?
Additional context (in case you’re interested)
I’m building a video calling application in Rust as a project in a college course. I’m building it using GTK and LibAdwaita.
The app is multithreaded, so I listen to the MPSC channel that connect the GUI with the backend: this because I need to be able to react when for example someone is calling me; or if I’m in a video call, I’m going to be receiving the frames from the backend. All of these changes are things I wanna be alert of, but without blocking the main loop. That’s why I chose to use idle functions, but maybe there’s a better way to listen to these events while not blocking the main loop…
I have a adw::NavigationView with adw::NavigationPages (equivalent to gtk::Stack and gtk::StackPage). One for the login; another one for the lobby, and another one of the call itself. I have some idle functions that retrieve new frames from the backend; but I don’t want those idle functions to run if the user is in the lobby or in the login page!
Thanks in advance, and I hope there is a viable solution to this whole thing!
Either only add the idle functions when the conditions are met (and remove them again if the conditions are unmet), or check the conditions at the start of the idle function and return with G_SOURCE_CONTINUE if they’re not met but you want to check again soon. That will result in the idle function being called on every main loop iteration, though, so it’s probably better to go with my first recommendation.
You could also create your own GSource subtype which does the checks in its checkvfunc, but that’s probably overkill for this situation.
Idle functions are the correct way to implement message passing between threads. Make sure that the data they’re passing is idempotent or correctly locked. And make sure that the idle callback doesn’t block too long, as it is run in the main thread and will block GTK’s rendering loop.
So, how can I make them run only when certain condition is met? For example, only when the user is inside a certain adw::NavigationPage/gtk::StackPage.
You add them or remove them as appropriate. If they are often reused, you can also create idle sources and attach/detach them from the mail loop as needed.
That’s why I chose to use idle functions, but maybe there’s a better way to listen to these events while not blocking the main loop…
There might be indeed, especially if the frequency at which you need to run them is high (e.g. matching framerate). It depends on whether you can integrate the “MPSC channel” (I’m not familiar with Rust so I don’t know the details of it) with GLib’s main loop or not. But e.g. it’s easy to integrate UNIX file descriptors, so maybe it fits your use case. Or create a custom GSource, but that’s a bit harder.
For some scenarios, I just did this by making the function return glib::ControlFlow::Break once I got the server’s response. Simpler than expected! IDK how I didn’t though about this before; so thanks for unblocking my thoughts.
For another situation, I used glib::Source::remove; it was a little more complicated since I had to store the idle function’s source ID and add it/remove it each time the adw::NavigationPage signaled that it is being shown/hidden. But worked. Not that bad, that piece of code.
Again, thanks Philip for the answer, you really thanked me!
Thanks, you gave me enough confidence to go full speed ahead with this solution!
The solution
Whoever can let me know if anything can be improved; but here I leave the two different kind of solutions I used for different situations; just in case it’s of use for someone in the future.
sign_in.connect_clicked({
glib::clone!(
#[weak]
username,
#[weak]
password,
#[weak]
backend_sender,
#[weak]
frontend_receiver,
#[weak]
navigation_view,
move |_| {
let username = username.text().to_string();
let password = password.text().to_string();
let _ = backend_sender.send(FrontendToBackendAPI::SignIn { username, password });
let home_page = home_page.clone();
glib::idle_add_local(move || {
if let Ok(response) = frontend_receiver.try_recv() {
match response {
BackendToFrontendAPI::Succeeded => {
navigation_view.push(&home_page);
return glib::ControlFlow::Break;
}
_ => {} //TODO Error handling missing.
}
};
glib::ControlFlow::Continue
});
}
)
});
/// Updates frames as frequently as possible without blocking the main loop;
/// but does so only when the user is inside a call.
fn upd_frames(page: &adw::NavigationPage, picture: >k::Picture, receiver: Rc<Receiver<Mat>>) {
let source_id: Rc<RefCell<Option<glib::SourceId>>> = Rc::new(RefCell::new(None));
// When the call page is shown, add/start the idle function.
page.connect_shown(glib::clone!(
#[weak]
picture,
#[weak]
receiver,
#[weak]
source_id,
move |_| {
let id = glib::idle_add_local(move || upd_frame(&picture, &receiver));
*source_id.borrow_mut() = Some(id);
}
));
// When the call page is hidden, remove/stop the idle function.
page.connect_hidden(move |_| {
if let Some(id) = source_id.borrow_mut().take() {
id.remove();
}
});
}
You’re busy-looping, constantly spinning the CPU only to check if maybe someone has sent a message through the channel at this moment. And then check again. And again. And again. You should make sure the main thread is only waken up when the sender actually has a message do deliver.
If the sender code is also yours, you could instead make the sender call glib::idle_add_once (note the once part) to run the receiving logic (navigation_view.push(&home_page)) on the main thread. No need for a channel.