Search code examples
rustgtk

Rust GTK: Trigger ApplicationWindow/DrawingArea redraw on a timer?


I'm sure there's an easy way to do this but I don't know what it is. I have a very basic gtk::{Application, ApplicationWindow, DrawingArea}; setup. I want the DrawingArea::connect_draw closure to be triggered repeatedly on a timer, so it updates according to some changing state. (It would also be cool if it could be actively triggered by other threads, but a timer is fine.)

So far everything I've found that would work on a timer fails because it would mean moving the ApplicationWindow to another thread. (fails with NonNull<GObject> cannot be shared between threads safely) What I have currently triggers redraw on generic events, so if I click my mouse on the window it will redraw, but not do so automatically.

That code is below, but please show me how to make this work?

//BOILER PLATE SCROLL DOWN

extern crate cairo;
extern crate rand;
extern crate gtk;
extern crate gdk;
extern crate glib;
use std::{thread, time};
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, DrawingArea};
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};


fn main(){
    let app = Application::builder()
        .application_id("org.example.HelloWorld")
        .build();
    let (tx, rx ) : (Sender<f64>, Receiver<f64>)= mpsc::channel();
    gtk::init().expect("GTK init failed");
    let draw_area = DrawingArea::new();
    let _id = draw_area.connect_draw(move |_unused, f| {
        let red = rx.recv().unwrap();
        f.set_source_rgb(red,0.5, 0.5);
        f.paint().expect("Painting failed");
        Inhibit(false)
    });
    app.connect_activate(move |app| {
        let win = ApplicationWindow::builder()
            .application(app)
            .default_width(320)
            .default_height(200)
            .title("Hello, World!")
            .build();
        win.add(&draw_area);
        win.show_all();
        
        //IMPORTANT STUFF STARTS HERE

        win.connect_event(|w, _g|{ //HORRIBLE HACK HELP FIX
            w.queue_draw();
            Inhibit(false)
        });

        glib::timeout_add_seconds(1, ||{
            println!("I wish I could redraw instead of printing this line");
            Continue(true)
        });

        //fails with "`NonNull<GObject>` cannot be shared between threads safely" :
        // glib::timeout_add_seconds(1, ||{
        //     win.queue_draw();
        //     Continue(true)
        // });

        //IMPORTANT STUFF ENDS HERE
    });
    thread::spawn(move || {
        loop {
            thread::sleep(time::Duration::from_millis(100));
            tx.send(rand::random::<f64>()).unwrap();
        }
    });
    app.run();
}

EDIT: I tried a mutex version, maybe have implemented it wrong. The following code gives the same error (NonNull<GObject> cannot be shared between threads safely)

    let mut_win = Mutex::new(win);
    let arc_win = Arc::new(mut_win);

    glib::timeout_add_seconds(1, move ||{
        let mut w = arc_win.lock().unwrap();
        (*w).queue_draw();
        Continue(true)
    });

Solution

  • Use glib::timeout_add_seconds_local() instead of the non-local version if you're doing everything on the same thread.

    The generic version requires a Send-able closure and can be called from any thread at any time, calling the closure from your main thread. The local version can only be called from the main thread and panics otherwise.

    By not requiring a Send-able closure, you can move a reference to your widgets into the closure.