Search code examples
multithreadingrustgtk3gtk-rs

gtk-rs: how to update view from another thread


I am creating a UI application with gtk-rs. In that application, I have to spawn a thread to continuously communicate with another process. Sometimes, I have to update the UI based on what happens in that thread. But, I'm not sure how to do this because I am not able to hold a reference to any part of the UI across threads.

Here is the code I tried:

use gtk;

fn main() {
    let application =
        gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default()).unwrap()

    application.connect_activate(|app| {
        let ui_model = build_ui(app);
        setup(ui_model);
    });

    application.run(&[]);
}

struct UiModel { main_buffer: gtk::TextBuffer }

fn build_ui(application: &gtk::Application) -> UiModel {
    let glade_src = include_str!("test.glade");
    let builder = gtk::Builder::new();
    builder
        .add_from_string(glade_src)
        .expect("Couldn't add from string");

    let window: gtk::ApplicationWindow = builder.get_object("window").unwrap();
    window.set_application(Some(application));
    window.show_all();

    let main_text_view: gtk::TextView = builder.get_object("main_text_view")

    return UiModel {
        main_buffer: main_text_view.get_buffer().unwrap(),
    };
}

fn setup(ui: UiModel) {
    let child_process = Command::new("sh")
        .args(&["-c", "while true; do date; sleep 2; done"])
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let incoming = child_process.stdout.unwrap();

    std::thread::spawn(move || {                              // <- This is the part to pay
        &BufReader::new(incoming).lines().for_each(|line| {   //    attention to.
            ui.main_buffer.set_text(&line.unwrap());          //    I am trying to update the
        });                                                   //    UI text from another thread.
    });
}

But, I get the error:

    |       std::thread::spawn(move || {
    |  _____^^^^^^^^^^^^^^^^^^_-
    | |     |
    | |     `*mut *mut gtk_sys::_GtkTextBufferPrivate` cannot be sent between threads safely

This makes sense. I can understand that the Gtk widgets aren't thread safe. But then how do I update them? Is there a way to send signals to the UI thread safely? or is there a way to run the .lines().for_each( loop in the same thread in a way that does not block the UI?

Whatever solution I go with will have to be very high performance. I will be sending much more data than in the example and I want a very low latency screen refresh.

Thanks for your help!


Solution

  • Ok, I solved the problem. For anyone in the future, here is the solution.

    glib::idle_add(|| {}) lets you run a closure from another thread on the UI thread (thansk @Zan Lynx). This would be enough to solve the thread safety issue, but it's not enough to get around the borrow checker. No GTKObject is safe to send between threads, so another thread can never even hold a reference to it, even if it will never use it. So you need to store the UI references globally on the UI thread and set up a communication channel between threads. Here is what I did step by step:

    1. Create a way to send data between threads that does not involve passing closures. I used std::sync::mpsc for now but another option might be better long-term.
    2. Create some thread-local global storage. Before you ever start the second thread, store your UI references and the receiving end of that communication pipeline globally on the main thread.
    3. Pass the sending end of the channel to the second thread via a closure. Pass the data you want through that sender.
    4. After passing the data through, use glib::idle_add() -- not with a closure but with a static function -- to tell the UI thread to check for a new message in the channel.
    5. In that static function on the UI thread, access your global UI and receiver variables and update the UI.

    Thanks to this thread for helping me figure that out. Here is my code:

    extern crate gio;
    extern crate gtk;
    extern crate pango;
    
    use gio::prelude::*;
    use gtk::prelude::*;
    use std::cell::RefCell;
    use std::io::{BufRead, BufReader};
    use std::process::{Command, Stdio};
    use std::sync::mpsc;
    
    fn main() {
        let application =
            gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default())
                .unwrap();
    
        application.connect_activate(|app| {
            let ui_model = build_ui(app);
            setup(ui_model);
        });
    
        application.run(&[]);
    }
    
    struct UiModel {
        main_buffer: gtk::TextBuffer,
    }
    
    fn build_ui(application: &gtk::Application) -> UiModel {
        let glade_src = include_str!("test.glade");
        let builder = gtk::Builder::new();
        builder
            .add_from_string(glade_src)
            .expect("Couldn't add from string");
    
        let window: gtk::ApplicationWindow = builder.get_object("window").unwrap();
        window.set_application(Some(application));
        window.show_all();
    
        let main_text_view: gtk::TextView = builder.get_object("main_text_view").unwrap();
    
        return UiModel {
            main_buffer: main_text_view.get_buffer().unwrap(),
        };
    }
    
    fn setup(ui: UiModel) {
        let (tx, rx) = mpsc::channel();
        GLOBAL.with(|global| {
            *global.borrow_mut() = Some((ui, rx));
        });
        let child_process = Command::new("sh")
            .args(&["-c", "while true; do date; sleep 2; done"])
            .stdout(Stdio::piped())
            .spawn()
            .unwrap();
    
        let incoming = child_process.stdout.unwrap();
    
        std::thread::spawn(move || {
            &BufReader::new(incoming).lines().for_each(|line| {
                let data = line.unwrap();
                // send data through channel
                tx.send(data).unwrap();
                // then tell the UI thread to read from that channel
                glib::source::idle_add(|| {
                    check_for_new_message();
                    return glib::source::Continue(false);
                });
            });
        });
    }
    
    // global variable to store  the ui and an input channel
    // on the main thread only
    thread_local!(
        static GLOBAL: RefCell<Option<(UiModel, mpsc::Receiver<String>)>> = RefCell::new(None);
    );
    
    // function to check if a new message has been passed through the
    // global receiver and, if so, add it to the UI.
    fn check_for_new_message() {
        GLOBAL.with(|global| {
            if let Some((ui, rx)) = &*global.borrow() {
                let received: String = rx.recv().unwrap();
                ui.main_buffer.set_text(&received);
            }
        });
    }