Iced框架映射Subscription触发‘closure is capturing’ panic求助
问题:Iced GUI激光指针程序触发Panic
我正在构建一个模拟激光游戏的简单GUI,红点会在不同时刻以不同速度跟随隐形目标,靠近鼠标光标时会逃离。使用Iced 0.13.1和fastrand 2.3.0,运行代码时主线程触发panic,错误提示:
thread 'main' panicked at ...: the closure laser_pointer::subscription::{{closure}} provided in `Subscription::map` is capturing
怀疑自定义subscription函数存在生命周期问题,尝试用Arc<Mutex<LaserPointerApp>>共享所有权但无效,求解决方案。
完整错误信息
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)
原代码
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] Slow, Fast, CrazyFast, Still, } #[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() }
解决方案
问题根源
panic的核心原因是subscription函数中传入的LaserPointerApp实例被闭包捕获,但Iced的Subscription::map要求闭包必须满足'static生命周期,同时不能捕获可能导致所有权冲突的变量。原代码中,闭包在_分支引用了laserpointer.rectangle,这种捕获方式会让闭包的生命周期与laserpointer绑定,不符合Iced订阅系统的要求。
另外,原代码试图通过订阅所有事件来触发指针更新,这种设计不合理——应该用定时订阅来驱动指针的持续移动,而不是依赖无关事件。
修复步骤
- 移除订阅中对
LaserPointerApp的捕获:订阅只负责传递鼠标移动、窗口调整事件,不需要持有应用实例。 - 添加定时更新订阅:用
iced::time::every创建定时事件,驱动指针的移动、速度和目标更新。 - 调整Message枚举:新增
Tick消息用于定时更新。 - 重构update逻辑:将指针的自动更新逻辑移到
Tick消息分支中。
修复后的完整代码
use iced::{ time::{self, Duration}, Point, Element, Subscription, Event, Rectangle, window, Color, Length, Renderer, Theme, }; use iced::widget::canvas::{self, Frame, Geometry, Path}; use iced::mouse::Cursor; use fastrand; #[derive(Default, Clone, Copy)] struct LaserPointer { x: f32, y: f32, speed: Speed, imaginary_target: Point, } #[derive(Clone, Copy, Default)] enum Speed { #[default] Slow, Fast, CrazyFast, Still, } #[derive(Debug, Clone)] enum Message { MovePointer(Point), UpdateFrame(Rectangle), Tick, // 新增定时更新消息 } #[derive(Default, Clone)] struct LaserPointerApp { pointer: LaserPointer, rectangle: Rectangle, } impl From<LaserPointer> for Point { fn from(pointer: LaserPointer) -> Self { Point { x: pointer.x, y: pointer.y, } } } 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 }, } } 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; } } 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, } } 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() -> Subscription<Message> { Subscription::batch([ iced::event::listen().map(|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::Tick, // 无关事件不触发UpdateFrame,改用Tick驱动 }), // 定时订阅,每16ms触发一次(约60帧) time::every(Duration::from_millis(16)).map(|_| Message::Tick), ]) } 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()); 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; } Message::Tick => { // 在定时tick中处理指针的自动更新逻辑 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 { iced::application("Awesome laser pointer", update, view) .subscription(|_| subscription()) // 不再传递应用实例给订阅 .run() }
关键修改说明
- 新增
Tick消息:用于定时驱动指针的移动、速度和目标更新,避免依赖无关事件。 - 重构
subscription函数:不再接收LaserPointerApp参数,改用Subscription::batch合并事件监听和定时订阅,闭包不再捕获应用实例,满足'static要求。 - 调整
update逻辑:将指针自动更新的逻辑从UpdateFrame移到Tick分支,窗口调整仅更新边界。 - 移除不必要的
RefCell:原代码中RefCell未被使用,直接删除。
内容的提问来源于stack exchange,提问作者karim abousselham
相关产品推荐
相关产品推荐

