From f07d6e19fca7cc16da7bfef31e5f4f5db2f9946e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 16 Oct 2026 23:25:59 +0300 Subject: [PATCH] Save: don't wait forever for the thumbnail frame or screenshot A save from the active drawing first redraws the viewport without its overlays and screenshots it for the file's preview. When the compositor delivered no frame or never answered the screenshot (an occluded or throttled Wayland window), the save never started: the input layer stayed hidden, so the window ignored the mouse and keyboard, and automation requests waited on the save forever. A timer now stands in for the frame, and the screenshot races a half-second timeout; whichever answers first runs the save, without the refreshed preview if it has to. Closes #1801 Co-Authored-By: Claude Opus 5.5 --- src/app/update/file.rs | 41 +++++++++++++++++++++++++++++++++-------- src/app/view/mod.rs | 8 +++++++- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index c4a7b38bc..2cd0935b8 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -3578,14 +3578,39 @@ impl OpenCADStudio { ) }; match capture_window { - Some(window) => iced::window::screenshot(window) - .map(Some) - .then(move |screenshot| { - Task::batch([ - Task::done(Message::ThumbnailCaptureFinished), - run_save(screenshot), - ]) - }), + // The screenshot may never come back (a window the compositor + // does not draw), so it races a short timeout: whichever answers + // first runs the save, with or without the picture. + Some(window) => { + fn finish( + slot: &std::sync::Mutex>, + screenshot: Option, + ) -> Task + where + F: FnMut(Option) -> Task, + { + match slot.lock().ok().and_then(|mut run| run.take()) { + Some(mut run_save) => Task::batch([ + Task::done(Message::ThumbnailCaptureFinished), + run_save(screenshot), + ]), + None => Task::none(), + } + } + let slot = std::sync::Arc::new(std::sync::Mutex::new(Some(run_save))); + let (shot_slot, timeout_slot) = (slot.clone(), slot); + let (tx, rx) = iced::futures::channel::oneshot::channel::<()>(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = tx.send(()); + }); + Task::batch([ + iced::window::screenshot(window) + .map(Some) + .then(move |screenshot| finish(&shot_slot, screenshot)), + Task::perform(rx, |_| ()).then(move |_| finish(&timeout_slot, None)), + ]) + } None => { self.thumbnail_capture_clean = false; run_save(None) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 3e767f6e2..e2f9936d4 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2861,8 +2861,14 @@ impl OpenCADStudio { } else { Subscription::none() }; + // A compositor that delivers no frame (an occluded or throttled + // window) must not hold the save: a timer goes ahead without it. let thumbnail_capture = if self.thumbnail_capture_clean { - window::frames().map(|_| Message::ThumbnailCaptureFrame) + Subscription::batch([ + window::frames().map(|_| Message::ThumbnailCaptureFrame), + iced::time::every(std::time::Duration::from_millis(300)) + .map(|_| Message::ThumbnailCaptureFrame), + ]) } else { Subscription::none() };