mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
214 lines
9.6 KiB
Rust
214 lines
9.6 KiB
Rust
//! Placing files off the UI thread (desktop): a file the user places (the Place dialog's OK, a file
|
|
//! dropped on the canvas, the OK of the PDF, DXF and text import options Place asks for) is read and
|
|
//! decoded on a thread that reads one file at a time while the status bar shows it, and is placed in
|
|
//! the document it was placed in once it is ready, as it would be at once: the same object, undo
|
|
//! step, selection and errors. The files the Place dialog loads the place cursor with are read, and
|
|
//! their thumbnails made, the same way, and so is the file each click or drag of the place cursor
|
|
//! places. Files are read and placed in the order they were started. `file.place` and
|
|
//! `file.place.queue` from MCP and scripts run at once, and so does every place on wasm (no
|
|
//! threads), with `VECTORCRAFT_SYNC_OPEN` set, and in unit tests that do not set
|
|
//! `off_the_ui_thread` ([`available`]).
|
|
|
|
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_engine::cmd::place::{PlaceRead, QueueRead};
|
|
use vectorcraft_engine::file_access::ScopeHandle;
|
|
|
|
use crate::VectorcraftApp;
|
|
|
|
/// What was read: one file to place, or the files to load the place cursor with.
|
|
enum Read {
|
|
Place(Box<PlaceRead>),
|
|
Queue(QueueRead),
|
|
}
|
|
|
|
/// Files being read for Place.
|
|
pub struct Pending {
|
|
/// The file's name ("photo.tif"; the names joined with ", " for the place cursor's files).
|
|
pub(crate) name: String,
|
|
/// What the status bar shows ("Placing photo.tif").
|
|
pub label: String,
|
|
/// The document they are placed in ([`vectorcraft_engine::DocState::uid`]).
|
|
doc: u64,
|
|
/// Whether the file was dropped on the canvas (its messages say so, as
|
|
/// [`crate::place::drop_files`]'s do).
|
|
dropped: bool,
|
|
done: Receiver<Result<Read, String>>,
|
|
}
|
|
|
|
/// A read waiting for the thread that reads the files.
|
|
type Job = Box<dyn FnOnce() + Send>;
|
|
|
|
/// The files being read for Place, in the order they were started.
|
|
#[derive(Default)]
|
|
pub struct Placing {
|
|
pub pending: Vec<Pending>,
|
|
/// Set by unit tests whose app reads the files the user places off the UI thread.
|
|
pub(crate) off_the_ui_thread: bool,
|
|
/// The thread that reads the files, one at a time in the order they were started: started with
|
|
/// the first read, it ends when the app does.
|
|
reader: Option<Sender<Job>>,
|
|
/// Unit tests: how many reads run now, and the most that ran at once.
|
|
#[cfg(test)]
|
|
pub(crate) reading: std::sync::Arc<[std::sync::atomic::AtomicUsize; 2]>,
|
|
}
|
|
|
|
/// Does `app` read a file the user places off the UI thread? Where documents open off it
|
|
/// ([`crate::opening::available`]), and in the unit tests that set `off_the_ui_thread`.
|
|
pub fn available(app: &VectorcraftApp) -> bool {
|
|
app.placing.off_the_ui_thread || crate::opening::available()
|
|
}
|
|
|
|
/// `file.place` with `p` for the active document, its file read and decoded off the UI thread
|
|
/// ([`vectorcraft_engine::Session::prepare_place`], [`vectorcraft_engine::cmd::place::PlaceJob::read`])
|
|
/// → `{background: true, status}`; `dropped` when it was dropped on the canvas. The params are
|
|
/// checked here first, so a bad one is an error at once. When no thread could start, the file is
|
|
/// placed at once → `file.place`'s reply.
|
|
pub fn start(app: &mut VectorcraftApp, p: &Value, dropped: bool) -> Result<Value, String> {
|
|
let job = app.session.prepare_place(p).map_err(|e| e.to_string())?;
|
|
let name = job.name();
|
|
let started = spawn(app, name.clone(), name, dropped, move || job.read().map(|r| Read::Place(Box::new(r))).map_err(|e| e.to_string()))?;
|
|
started.map_or_else(
|
|
|| {
|
|
let r = app.session.execute("file.place", p).map_err(|e| e.to_string());
|
|
app.sync_views();
|
|
r
|
|
},
|
|
Ok,
|
|
)
|
|
}
|
|
|
|
/// `file.place.queue` with `p` for the active document, its files read and their thumbnails made off
|
|
/// the UI thread → `{background: true, status}`; the place cursor is loaded once they are ready. When no thread could start, the cursor is loaded at once → `file.place.queue`'s reply.
|
|
pub fn start_queue(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
|
let job = app.session.prepare_place_queue(p).map_err(|e| e.to_string())?;
|
|
let names = job.names().join(", ");
|
|
let started = spawn(app, names.clone(), names, false, move || job.read().map(Read::Queue).map_err(|e| e.to_string()))?;
|
|
match started {
|
|
Some(r) => Ok(r),
|
|
None => {
|
|
let r = app.session.execute("file.place.queue", p).map_err(|e| e.to_string());
|
|
app.sync_views();
|
|
let r = r?;
|
|
crate::place::show_thumbnails(app, &r);
|
|
Ok(r)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run `read` for the active document on the thread that reads the files, after the reads started
|
|
/// before it (so a batch of large pictures is decoded one at a time, as at once) →
|
|
/// `{background: true, status}`, or `None` when that thread could not start.
|
|
fn spawn(
|
|
app: &mut VectorcraftApp,
|
|
name: String,
|
|
shown: String,
|
|
dropped: bool,
|
|
read: impl FnOnce() -> Result<Read, String> + Send + 'static,
|
|
) -> Result<Option<Value>, String> {
|
|
let doc = app.session.doc().map_err(|e| e.to_string())?.uid;
|
|
let (tx, done) = channel();
|
|
// The automation roots in force here are in force there.
|
|
let scope = ScopeHandle::capture();
|
|
#[cfg(test)]
|
|
let reading = app.placing.reading.clone();
|
|
let job: Job = Box::new(move || {
|
|
#[cfg(test)]
|
|
{
|
|
let now = reading[0].fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
|
|
reading[1].fetch_max(now, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
let r = scope
|
|
.run(|| vectorcraft_engine::guard::catch_panic(read))
|
|
.unwrap_or_else(|msg| Err(format!("internal error: {msg} (please report this bug)")));
|
|
#[cfg(test)]
|
|
reading[0].fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
|
|
// The app may have quit meanwhile: nothing to hand it to.
|
|
let _ = tx.send(r);
|
|
});
|
|
// A reader that has ended gives the job back.
|
|
let job = match &app.placing.reader {
|
|
Some(reader) => reader.send(job).err().map(|e| e.0),
|
|
None => Some(job),
|
|
};
|
|
if let Some(job) = job {
|
|
let (reader, jobs) = channel::<Job>();
|
|
let started = std::thread::Builder::new().name("vectorcraft-place".into()).spawn(move || {
|
|
for job in jobs {
|
|
job();
|
|
}
|
|
});
|
|
if started.is_err() {
|
|
return Ok(None);
|
|
}
|
|
let _ = reader.send(job);
|
|
app.placing.reader = Some(reader);
|
|
}
|
|
let label = format!("Placing {shown}");
|
|
app.placing.pending.push(Pending { name, label: label.clone(), doc, dropped, done });
|
|
let status = format!("{label}…");
|
|
app.status(status.clone());
|
|
Ok(Some(json!({ "background": true, "status": status })))
|
|
}
|
|
|
|
/// Place the files that are ready (each frame), in the order they were started: a file read
|
|
/// sooner than one started before it waits for that one. Not while a dialog is open, an
|
|
/// interaction is open, or a tool is busy with a drag of its own (the place cursor's) while the
|
|
/// pointer button is held, as [`crate::opening::poll`]: a placed file's document becomes the
|
|
/// active one and the dialog's OK acts on the active document; a drag in another document is
|
|
/// canceled when the active document changes, and a drag or preview in the same one previews from
|
|
/// the document as it was before it began, without the placed art. A tool that is busy while the
|
|
/// button is not held is waiting between its clicks (the Dimension tool after its first point):
|
|
/// the file is placed unless the tool has a preview open (the Flare tool after its center drag).
|
|
pub fn poll(app: &mut VectorcraftApp) {
|
|
while app.ui.dialog.is_none()
|
|
&& !app.session.in_interaction()
|
|
&& !(app.session.tool_busy() && app.session.pointer_held())
|
|
&& let Some(p) = app.placing.pending.first()
|
|
{
|
|
let r = match p.done.try_recv() {
|
|
Ok(r) => r,
|
|
Err(TryRecvError::Empty) => return,
|
|
Err(TryRecvError::Disconnected) => Err("the file stopped being read (please report this bug)".to_string()),
|
|
};
|
|
let p = app.placing.pending.remove(0);
|
|
// The status bar no longer says it is placing.
|
|
if app.ui.status == format!("{}…", p.label) {
|
|
app.ui.status.clear();
|
|
}
|
|
finish(app, &p, r);
|
|
}
|
|
}
|
|
|
|
/// Place files read off the UI thread in their document, which becomes the active one again, as
|
|
/// [`crate::place::run`] and [`crate::place::queue`] place files read at once; an error shows in
|
|
/// the status bar as it does then.
|
|
fn finish(app: &mut VectorcraftApp, p: &Pending, r: Result<Read, String>) {
|
|
let Some(i) = app.session.documents().iter().position(|st| st.uid == p.doc) else {
|
|
let name = &p.name;
|
|
if p.dropped {
|
|
app.status(format!("Couldn't place {name}: the document it was dropped on was closed"));
|
|
} else {
|
|
app.status(format!("Couldn't place {name}: the document it was placed in was closed"));
|
|
}
|
|
return;
|
|
};
|
|
app.session.set_active(i);
|
|
let r = match r {
|
|
Ok(Read::Place(read)) => app.session.place_read(*read).map(|_| ()).map_err(|e| e.to_string()),
|
|
Ok(Read::Queue(read)) => app.session.place_queue_read(read).map(|r| crate::place::show_thumbnails(app, &r)).map_err(|e| e.to_string()),
|
|
Err(e) => Err(e),
|
|
};
|
|
app.sync_views();
|
|
let name = &p.name;
|
|
match r {
|
|
Err(e) if p.dropped => app.status(format!("Couldn't place {name}: {e}")),
|
|
Err(e) => app.status(e),
|
|
Ok(()) => {}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|