Search code examples
rustgtk

Toggling a bool from within a loop and Fn losure with rust and gtk


I'm trying to make a tic tac toe game with rust and gtk, and in order to swap turns I'm toggling bool when a game button is clicked. Because of the rust/gtk integration, the onclick events run within an Fn closure, so I can't directly edit a mutable reference to the bool.

Based on this post, I moved from a basic bool to a bool within a cell as that has interior mutability, allowing it to be varied from within the closure.

However, the variable gets moved into the closure, and in the next iteration of the loop/usage of xturn I get

--> src/main.rs:45:44
   |
39 |     let xturn = Cell::new(true);
   |         ----- move occurs because `xturn` has type `std::cell::Cell<bool>`, which does not implement the `Copy` trait
...
45 |             current_button.connect_clicked(move |current_button|{
   |                                            ^^^^^^^^^^^^^^^^^^^^^ value moved into closure here, in previous iteration of loop
46 |                 if current_button.label().unwrap() == ""{
47 |                     if xturn.get() {
   |                        ----- use occurs due to use in closure 

Code below

let game_grid: [[Button; 3];3] = [[new_game_button(), new_game_button(), new_game_button()],[new_game_button(), new_game_button(), new_game_button()],[new_game_button(), new_game_button(), new_game_button()]];
let xturn = Cell::new(true);

for i in 0..3{
    for o in 0..3{
        let current_button = &game_grid[i][o];
        grid.attach(current_button, i.try_into().unwrap(), o.try_into().unwrap(), 1, 1);
        current_button.connect_clicked(move |current_button|{
            if current_button.label().unwrap() == ""{
                if xturn.get() {
                    current_button.set_label("X");
                } else{
                    current_button.set_label("O");
                }; 
                xturn.set(!xturn.get());

            } else{
                println!("This spot is taken! Go Again.");
            }
            
        });

    }
}

Creating a reference to each use of xturn by adding & results in the same initial error message

Getting rid of the move at the start of the closure results in this error

error[E0373]: closure may outlive the current function, but it borrows `xturn`, which is owned by the current function

Complete code

use std::cell::Cell;
use gtk4 as gtk;
use gtk::{prelude::*, Label};
use gtk::{Align, Application, ApplicationWindow, Button, Grid};

fn main() {
    let app = Application::builder()
        .application_id("org.beribus.tictactoe")
        .build();

    app.connect_activate(build_ui);

    println!("Hello, world!");
    app.run();
}

fn build_ui(app: &gtk::Application){
    let window = ApplicationWindow::builder()
        .application(app)
        .default_width(360)
        .default_height(360)
        .title("GTK Tac Toe")
        .build();   

    let grid = Grid::builder()
        .margin_top(10)
        .margin_bottom(10)
        .margin_start(10)
        .margin_end(10)
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .row_spacing(6)
        .column_spacing(6)
        .build();

    window.set_child(Some(&grid));

    let game_grid: [[Button; 3];3] = [[new_game_button(), new_game_button(), new_game_button()],[new_game_button(), new_game_button(), new_game_button()],[new_game_button(), new_game_button(), new_game_button()]];
    let xturn = Cell::new(true);
    for i in 0..3{
        for o in 0..3{
            let current_button = &game_grid[i][o];
            grid.attach(current_button, i.try_into().unwrap(), o.try_into().unwrap(), 1, 1);
            current_button.connect_clicked(move |current_button|{
                if current_button.label().unwrap() == ""{
                    if xturn.get() {
                        current_button.set_label("X");
                    } else{
                        current_button.set_label("O");
                    }; 
                    &xturn.set(!&xturn.get());

                } else{
                    println!("This spot is taken! Go Again.");
                }
                
            });

        }
    }
    let text = Label::builder()
        .label("woro")
        .build();

    if xturn.get(){
        text.set_label("yoyooyo it's X's turn");
    } else{
        text.set_label("yoyooyo it's O's turn");
    }
    grid.attach(&text, 0,4,3,1);

    let reset_button = Button::builder()
        .halign(Align::Center)
        .valign(Align::Center)
        .label("Reset Game")
        .build();
    reset_button.connect_clicked(move |_|{
        for i in 0..3{
            for o in 0..3{
                let current_button = &game_grid[i][o];
                current_button.set_label("");
            }
        }
    });
    grid.attach(&reset_button, 0,3,3,1);

    window.show();
}

fn new_game_button() -> Button{
    let button = Button::builder()
        .halign(Align::Center)
        .valign(Align::Center)
        .label("")
        .width_request(90)
        .height_request(90)
        .build();
    button
}

Solution

  • Both approaches are flawed, as the compiler indicated.

    • When you use the move approach, you are telling the closure that it can have xturn, but since this is in a loop, it can execute multiple times. You can't move the same value twice (since it is gone after the first time) and so the second move is disallowed.
    • When you try to capture by reference, the compiler is telling you that xturn belongs to the function in which it is declared, but the closures you are creating in the loop will outlive the function, and therefore the references will become invalid.

    It looks like you want shared ownership of a single bool, which is implemented in Rust by the Rc struct, which performs reference-counting to determine when the shared value can be destroyed.

    However, Rc does not permit mutable borrows while more than one Rc exists for the same shared value, so you still need the interior mutability of Cell. The final type that you want to use is Rc<Cell<bool>>. (RefCell isn't required here since you only need to be able to get and set the bool value; you don't ever need a reference to it.)

    You need each iteration of the loop to move a copy of the Rc. Each separate Rc value will refer to the same Cell. For example, this function returns 9 closures that all do the same thing: they toggle the shared boolean value and return it in a tuple along with their own index:

    fn create_closures() -> Vec<Box<dyn Fn() -> (i32, bool)>> {
        let xturn = Rc::new(Cell::new(false));
        
        (0..9).map(|i| -> Box<dyn Fn() -> (i32, bool)> {
            // Make a clone of the Rc that this closure can steal.
            let xturn = xturn.clone();
            
            Box::new(move || {
                let v = !xturn.get();
                xturn.set(v);
                (i, v)
            })
        }).collect()
    }
    

    (Playground)