Search code examples
rustgtkborrow-checker

Rust: Why can't I move a value twice?


New to Rust, really feel like I'm missing something here.

In the below code, there are two nested closures which are important to the question: app.connect_activate and draw_area.connect_draw. I am trying to use a channel to send values into the connect_draw closure, so that what is drawn will reflect the state of a simulation that changes over time (currently represented by random colors).

What I've found is very weird to me and I do not understand at all:

  • if I create the channel within connect_activate, I can call rx.recv within connect_draw (this is the first example below)
  • if I create the channel outside of connect_activate, I can call rx.recv within connect_activate (example omitted for brevity)
  • if I create the channel outside of connect_activate, I cannot call rx.recv within connect_draw; it fails with cannot move out of rx, a captured variable in an Fn closure (this is the second example below)

In other words: I can move the created channel across either closure boundary, but not both. It can move from the scope of main into the scope of connect_activate, or from the scope of connect_activate into the scope of connect_draw, but it cannot do both in sequence. Why would this be the case? If I can move it into connect_activate, shouldn't it then be just as "owned" as if it were created there? Both cases do seem to be truly "moved", insofar as they fail if I omit the "move" keyword. What's going on here?

Bonus question: There's a hideous placeholder where I'm re-drawing every time I get any event, because I still don't know how to run events off a timer. Halp?

Example 1: (WORKS)

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();
    app.connect_activate(move |app| {
        let (tx, rx ) : (Sender<f64>, Receiver<f64>)= mpsc::channel();
        let draw_area = DrawingArea::new();
        let _id = draw_area.connect_draw(move |_unused, f| {
            let red = rx.recv().unwrap();
            let green = rx.recv().unwrap();
            let blue = rx.recv().unwrap();
            f.set_source_rgb(red,green, blue);
            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.show_all();
        
        win.connect_event(|w, _g|{ //Placeholder until I learn to make queue_draw fire on a timer
            w.queue_draw();
            Inhibit(false)
        });
        thread::spawn(move || {
            loop {
                thread::sleep(time::Duration::from_millis(100));
                tx.send(rand::random::<f64>()).unwrap();
            }
        });
    });
    app.run();
}

Example 2 (DOES NOT WORK)

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();
    app.connect_activate(move |app| {
        let draw_area = DrawingArea::new();
        let _id = draw_area.connect_draw(move |_unused, f| {
            let red = rx.recv().unwrap();
            let green = rx.recv().unwrap();
            let blue = rx.recv().unwrap();
            f.set_source_rgb(red,green, blue);
            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.show_all();
        
        win.connect_event(|w, _g|{ //Placeholder until I learn to make queue_draw fire on a timer
            w.queue_draw();
            Inhibit(false)
        });
    });
    thread::spawn(move || {
        loop {
            thread::sleep(time::Duration::from_millis(100));
            tx.send(rand::random::<f64>()).unwrap();
        }
    });
    app.run();
}

Solution

  • A gtk application's connect_activate() method takes in a Fn parameter, meaning it can be called multiple times. You cannot move a captured variable out of a Fn closure since the variable will have been moved next time you tried to call it. The first example works because it only moves a local variable that would be created each time.