Search code examples
rustgtk

Rust: Receive from channel in DrawingArea::connect_draw?


I'm trying to make a program in which a window displays the changing state of a simulation. My idea for how to do this (and I'm very open to other suggestions) is to spawn off a thread which handles the simulation logic, and send messages detailing the state which are read in the draw handler. My current attempt at this looks like:

extern crate cairo;
extern crate rand;
extern crate gtk;
extern crate gdk;
use std::{thread, time};
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, DrawingArea};
use std::sync::mpsc;
fn main(){

    let (tx, rx )= mpsc::channel();

    let app = Application::builder()
        .application_id("org.example.HelloWorld")
        .build();
    app.connect_activate(|app| {
    let draw_area = DrawingArea::new();
    let _id = draw_area.connect_draw(|_unused, f| {
        let red : f64 = rx.recv().unwrap();
        f.set_source_rgb(red, 0.0, 0.0);
        f.paint().expect("Painting failed");
        Inhibit(false)
    });
    let win = ApplicationWindow::builder()
        .application(app)
        .default_width(320)
        .default_height(200)
        .title("Hello, World!")
        .build();
    win.add(&draw_area);

    win.connect_event(|w, _g|{ //Placeholder until I learn to make queue_draw fire on a timer
        w.queue_draw();
        Inhibit(false)
    });
    win.show_all();
    });
    thread::spawn(move || {
        for _i in 0..9 {
            thread::sleep(time::Duration::from_millis(1000));
            tx.send(rand::random::<f64>()).unwrap();
        }

    });
    app.run();
}

This fails because rx does not live long enough. I think I understand why the compiler thinks that, but I'm not clear how to resolve the issue, or if this is a viable approach to begin with.

As a bonus question, there's an absolute hack in this code with the win.connect_event call, which lets me trigger a redraw by any action. I want that to just trigger at regular intervals, but I haven't found a way to do that yet - any help would be very appreciated.


Solution

  • I eventually figured out a way (asking Rust: Why can't I move a value twice? ) along the way. Describing what I learned:

    • rx must be moved into a closure that uses it, if the closure might outlive the original scope in which rx was created. (This happens twice in the code in the question).
    • This code has two nested closures, with rx created outside of each. You can "give" rx to the outer closure, but it cannot then give it to the inner one, because the outer one might be called repeatedly (for all rust knows). If it gives away its only copy, it might not have one for second calls, which would be unsafe.

    My solution has been to define the connect_draw enclosure outside of the connect_activate closure. Note that this solution still isn't perfect as I haven't figure dout how to trigger connect_draw on a timer like a grown up, but the channel issue seems to be solved:

    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();
    
            win.connect_event(|w, _g|{ //HORRIBLE HACK HELP FIX
                w.queue_draw();
                Inhibit(false)
            });
        });
        thread::spawn(move || {
            loop {
                thread::sleep(time::Duration::from_millis(100));
                tx.send(rand::random::<f64>()).unwrap();
            }
        });
        app.run();
    }