I am currently able to make a login-form in Gtk-rs, send the form-data in a POST-request to my server to authenticate, and retrieve the response and session.
But now I also have a label which I want to give the text "success" or "failure" depending on if the authentication was successful.
// Make label
let label = gtk::Label::new(Some(""));
// On button click call async login-function to send a POST-request to server with username and password
login_button.connect_clicked(move | _button| {
let username = username_input.text().as_str().to_string();
let password = password_input.text().as_str().to_string();
tokio::spawn(async move {
match server::login(username, password).await {
true => label.set_text("success"), // SUCCESS
false => label.set_text("failure"), // FAILURE
};
});
});
Compile error:
error: future cannot be sent between threads safely
--> src/main.rs:104:22
|
104 | tokio::spawn(async move {
| ______________________^
105 | | match server::login(username, password).await {
106 | | true => label.set_text("SUCCESS"),
107 | | false => label.set_text("FAILURE"),
108 | | };
109 | | });
| |_________^ future created by async block is not `Send`
|
tasks (i.e. futures spawned on a runtime) is scheduled by the runtime and can be running on any work thread, but gtk requires all UI operations must run on the same thread as the event loop (typically it's the "main" thread)
you should send a notification from the async task to the event loop thread, and update the UI accordingly. you can use the glib::MainContext type to register an callback on the event loop thread.
let (tx, rx) = MainContext::channel(Priority::default());
tokio::spawn(async move {
let successful = server::login(username, password).await;
tx.send(successful).unwrap();
});
// you'll need a weak reference to the widget
// and move the weak reference to the callback
let weak_label = label.downgrade();
rx.attach(None, move |successful| {
let label = weak_label.upgrade().unwrap();
if successful {
label.set_text("success");
} else {
label.set_text("failure");
}
// if this is a one shot event callback, return `Break`
// if this callback need to be called repeatedly, return `Continue`
ControlFlow::Break
});
I want to add, glib::MainContext is itself an async runtime, in certain cases, you might not need the tokio runtime, in which case, the code can be simplified dramatically:
// `MainContext::spawn_local()` doesn't require `Send` bound
// you still need a weak reference though
let weak_label = label.downgrade();
login_button.connect_clicked(move | button| {
let username = username_input.text().as_str().to_string();
let password = password_input.text().as_str().to_string();
MainContext::default()::spawn_local(async move {
match server::login(username, password).await {
true => weak_label.upgrade().unwrap().set_text("success"), // SUCCESS
false => weak_label.upgrade().unwrap().set_text("failure"), // FAILURE
};
});
});
but this won't work if the future depends on some tokio specific features. unfortunately, tokio conflates different concepts (like scheduler/executor, reactor, poller, etc), into its single Runtime type, and many async libraries are tightly coupled with tokio and can't be re-used with other async runtimes (such as glib).
You can use the async-compat crate to solve the tokio problem. If you don't want to add yet another crate I invite you to go look at its source code. It's really short and can be replicated in your own project with even less code if you only care about specific features.