Merge pull request #5418 from storytold/fix/sec-ui-carry-automation-scope

UI: opening, file picks and the render worker keep the automation roots they started under
This commit is contained in:
BFlat
2026-11-03 23:55:12 +07:00
committed by GitHub
6 changed files with 66 additions and 14 deletions
+9 -1
View File
@@ -654,7 +654,15 @@ fn draw_frame(app: &mut VectorcraftApp, ui: &Ui, frame: Frame) {
app.canvas.shown = Some((doc.clone(), opts.clone()));
app.perf.render_ms = now_ms() - t0;
}
(Some(worker), true, None) => worker.submit(crate::render_worker::Job { key: key.clone(), doc: doc.clone(), w, h, view, opts }),
(Some(worker), true, None) => worker.submit(crate::render_worker::Job {
key: key.clone(),
doc: doc.clone(),
w,
h,
view,
opts,
scope: vectorcraft_engine::file_access::ScopeHandle::capture(),
}),
_ => {
let t0 = now_ms();
let img = app.canvas.renderer.render(doc, w, h, view, &opts);
+9 -1
View File
@@ -98,7 +98,15 @@ pub(super) fn fill_uncovered(
let Some(worker) = app.canvas.fill_worker.as_mut().filter(|_| !gaps.is_empty()) else { return };
if !app.canvas.fill.as_ref().is_some_and(|f| covers(&f.key)) && !worker.pending.as_ref().is_some_and(covers) {
let (w, h, view) = fill_view(key);
worker.submit(crate::render_worker::Job { key: key.clone(), doc: doc.clone(), w, h, view, opts });
worker.submit(crate::render_worker::Job {
key: key.clone(),
doc: doc.clone(),
w,
h,
view,
opts,
scope: vectorcraft_engine::file_access::ScopeHandle::capture(),
});
}
let Some(f) = app.canvas.fill.as_ref().filter(|f| usable(&f.key)) else { return };
let to_uv = Affine::scale_non_uniform(1.0 / f64::from(f.size.0.max(1)), 1.0 / f64::from(f.size.1.max(1))) * f.view * screen_to_doc;
+6 -2
View File
@@ -20,6 +20,9 @@ pub struct Pending {
/// What the status bar shows ("Opening Poster.vectorcraft").
pub label: String,
done: Receiver<Result<ReadFile, String>>,
/// The automation roots in force where the open started: the document is installed under
/// them too (its links are read as it opens).
scope: ScopeHandle,
}
/// The files being read.
@@ -57,6 +60,7 @@ pub fn start(app: &mut VectorcraftApp, path: &str, bytes: Vec<u8>) -> Result<Val
let low_res_proxy_eps = app.session.prefs.low_res_proxy_eps;
// The automation roots in force here are in force there.
let scope = ScopeHandle::capture();
let opener = scope.clone();
let spawned = std::thread::Builder::new().name("vectorcraft-open".into()).spawn(move || {
let r = scope
.run(|| {
@@ -74,7 +78,7 @@ pub fn start(app: &mut VectorcraftApp, path: &str, bytes: Vec<u8>) -> Result<Val
return Err(Arc::try_unwrap(bytes).unwrap_or_else(|b| b.to_vec()));
}
let label = format!("Opening {}", fileio::file_name(path));
app.opening.pending.push(Pending { path: path.to_string(), label: label.clone(), done });
app.opening.pending.push(Pending { path: path.to_string(), label: label.clone(), done, scope: opener });
let status = format!("{label}…");
app.status(status.clone());
Ok(json!({ "background": true, "status": status }))
@@ -109,7 +113,7 @@ pub fn poll(app: &mut VectorcraftApp) {
if app.ui.status == format!("{}…", p.label) {
app.ui.status.clear();
}
finish(app, &p.path, r);
p.scope.run(|| finish(app, &p.path, r));
}
}
+22
View File
@@ -310,3 +310,25 @@ fn a_file_read_off_the_ui_thread_has_its_links_and_type_as_one_read_at_once() {
}
let _ = std::fs::remove_dir_all(&dir);
}
/// A file opened under automation roots is installed under them too: the open keeps the scope it
/// started in, not the desktop's access of the frame it finishes in.
#[test]
fn a_file_opened_under_automation_roots_finishes_under_them() {
use vectorcraft_engine::file_access;
let dir = scope_dir();
let roots = file_access::AutomationRoots::new(&[dir.as_path()], &[]).unwrap();
let mut app = VectorcraftApp::new(Session::new(), Services::default());
file_access::confine(roots.as_ref(), || {
start(&mut app, "/art/Scoped.vectorcraft", vectorcraft_format::save(&vectorcraft_doc::Document::new(10.0, 10.0), false))
})
.unwrap();
let scope = app.opening.pending[0].scope.clone();
assert!(scope.run(file_access::current).is_some(), "the roots go with the open");
}
fn scope_dir() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("vectorcraft-opening-scope-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
+15 -9
View File
@@ -20,6 +20,7 @@ use std::rc::Rc;
use std::sync::mpsc::{Receiver, TryRecvError};
use serde_json::Value;
use vectorcraft_engine::file_access::ScopeHandle;
use crate::state::Dialog;
use crate::{FilePick, VectorcraftApp};
@@ -91,6 +92,9 @@ struct Waiting {
entry: Entry,
/// The active document when it was asked for ([`vectorcraft_engine::DocState::uid`]).
doc: Option<u64>,
/// The automation roots in force when it was asked for: the entry runs again under them, not
/// with the desktop's access of the frame the answer comes in.
scope: ScopeHandle,
}
/// The app's file dialogs ([`VectorcraftApp`]'s `picks`).
@@ -286,7 +290,7 @@ fn ask(app: &mut VectorcraftApp, request: PickRequest) -> Option<PickAnswer> {
(Some(entry), Some(start)) => {
if let Some(answer) = start(request.clone()) {
let doc = entry.document(app);
app.picks.waiting = Some(Waiting { request, answer, entry, doc });
app.picks.waiting = Some(Waiting { request, answer, entry, doc, scope: ScopeHandle::capture() });
return None;
}
}
@@ -333,20 +337,22 @@ pub(crate) fn poll(app: &mut VectorcraftApp, ctx: &egui::Context) {
// The dialog's thread is gone: as cancelled.
Err(TryRecvError::Disconnected) => vec![],
};
let Some(Waiting { request, entry, doc, .. }) = app.picks.waiting.take() else { return };
let Some(Waiting { request, entry, doc, scope, .. }) = app.picks.waiting.take() else { return };
app.picks.refused = false;
// Once more: what waited for the dialog (files from the Finder, the menu bar) goes on.
ctx.request_repaint();
if answer.is_empty() {
return;
}
if !on_document(app, doc) {
return;
}
app.picks.answer = Some((request, answer));
rerun(app, entry);
// An entry that took another way this time leaves the answer unused.
app.picks.answer = None;
scope.run(|| {
if !on_document(app, doc) {
return;
}
app.picks.answer = Some((request, answer));
rerun(app, entry);
// An entry that took another way this time leaves the answer unused.
app.picks.answer = None;
});
}
/// Make the document an entry asked for (`doc`, by its uid) active again before it runs again:
+5 -1
View File
@@ -7,6 +7,7 @@ use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, channel};
use vectorcraft_doc::Document;
use vectorcraft_engine::file_access::ScopeHandle;
use vectorcraft_geom::Affine;
use vectorcraft_render::{RenderOptions, Rendered, Renderer};
@@ -19,6 +20,9 @@ pub struct Job {
pub h: u32,
pub view: Affine,
pub opts: RenderOptions,
/// The automation roots in force where the job was made: the render reads linked files under
/// them, as it would on that thread.
pub scope: ScopeHandle,
}
pub struct Done {
@@ -56,7 +60,7 @@ impl Worker {
job = newer;
}
let t0 = crate::now_ms();
let img = r.render(&job.doc, job.w, job.h, job.view, &job.opts);
let img = job.scope.clone().run(|| r.render(&job.doc, job.w, job.h, job.view, &job.opts));
if done_tx.send(Done { key: job.key, doc: job.doc, opts: job.opts, img, ms: crate::now_ms() - t0, moved: r.stats.moved }).is_err()
{
break;