Files
vectorcraft/crates/ui-egui/src/background.rs
T
dotCooCoo 580aa8daa5 Clipboard: Copy and Cut make the system clipboard's formats on the background thread
Copy and Cut made the system clipboard's formats in the frame that ran
them. The formats include the SVG markup and a PNG of the selection at
1 px per pt, up to 4,096 px a side. In a release build on Windows, Copy
of 50,002 rectangles on a 1600 x 1200 pt page took 1.00 s in its frame.

The thread that writes background saves and exports now makes the
formats from a copy of the clipboard, and the app writes them in a
later frame. The same Copy takes 0.18 s in its frame, and the SVG and
PNG are the same bytes. While that thread has a save, an export, a print or
a recovery copy to finish, Copy and Cut make and write the formats at
once. A later Copy or Cut, or text copied in a field or with the Type
tool, drops formats that are not written yet. Until they are written,
Paste on the canvas takes the internal clipboard, and while a text
field or the Type tool has the keyboard the app waits for them.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-16 04:04:51 +07:00

181 lines
7.3 KiB
Rust

//! Background Save and Background Export (Preferences → File Handling): a save or an export encodes
//! and writes a snapshot of the document on a worker thread while editing goes on. The status bar
//! shows what is in progress; the result is applied on the UI thread when it arrives (a save marks
//! the document saved as of its snapshot, so edits made meanwhile keep it modified).
//!
//! It needs a thread-safe writer from the host ([`crate::Services::write_shared`], desktop);
//! without one (the web, which downloads) the work runs at once, as with the preference off.
use std::sync::mpsc::{Receiver, Sender, channel};
use serde_json::{Value, json};
use vectorcraft_engine::file_access::ScopeHandle;
use crate::VectorcraftApp;
/// Writes a file; callable from any thread.
pub type SharedWriteFn = std::sync::Arc<dyn Fn(&str, &[u8]) -> Result<(), String> + Send + Sync>;
/// Writes the file at a path with what the filler it is given writes into it (atomically, as
/// `vectorcraft_format::write_atomic_with`); callable from any thread.
pub type SharedStreamFn =
std::sync::Arc<dyn Fn(&str, &mut dyn FnMut(&mut dyn std::io::Write) -> std::io::Result<()>) -> Result<(), String> + Send + Sync>;
/// A writer for the work: the host's file writer (or browser download) on the UI thread, the
/// shared one on the worker.
pub type Writer<'a> = &'a mut dyn FnMut(&str, &[u8]) -> Result<(), String>;
/// Encodes and writes a snapshot (no access to the app) → the result handed to [`Then`].
type Work = Box<dyn FnOnce(Writer) -> Result<Value, String> + Send>;
/// Applies a result on the UI thread (document state, status, recent files) → the command result.
type Then = Box<dyn FnOnce(&mut VectorcraftApp, Result<Value, String>) -> Result<Value, String>>;
/// A job's id and the work's result, back from the worker.
type Done = (u64, Result<Value, String>);
struct Task {
id: u64,
work: Work,
write: SharedWriteFn,
/// The automation roots in force where the work was started, in force where it runs.
scope: ScopeHandle,
}
/// A job in progress.
pub struct Job {
id: u64,
/// What the status bar and `ui.inspect` show ("Saving Poster.vectorcraft"); empty for work they
/// don't show.
pub label: String,
/// The document the job reads (`DocState::uid`), if any.
pub doc: Option<u64>,
then: Then,
}
/// The worker and the jobs it has not finished.
#[derive(Default)]
pub struct Background {
worker: Option<(Sender<Task>, Receiver<Done>)>,
/// Jobs in the order they were started (the worker runs them in that order).
pub jobs: Vec<Job>,
next_id: u64,
}
impl Background {
/// The worker's channels, started on first use (`None` on wasm, which has no threads).
fn worker(&mut self) -> Option<&Sender<Task>> {
if cfg!(target_arch = "wasm32") {
return None;
}
if self.worker.is_none() {
let (tx, tasks) = channel::<Task>();
let (done_tx, done) = channel();
std::thread::Builder::new()
.name("vectorcraft-save".into())
.spawn(move || {
while let Ok(Task { id, work, write, scope }) = tasks.recv() {
let r = scope
.run(|| vectorcraft_engine::guard::catch_panic(|| work(&mut |p: &str, b: &[u8]| write(p, b))))
.unwrap_or_else(|msg| Err(format!("internal error: {msg} (please report this bug)")));
if done_tx.send((id, r)).is_err() {
break;
}
}
})
.ok()?;
self.worker = Some((tx, done));
}
self.worker.as_ref().map(|(tx, _)| tx)
}
/// Is a job reading document `uid` still running?
pub fn busy_with(&self, uid: u64) -> bool {
self.jobs.iter().any(|j| j.doc == Some(uid))
}
}
/// Run `work` on a worker thread when `enabled` and the host can write from one, else now; `then`
/// gets its result on the UI thread. → `then`'s result, or `{background: true, status}` while the
/// work runs (the status bar shows `label`). With an empty `label` neither the status bar nor
/// `ui.inspect` shows the work, and the reply has no `status`.
pub fn run(
app: &mut VectorcraftApp,
enabled: bool,
label: String,
doc: Option<u64>,
work: impl FnOnce(Writer) -> Result<Value, String> + Send + 'static,
then: impl FnOnce(&mut VectorcraftApp, Result<Value, String>) -> Result<Value, String> + 'static,
) -> Result<Value, String> {
let mut work: Work = Box::new(work);
let shared = app.services.write_shared.clone().filter(|_| enabled && app.services.download.is_none());
if let Some(write) = shared {
let id = app.background.next_id;
if let Some(tx) = app.background.worker() {
match tx.send(Task { id, work, write, scope: ScopeHandle::capture() }) {
Ok(()) => {
let bg = &mut app.background;
bg.next_id += 1;
bg.jobs.push(Job { id, label: label.clone(), doc, then: Box::new(then) });
if label.is_empty() {
return Ok(json!({ "background": true }));
}
let status = format!("{label}…");
app.status(status.clone());
return Ok(json!({ "background": true, "status": status }));
}
// The worker stopped: start a new one next time, and do this one now.
Err(e) => {
app.background.worker = None;
work = e.0.work;
}
}
}
}
let services = &mut app.services;
let r = work(&mut |p: &str, b: &[u8]| crate::io::write_to(services, p, b));
then(app, r)
}
/// Apply the results that arrived (each frame).
pub fn poll(app: &mut VectorcraftApp) {
while let Some(done) = app.background.worker.as_ref().and_then(|(_, rx)| rx.try_recv().ok()) {
finish(app, done);
}
}
/// Wait for every job and apply its result (before quitting, closing a document, or another save
/// of a document a job is reading).
pub fn wait_all(app: &mut VectorcraftApp) {
wait_until(app, |_| false);
}
/// Apply the jobs' results as they arrive (in the order the jobs were started) until `ready` holds
/// or no job is left.
pub fn wait_until(app: &mut VectorcraftApp, ready: impl Fn(&VectorcraftApp) -> bool) {
while !ready(app) && !app.background.jobs.is_empty() {
let Some(done) = app.background.worker.as_ref().and_then(|(_, rx)| rx.recv().ok()) else {
// The worker is gone: its jobs can't finish.
for job in std::mem::take(&mut app.background.jobs) {
apply(app, job, Err("the background worker stopped".into()));
}
app.background.worker = None;
return;
};
finish(app, done);
}
}
fn finish(app: &mut VectorcraftApp, (id, r): Done) {
let Some(i) = app.background.jobs.iter().position(|j| j.id == id) else { return };
let job = app.background.jobs.remove(i);
apply(app, job, r);
}
/// Hand a job its result; a failure goes to the status bar (the call that started it returned).
fn apply(app: &mut VectorcraftApp, job: Job, r: Result<Value, String>) {
if let Err(e) = (job.then)(app, r) {
app.status(format!("{} failed: {e}", job.label));
}
}