rustgtk-rsrust-futures

Updating UI elements from a async loop


What is the standard method for updating UI elements from a loop calling a web request in Rust using gtk-rs? I am having trouble updating a label in a UI element to display some data from a web request.

Is there a standard way of doing this? The issues I have been having at the moment have all been related to passing data between threads and as such I am curious to know what is the common way of doing this?

I have a code example here where I get an error stating futures are not Send. I am using fragile in order to pass a box into the thread.

At the moment I am trying to solve the issue of making my futures Send however I am not sure that will solve the issue or just leave me with another similar problem.

use gtk::prelude::*;
use gtk::Orientation;
use adw::Application;
use std::thread;
use std::time::Duration;

use fragile;

const APP_ID: &str = "org.currency_trades";

fn main() {
    let app = Application::builder().application_id(APP_ID).build();
    app.connect_activate(build_ui);
    app.run();
}

pub fn build_ui(app: &Application) -> gtk::Box {
    let home_box = fragile::Fragile::new(gtk::Box::new(Orientation::Vertical, 15));
    
    thread::spawn(move || {
        let box_gbp = gtk::Box::new(Orientation::Horizontal, 250);

        let gbp_label  = gtk::Label::builder()
            .label("GBP")
            .margin_top(12)
            .margin_start(50)
            .build();
        
        let gbp_price_label  = gtk::Label::builder()
            .label("Uninitialized")
            .margin_top(12)
            .margin_end(50)
            .build();

        box_gbp.append(&gbp_label);
        box_gbp.append(&gbp_price_label);

        home_box.get().append(&box_gbp);

        loop {
            let runtime = tokio::runtime::Runtime::new().unwrap();
            std::thread::sleep(Duration::from_millis(1000));
            let _ = runtime.block_on(runtime.spawn(async move {
                let gbp_label_request = match reqwest::get("https://www.boredapi.com/api/activity").await {
                    Ok(label)  => label,
                    Err(_)     => panic!("Panic!")
                };

                let gbp_label = match gbp_label_request.text().await {
                    Ok(r)  => r,
                    Err(_) => String::from("Unknown")
                };

                gbp_price_label.set_label(&gbp_label);
            }));
        }
    });
    return *home_box.get();
}

The associated error:


error: future cannot be sent between threads safely
   --> src/main.rs:89:52
    |
89  |               let _ = runtime.block_on(runtime.spawn(async move {
    |  ____________________________________________________^
90  | |                 let gbp_label_request = match reqwest::get("https://www.boredapi.com/api/activity").await {
91  | |                     Ok(label)  => label,
92  | |                     Err(_)     => panic!("Panic!")
...   |
100 | |                 gbp_price_label.set_label(&gbp_label);
101 | |             }));
    | |_____________^ future created by async block is not `Send`
    |
    = help: the trait `Sync` is not implemented for `*mut c_void`
note: captured value is not `Send`
   --> src/main.rs:100:17
    |
100 |                 gbp_price_label.set_label(&gbp_label);
    |                 ^^^^^^^^^^^^^^^ has type `gtk4::Label` which is not `Send`
note: required by a bound in `Runtime::spawn`
   --> /Users/andy/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs:192:21
    |
192 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `Runtime::spawn`

For more information about this error, try `rustc --explain E0271`.

Solution

  • There is an example in the gtk-rs event loop documentation that explains how to do this using MainContext::channel.

    let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
    // Connect to "clicked" signal of `button`
    button.connect_clicked(move |_| {
        let sender = sender.clone();
        // The long running operation runs now in a separate thread
        thread::spawn(move || {
            // Deactivate the button until the operation is done
            sender.send(false).expect("Could not send through channel");
            let ten_seconds = Duration::from_secs(10);
            thread::sleep(ten_seconds);
            // Activate the button again
            sender.send(true).expect("Could not send through channel");
        });
    });
    // The main loop executes the closure as soon as it receives the message
    receiver.attach(
        None,
        clone!(@weak button => @default-return Continue(false),
                    move |enable_button| {
                        button.set_sensitive(enable_button);
                        Continue(true)
                    }
        ),
    );