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

Viewed 140

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)
    });
2 Answers

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.

Okay, I eventually made it work, after stumbling onto gtk-rs: how to update view from another thread . The key is to stash window in a thread-local global (TBH I don't really understand what that means but it works), and then access it through a static function.

I had to modify the linked answer a bit because of scope disagreements between my channel and my window. Eventually I just decided to deal with them separately.

I strongly suspect this is not the right way to do this, but at least it runs.

extern crate cairo;
extern crate rand;
extern crate gtk;
extern crate gdk;
extern crate glib;

use std::sync::{Arc, Mutex};
use std::{thread, time, u32};
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, DrawingArea};
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::cell::RefCell;

const SIZE : usize = 400;
type Message = (usize, usize);
type Grid = [[bool; SIZE]; SIZE];

thread_local!(
    static GLOBAL: RefCell<Option<ApplicationWindow>> = RefCell::new(None);
);

fn check_update_display(){
    GLOBAL.with(|global|{
        if let Some(win) = &*global.borrow() {
            win.queue_draw();
        }
    })
}

fn main(){
    let app = Application::builder()
        .application_id("org.example.HelloWorld")
        .build();
    let (tx, rx ) : (Sender<Message>, Receiver<Message>) = mpsc::channel();
    gtk::init().expect("GTK init failed");
    let draw_area = DrawingArea::new();

    let grid_mut = Arc::new(Mutex::new([[false; SIZE]; SIZE]));

    let draw_grid_mut = Arc::clone(&grid_mut);
    let _id = draw_area.connect_draw(move |_unused, f| {
        let grid = *(draw_grid_mut.lock().unwrap());
        f.set_source_rgb(0.0,0.0, 0.0);
        f.paint().expect("Painting failed");
        f.set_source_rgb(1.0,1.0, 1.0);
        let mut count = 0;
        for i in 0 .. SIZE{
            for j in 0 .. SIZE {
                if grid[i][j] {
                    count = count + 1;
                    f.move_to(i as f64, j as f64);
                    f.rectangle(i as f64 * 3.0, j as f64 * 3.0 , 1.0, 1.0);
                }
            }
        }
        f.stroke().unwrap();
        Inhibit(false)
    });

    let reader_grid = Arc::clone(&grid_mut);
    thread::spawn(move ||{
        loop{
            let mut g = reader_grid.lock().unwrap();
            let (x, y) = rx.recv().unwrap();
            g[x][y] = true;
            drop(g);
            thread::sleep(time::Duration::from_millis(10));
        }
    });

    app.connect_activate(move |app| {
        let win =ApplicationWindow::builder()
            .application(app)
            .default_width(320)
            .default_height(200)
            .title("steveburg")
            .build();
        win.add(&draw_area);
        win.show_all();

        GLOBAL.with(|global|{
            *global.borrow_mut() = Some(win);
        });

        glib::timeout_add_seconds(1, move ||{
            check_update_display();
            Continue(true)
        });
    });


    thread::spawn(move || {
        steveburg(tx);
    });
    app.run();
}

fn random_pair() -> (i32, i32) {
    let (x, y) = ((rand::random::<u32>() % 3) as i32 - 1, (rand::random::<u32>() % 3) as i32 - 1);
    (x, y)
}

fn steveburg(tx : Sender<Message>){
    let mut grid : Grid = [[false; SIZE]; SIZE];
    loop{
        let (mut x, mut y) = (SIZE/2, SIZE/2);
        'drift: loop {
            if x == 0 || x == SIZE - 1 || y == 0 || y == SIZE - 1 {
                break 'drift;
            }
            for nx in 0 .. 3 {
                for ny in 0 .. 3 {
                    if grid[x + nx -1][y + ny -1] {break 'drift}
                }
            }
            let (xa, ya) = random_pair();
            (x, y) = ((x as i32+ xa) as usize, (y as i32 + ya) as usize);
        }
        grid[x][y] = true;
        tx.send((x, y)).unwrap();
        thread::sleep(time::Duration::from_millis(10));
    }
}
Related