//! Progress and Cancel of an export running off the UI thread (Background Export), on the shared //! [`vectorcraft_doc::cancel`] mechanism. //! //! The frontend makes a [`Progress`], runs the export inside [`scope`] on its worker and reads the //! counts from the UI thread to draw them; its Cancel button calls [`Progress::cancel`]. Inside the //! scope the encoders that work in steps (raster formats: one artboard at a time) report each step //! through [`vectorcraft_doc::cancel::progress`] and stop at the next check once cancelled; the //! renderer stops drawing and keeps no half-drawn pattern tile or effect raster. Encoders that //! make one file in one go report nothing: the frontend shows them as busy and checks //! [`Progress::cancelled`] itself before writing, so a cancelled export never writes a file. //! Export for Screens ([`super::ScreenExport::write`]) reports one step per file instead, keeps //! its encoders' reports out, and stops before the next file, so the files it wrote before the //! cancel stay. use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use vectorcraft_doc::cancel::{self, CancelToken, ProgressSink}; /// How far an export has got, shared between the worker and the UI thread. #[derive(Debug, Default)] pub struct Progress { token: CancelToken, done: AtomicU64, total: AtomicU64, /// The steps are files (Export for Screens), not artboards. files: bool, /// The last report came from a plug-in ([`vectorcraft_plugins::host::PROGRESS_STEP`]): its /// steps are its own, shown as a share of the work. share: AtomicBool, } impl Progress { pub fn new() -> Arc { Arc::new(Self::default()) } /// A [`Progress`] whose steps are files: the export of [`super::ScreenExport::write`]. pub fn of_files() -> Arc { Arc::new(Self { files: true, ..Self::default() }) } /// Are the steps files rather than artboards? pub fn counts_files(&self) -> bool { self.files } /// Are the steps a plug-in's own, which only a share of the work done describes? pub fn counts_share(&self) -> bool { self.share.load(Ordering::Relaxed) } /// `(steps done, steps in all)`; `total` is 0 while the export can't say (one file made in one go). pub fn counts(&self) -> (u64, u64) { let total = self.total.load(Ordering::Relaxed); (self.done.load(Ordering::Relaxed).min(total), total) } /// The share of the export done, 0 to 1; `None` while the export can't say. pub fn fraction(&self) -> Option { let (done, total) = self.counts(); (total > 0).then(|| done as f64 / total as f64) } /// Ask the export to stop: it writes nothing more, and a render under way stops drawing. pub fn cancel(&self) { self.token.cancel(); } pub fn cancelled(&self) -> bool { self.token.is_cancelled() } } /// Run `f` with `progress`'s token in effect on this thread and the steps reported to it. pub fn scope(progress: &Arc, f: impl FnOnce() -> R) -> R { let to = Arc::clone(progress); let sink: ProgressSink = Rc::new(move |done, total, what| { to.share.store(what == vectorcraft_plugins::host::PROGRESS_STEP, Ordering::Relaxed); to.total.store(total, Ordering::Relaxed); to.done.store(done, Ordering::Relaxed); }); cancel::scope(&progress.token, Some(sink), f) } #[cfg(test)] mod tests { use super::*; #[test] fn a_raster_export_counts_its_artboards_and_stops_once_cancelled() { let mut doc = vectorcraft_doc::Document::new(100.0, 100.0); let second = doc.artboards[0].clone(); doc.artboards.push(second); let p = Progress::new(); assert_eq!(p.fraction(), None, "nothing reported yet"); let png = scope(&p, || super::super::encode_all(&doc, "png", &serde_json::json!({"useArtboards": true}))); assert!(png.is_ok(), "{:?}", png.err()); assert_eq!(p.counts(), (1, 2), "the step under way when the last artboard started"); p.cancel(); let r = scope(&p, || super::super::encode_all(&doc, "png", &serde_json::json!({"useArtboards": true}))); assert!(matches!(r, Err(crate::EngineError::Cancelled)), "{:?}", r.err()); assert!(!cancel::is_cancelled(), "the scope ended"); } /// A plug-in's reports count as a share of the work, not as artboards or files. #[test] fn a_plugin_s_reports_count_as_a_share_of_the_work() { let p = Progress::new(); scope(&p, || cancel::progress(1, 4, "artboard")); assert_eq!((p.counts(), p.counts_share()), ((1, 4), false)); scope(&p, || cancel::progress(3, 8, vectorcraft_plugins::host::PROGRESS_STEP)); assert_eq!((p.counts(), p.counts_share()), ((3, 8), true)); } }