mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
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.
181 lines
7.3 KiB
Rust
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));
|
|
}
|
|
}
|