2

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)
    });
Edward Peters
  • 3,623
  • 2
  • 16
  • 39
  • 1
    I've been banging my head on this problem. Why is it so hard in Rust to share values between threads? I've got a perfect solution to this question that works in Python, but not in Rust! – Sylvester Kruin Mar 28 '22 at 15:03
  • Have you tried putting `win` in a Mutex? – PitaJ Mar 28 '22 at 15:22
  • @SylvesterKruin I mean, in general the whole point of Rust is to get both speed and safety, by having very strict compile time checks for thread/memory safety. Python has way more baggage carried over into runtime (garbage collection, etc), and way lower safety. – Edward Peters Mar 28 '22 at 15:23
  • 1
    I would recommend adding the specific compiler error into your question. – BobMorane Mar 28 '22 at 15:41
  • 1
    @BobMorane The specific error depends on what I attempt, but there's already one there (in the commented out section, starting with "fails with"). Edited to point that out in the question body as well. That said, I'm not so much trying to fix a specific error as discover the correct way - what I'm doing might be 100% wrong-headed. – Edward Peters Mar 28 '22 at 15:43
  • @PitaJ Just tried, got the same error which makes me think I'm doing it wrong. Edited my attempt into the question – Edward Peters Mar 28 '22 at 15:47
  • Have you tried with just `Mutex`, not `Arc`? – PitaJ Mar 28 '22 at 16:02
  • 1
    @PitaJ yeah actually, tried both – Edward Peters Mar 28 '22 at 16:03

2 Answers2

1

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.

Sebastian Dröge
  • 2,063
  • 11
  • 9
0

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));
    }
}
Edward Peters
  • 3,623
  • 2
  • 16
  • 39
  • Dear Lord... how complicated for something so simple... at least it works and you can go on. Anyways, I would suggest you accept your own answer if it solves your issue! – BobMorane Mar 31 '22 at 12:10