rust

"closure is capturing" panic when mapping iced Subscription


I am trying to build a simple GUI which simulates a laser game where I let a red point follow an invisible target with deifferent speeds at different momements and escape the mouse cursor when gets close to it.

I used Iced version "0.13.1" as well as fastrand "2.3.0". Now here is the code:

use iced::{Point, Element, Subscription, Event, Rectangle, window, Color, Length,
    Renderer, Theme};
use iced::widget::canvas::{self, Frame, Geometry, Path};
use iced::mouse::Cursor;
use fastrand;
use std::cell::RefCell;

#[derive(Default, Clone, Copy)]
struct LaserPointer {
    x: f32,
    y: f32,
    speed: Speed,
    imaginary_target: Point,
}

#[derive(Clone, Copy, Default)]
enum Speed {
    #[default]
    Still,
    Slow,
    Fast,
    CrazyFast,
}

#[derive(Debug, Clone)]
enum Message {
    MovePointer(Point),
    UpdateFrame(Rectangle),
}
#[derive(Default, Clone)]
struct LaserPointerApp {
    pointer: LaserPointer,
    rectangle: Rectangle,
}
//Implementing From here isn’t essential, but it helps the following code be a bit cleaner.
impl From<LaserPointer> for Point {
        fn from(pointer: LaserPointer) -> Self {
            Point {
                x: pointer.x,
                y: pointer.y,
            }
        }
}
//We define this function because iced does not have an equivalent method
fn get_bottom_right(rect: Rectangle) -> Point {
    Point {
        x: rect.x + rect.width,
        y: rect.y + rect.height,
    }
}

impl LaserPointer {
    fn new() -> Self {
        Self {
        x: 50.0,
        y: 50.0,
        speed: Speed::default(),
        imaginary_target: Point { x: 50.0, y: 50.0 },
        }
    }

    //This method now handles the random laser pointer movement when the mouse arrow gets too close.
    fn random_movement(&mut self, amount: f32) {
        if fastrand::bool() {
            self.x += fastrand::f32() * amount;
            } else {
            self.x -= fastrand::f32() * amount;
            }
            if fastrand::bool() {
                self.y += fastrand::f32() * amount;
            } else {
                self.y -= fastrand::f32() * amount;
            }
    }
    //We don’t want the speed to change too frequently (cats get bored when a laser pointer moves too quickly), so
    //we’ll use a random f32 from 0.0 to 1.0 and only change when the number is greater than 0.98. In practice, this
    //will mean a speed change every few seconds. The following try_change_target() changes the invisible target
    //for the pointer in the same way.
    fn try_change_speed(&mut self) {
            use Speed::*;
            if fastrand::f32() > 0.98 {
                self.speed = match fastrand::u8(0..3) {
                    0 => Still,
                    1 => Slow,
                    2 => Fast,
                    _ => CrazyFast,
            }
        }
    }
    fn try_change_target(&mut self, rect: Rectangle) {
        let bottom_right = get_bottom_right(rect);
        if fastrand::f32() > 0.98 {
            self.imaginary_target = Point {
            x: fastrand::f32() * bottom_right.x,
            y: fastrand::f32() * bottom_right.y,
            }
        }
    }
    fn change_speed(&self) -> f32 {
        match self.speed {
            Speed::Still => 0.0,
            Speed::Slow => 0.05,
            Speed::Fast => 0.1,
            Speed::CrazyFast => 0.3,
        }
    }
    //Finally, we have this method to move the laser pointer once every loop. One of the speeds is 0.0,
    //though, so it will stay absolutely still in that case.
    fn move_self(&mut self) {
        let x_from_target = self.imaginary_target.x - self.x;
        let y_from_target = self.imaginary_target.y - self.y;
        self.x += fastrand::f32() * x_from_target * self.change_speed();
        self.y += fastrand::f32() * y_from_target * self.change_speed();
    }
}

impl LaserPointerApp{
fn new() -> Self {
    LaserPointerApp {
        pointer: LaserPointer::new(),
        rectangle: Rectangle {
            x: 10.0,
            y: 10.0,
            width: 800.0,
            height: 600.0,
        },
    }
}
}

fn subscription(laserpointer: LaserPointerApp) -> Subscription<Message> {
    iced::event::listen().map(move |event| match event {
        Event::Mouse(iced::mouse::Event::CursorMoved{position})=>Message::MovePointer(position),
        Event::Window(window::Event::Resized (resize_info)) => {
            
            Message::UpdateFrame(Rectangle {
                x: 0.0,
                y: 0.0,
                width: resize_info.width as f32,
                height: resize_info.height as f32,
            })
        }
        _ => {
            Message::UpdateFrame(laserpointer.rectangle)}
    })
}

impl canvas::Program<Message> for LaserPointerApp {
    type State = ();

    fn draw(&self, _state: &Self::State, renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) 
    -> Vec<Geometry> {
        let mut frame = Frame::new(renderer,bounds.size());

        // Draw the red laser pointer
        let path = Path::circle(Point::new(self.pointer.x, self.pointer.y), 20.0);
        frame.fill(&path, Color::from_rgb(1.0, 0.0, 0.0));

        vec![frame.into_geometry()]
    }
}

fn update(laserpointer: &mut LaserPointerApp, message: Message) {
    match message {
        Message::MovePointer(pos) => {
        if (laserpointer.pointer.x - pos.x).abs() < 20.0 && (laserpointer.pointer.y - pos.y).abs() < 20.0 {
            laserpointer.pointer.random_movement(50.0);
        }
    },
        Message::UpdateFrame(new_bounds) => {
            laserpointer.rectangle = new_bounds;
            laserpointer.pointer.try_change_speed();
            laserpointer.pointer.try_change_target(laserpointer.rectangle);
            laserpointer.pointer.move_self();

        }
}
}

fn view(laserpointer: &LaserPointerApp) -> Element<Message> {
    iced::widget::Canvas::new(laserpointer)
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

fn main() -> iced::Result {
    let laserpointer = LaserPointerApp::new();
    iced::application("Awesome laser pointer",update,view)
    .subscription(move |app| subscription(laserpointer.clone()))
    .run()
}

After running this, the main thread panicks and the compilation report says the following:

finished `dev` profile [unoptimized + debuginfo] target(s) in 28.23s
     Running `target\debug\laser_pointer.exe`
thread 'main' panicked at C:\Users\hp\.cargo\registry\src\index.crates.io-6f17d22bba15001f\iced_futures-0.13.2\src\subscription.rs:249:9:
the closure laser_pointer::subscription::{{closure}} provided in `Subscription::map` is capturing
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\laser_pointer.exe` (exit code: 101)

It seems there must be a lifetime issue with the subscription function I defined earlier. But I cannot get it well.

I tried to introduce Arc<Mutex<LaserPointerApp>> as the type of subscription function's argument to enable ownership sharing and avoid lifetime issues, but the error report is the same.


Solution

  • The reason you get that error is relatively straight forward. You didn't respect the constraints of Subscription::map:

    The closure provided must be a non-capturing closure.

    That means you cannot reference any local variables in the closure you pass it, laserpointer cannot appear anywhere in that closures body.