|
|
|
|
@ -38,10 +38,21 @@ pub use control::{ControlRequest, ControlResponse};
|
|
|
|
|
pub type ReadFn = Box<dyn Fn(&str) -> Result<Vec<u8>, String>>;
|
|
|
|
|
pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
|
|
|
|
|
pub type PickFn = Box<dyn FnMut(&str) -> Option<String>>;
|
|
|
|
|
pub type OpenAsyncFn = Box<dyn FnMut(&str)>;
|
|
|
|
|
/// Context captured before any host file-picker or read await.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct ImportRequest {
|
|
|
|
|
pub purpose: String,
|
|
|
|
|
pub target: Option<u64>,
|
|
|
|
|
}
|
|
|
|
|
pub struct ImportedFile {
|
|
|
|
|
pub request: ImportRequest,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub bytes: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
pub type OpenAsyncFn = Box<dyn FnMut(ImportRequest)>;
|
|
|
|
|
pub type DownloadFn = Box<dyn FnMut(&str, &[u8])>;
|
|
|
|
|
/// Files `(name, bytes)` delivered asynchronously by the host (web file picker, dropped files).
|
|
|
|
|
pub type Inbox = std::sync::Arc<std::sync::Mutex<Vec<(String, Vec<u8>)>>>;
|
|
|
|
|
/// Files delivered asynchronously with their original operation and document identity.
|
|
|
|
|
pub type Inbox = std::sync::Arc<std::sync::Mutex<Vec<ImportedFile>>>;
|
|
|
|
|
|
|
|
|
|
/// Platform services injected by the host (desktop or web).
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
@ -58,8 +69,7 @@ pub struct Services {
|
|
|
|
|
/// Hand bytes to the user as a named file (browser download). When set, Save uses it
|
|
|
|
|
/// instead of writing to a path.
|
|
|
|
|
pub download: Option<DownloadFn>,
|
|
|
|
|
/// Files delivered asynchronously, drained every frame: documents ([`opens_as_document`]) →
|
|
|
|
|
/// `file.openBytes`, `.ase` → `swatch.load`, anything else → `file.place`.
|
|
|
|
|
/// Files delivered asynchronously, drained every frame with their captured request context.
|
|
|
|
|
pub inbox: Option<Inbox>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -386,6 +396,7 @@ pub struct Perf {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct DesignApp {
|
|
|
|
|
pending_pdf: Option<ImportedFile>,
|
|
|
|
|
pub session: Session,
|
|
|
|
|
pub ui: UiState,
|
|
|
|
|
pub services: Services,
|
|
|
|
|
@ -434,6 +445,7 @@ impl DesignApp {
|
|
|
|
|
// The font menus and the first file opened need the installed fonts: catalog them now.
|
|
|
|
|
designcraft_fonts::FontDb::global().scan_in_background();
|
|
|
|
|
DesignApp {
|
|
|
|
|
pending_pdf: None,
|
|
|
|
|
session,
|
|
|
|
|
ui: UiState::default(),
|
|
|
|
|
services,
|
|
|
|
|
@ -537,6 +549,7 @@ impl DesignApp {
|
|
|
|
|
/// Ask the host for a file to open (`open`) or place (`place`). Synchronous pickers run the
|
|
|
|
|
/// command right away; asynchronous ones (web) deliver the file through the inbox.
|
|
|
|
|
pub fn pick_and_open(&mut self, purpose: &str) -> Result<Value, String> {
|
|
|
|
|
let target = self.session.active().map(|d| d.uid);
|
|
|
|
|
let cmd = if purpose == "place" { "file.place" } else { "file.open" };
|
|
|
|
|
if let Some(pick) = self.services.pick_open.as_mut() {
|
|
|
|
|
return match pick(purpose) {
|
|
|
|
|
@ -545,7 +558,8 @@ impl DesignApp {
|
|
|
|
|
let pages =
|
|
|
|
|
self.services.read.as_mut().and_then(|r| r(&path).ok()).and_then(|b| designcraft_render::pdf_page_count(&b)).unwrap_or(1);
|
|
|
|
|
if pages > 1 {
|
|
|
|
|
self.ui.dialog = Some(dialogs::Dialog::new("pdfImport", json!({"path": path, "page": "1", "pages": pages})));
|
|
|
|
|
self.ui.dialog =
|
|
|
|
|
Some(dialogs::Dialog::new("pdfImport", json!({"path": path, "page": "1", "pages": pages, "target": target})));
|
|
|
|
|
Ok(Value::Null)
|
|
|
|
|
} else {
|
|
|
|
|
self.open_file(cmd, json!({"path": path}))
|
|
|
|
|
@ -555,33 +569,99 @@ impl DesignApp {
|
|
|
|
|
None => Ok(Value::Null),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
let request = self.import_request(purpose);
|
|
|
|
|
if let Some(open) = self.services.open_async.as_mut() {
|
|
|
|
|
open(purpose);
|
|
|
|
|
open(request);
|
|
|
|
|
}
|
|
|
|
|
Ok(Value::Null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Open or place files delivered through the inbox.
|
|
|
|
|
pub fn import_request(&self, purpose: &str) -> ImportRequest {
|
|
|
|
|
ImportRequest { purpose: purpose.into(), target: self.session.active().map(|d| d.uid) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn activate_import_target(&mut self, request: &ImportRequest) -> Result<(), String> {
|
|
|
|
|
let Some(index) = request.target.and_then(|uid| self.session.documents().iter().position(|d| d.uid == uid)) else {
|
|
|
|
|
let message = "Import cancelled: the originating document has closed";
|
|
|
|
|
self.status(message);
|
|
|
|
|
return Err(message.into());
|
|
|
|
|
};
|
|
|
|
|
self.run("file.activate", json!({"index": index}))?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn cancel_pdf_import(&mut self) {
|
|
|
|
|
self.pending_pdf = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn confirm_pdf_import(&mut self, page: u64, crop: String) -> Result<Value, String> {
|
|
|
|
|
let file = self.pending_pdf.take().ok_or("PDF import cancelled: no pending file")?;
|
|
|
|
|
self.activate_import_target(&file.request)?;
|
|
|
|
|
self.open_file(
|
|
|
|
|
"file.place",
|
|
|
|
|
json!({"name": file.name, "base64": designcraft_engine::cmd::base64_encode(&file.bytes), "pdfPage": page, "pdfCrop": crop}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A single unresolved PDF owns its bytes. Later deliveries cancel explicitly rather than
|
|
|
|
|
/// replacing its options. Cancellation, replacement and target closure release its payload.
|
|
|
|
|
fn drain_inbox(&mut self) {
|
|
|
|
|
if let Some(file) = &self.pending_pdf {
|
|
|
|
|
let target_exists = self.session.documents().iter().any(|d| Some(d.uid) == file.request.target);
|
|
|
|
|
let owns_dialog =
|
|
|
|
|
self.ui.dialog.as_ref().is_some_and(|d| d.id == "pdfImport" && d.fields.get("async").and_then(Value::as_bool) == Some(true));
|
|
|
|
|
if !target_exists || !owns_dialog {
|
|
|
|
|
self.pending_pdf = None;
|
|
|
|
|
if !target_exists {
|
|
|
|
|
if owns_dialog {
|
|
|
|
|
self.ui.dialog = None;
|
|
|
|
|
}
|
|
|
|
|
self.status("PDF import cancelled: the originating document has closed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let Some(inbox) = self.services.inbox.clone() else { return };
|
|
|
|
|
let files = std::mem::take(&mut *inbox.lock().unwrap_or_else(|e| e.into_inner()));
|
|
|
|
|
for (name, bytes) in files {
|
|
|
|
|
let b64 = designcraft_engine::cmd::base64_encode(&bytes);
|
|
|
|
|
let lower = name.to_ascii_lowercase();
|
|
|
|
|
let r = if lower.ends_with(".ase") {
|
|
|
|
|
self.run("swatch.load", json!({"base64": b64}))
|
|
|
|
|
} else if opens_as_document(&name) {
|
|
|
|
|
let title = name.rsplit_once('.').map_or(name.as_str(), |(stem, _)| stem);
|
|
|
|
|
self.open_file("file.openBytes", json!({"name": title, "base64": b64}))
|
|
|
|
|
} else {
|
|
|
|
|
self.open_file("file.place", json!({"name": name, "base64": b64}))
|
|
|
|
|
};
|
|
|
|
|
if let Err(e) = r {
|
|
|
|
|
for file in files {
|
|
|
|
|
if self.ui.dialog.is_some() {
|
|
|
|
|
self.status(format!("{}: import cancelled while a dialog is open; choose the file again", file.name));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let name = file.name.clone();
|
|
|
|
|
if let Err(e) = self.import_file(file) {
|
|
|
|
|
self.status(format!("{name}: {e}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn import_file(&mut self, file: ImportedFile) -> Result<Value, String> {
|
|
|
|
|
let lower = file.name.to_ascii_lowercase();
|
|
|
|
|
let layout = opens_as_document(&file.name);
|
|
|
|
|
let place =
|
|
|
|
|
file.request.purpose == "place" || (matches!(file.request.purpose.as_str(), "drop" | "open") && !layout && !lower.ends_with(".ase"));
|
|
|
|
|
if place || lower.ends_with(".ase") || file.request.purpose == "swatches" {
|
|
|
|
|
self.activate_import_target(&file.request)?;
|
|
|
|
|
}
|
|
|
|
|
if place && lower.ends_with(".pdf") {
|
|
|
|
|
let pages = designcraft_render::pdf_page_count(&file.bytes).unwrap_or(1);
|
|
|
|
|
if pages > 1 {
|
|
|
|
|
self.ui.dialog =
|
|
|
|
|
Some(dialogs::Dialog::new("pdfImport", json!({"async": true, "name": file.name.clone(), "page": "1", "pages": pages})));
|
|
|
|
|
self.pending_pdf = Some(file);
|
|
|
|
|
return Ok(Value::Null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let b64 = designcraft_engine::cmd::base64_encode(&file.bytes);
|
|
|
|
|
if lower.ends_with(".ase") || file.request.purpose == "swatches" {
|
|
|
|
|
self.run("swatch.load", json!({"base64": b64}))
|
|
|
|
|
} else if place {
|
|
|
|
|
self.open_file("file.place", json!({"name": file.name, "base64": b64}))
|
|
|
|
|
} else {
|
|
|
|
|
let title = file.name.rsplit_once('.').map_or(file.name.as_str(), |(stem, _)| stem);
|
|
|
|
|
self.open_file("file.openBytes", json!({"name": title, "base64": b64}))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn select_tool(&mut self, id: &str) {
|
|
|
|
|
let _ = self.run("tool.select", json!({"tool": id}));
|
|
|
|
|
self.ui.flyout = None;
|
|
|
|
|
@ -1108,3 +1188,254 @@ pub(crate) mod test_window {
|
|
|
|
|
h.run_steps(4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod browser_import_tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn app() -> DesignApp {
|
|
|
|
|
let mut app = DesignApp::new(Session::new(), Services { inbox: Some(Inbox::default()), ..Default::default() });
|
|
|
|
|
app.run("file.new", json!({"title": "A"})).unwrap();
|
|
|
|
|
app
|
|
|
|
|
}
|
|
|
|
|
fn deliver(app: &mut DesignApp, request: ImportRequest, name: &str, bytes: Vec<u8>) {
|
|
|
|
|
app.services.inbox.as_ref().unwrap().lock().unwrap().push(ImportedFile { request, name: name.into(), bytes });
|
|
|
|
|
app.drain_inbox();
|
|
|
|
|
}
|
|
|
|
|
fn image() -> Vec<u8> {
|
|
|
|
|
br##"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10" fill="#ff0000"/></svg>"##.to_vec()
|
|
|
|
|
}
|
|
|
|
|
fn pdf_pages(count: usize) -> Vec<u8> {
|
|
|
|
|
let kids = (0..count).map(|i| format!("{} 0 R", 3 + i * 2)).collect::<Vec<_>>().join(" ");
|
|
|
|
|
let mut objects = vec!["<< /Type /Catalog /Pages 2 0 R >>".to_string(), format!("<< /Type /Pages /Kids [{kids}] /Count {count} >>")];
|
|
|
|
|
for (i, color) in ["1 0 0", "0 1 0", "0 0 1"].iter().take(count).enumerate() {
|
|
|
|
|
let content = format!("{color} rg 0 0 200 200 re f\n");
|
|
|
|
|
objects.push(format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents {} 0 R >>", 4 + i * 2));
|
|
|
|
|
objects.push(format!("<< /Length {} >>\nstream\n{content}endstream", content.len()));
|
|
|
|
|
}
|
|
|
|
|
let mut result = "%PDF-1.4\n".to_string();
|
|
|
|
|
let mut offsets = Vec::new();
|
|
|
|
|
for (i, object) in objects.iter().enumerate() {
|
|
|
|
|
offsets.push(result.len());
|
|
|
|
|
result.push_str(&format!("{} 0 obj\n{object}\nendobj\n", i + 1));
|
|
|
|
|
}
|
|
|
|
|
let xref = result.len();
|
|
|
|
|
result.push_str(&format!("xref\n0 {}\n0000000000 65535 f \n", offsets.len() + 1));
|
|
|
|
|
for offset in offsets {
|
|
|
|
|
result.push_str(&format!("{offset:010} 00000 n \n"));
|
|
|
|
|
}
|
|
|
|
|
result.push_str(&format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1));
|
|
|
|
|
result.into_bytes()
|
|
|
|
|
}
|
|
|
|
|
fn pdf() -> Vec<u8> {
|
|
|
|
|
pdf_pages(3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn picker_receives_context_before_returning_and_cancel_does_not_edit() {
|
|
|
|
|
let captured = std::sync::Arc::new(std::sync::Mutex::new(None));
|
|
|
|
|
let copy = captured.clone();
|
|
|
|
|
let mut app = app();
|
|
|
|
|
app.services.open_async = Some(Box::new(move |request| *copy.lock().unwrap() = Some(request)));
|
|
|
|
|
let uid = app.session.doc().unwrap().uid;
|
|
|
|
|
app.pick_and_open("place").unwrap();
|
|
|
|
|
app.run("file.new", json!({"title": "B"})).unwrap();
|
|
|
|
|
let request = captured.lock().unwrap().take().unwrap();
|
|
|
|
|
assert_eq!(request.purpose, "place");
|
|
|
|
|
assert_eq!(request.target, Some(uid));
|
|
|
|
|
assert!(app.session.documents().iter().all(|d| d.doc.assets.is_empty()));
|
|
|
|
|
deliver(&mut app, request, "broken.pdf", b"not a PDF".to_vec());
|
|
|
|
|
assert!(app.session.documents().iter().all(|d| d.doc.assets.is_empty()));
|
|
|
|
|
assert!(app.pending_pdf.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn delayed_place_activates_original_document_and_undo_belongs_to_it() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
app.run("file.new", json!({"title": "B"})).unwrap();
|
|
|
|
|
deliver(&mut app, request, "red.svg", image());
|
|
|
|
|
assert_eq!(app.session.active_index(), Some(0));
|
|
|
|
|
assert_eq!(app.session.documents()[0].doc.assets.len(), 1);
|
|
|
|
|
assert!(app.session.documents()[1].doc.assets.is_empty());
|
|
|
|
|
app.run("edit.undo", json!({})).unwrap();
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.assets.is_empty());
|
|
|
|
|
app.run("edit.redo", json!({})).unwrap();
|
|
|
|
|
assert_eq!(app.session.doc().unwrap().doc.assets.len(), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn closed_target_is_not_replaced_by_a_reused_tab_index() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
app.run("file.close", json!({})).unwrap();
|
|
|
|
|
app.run("file.new", json!({"title": "replacement"})).unwrap();
|
|
|
|
|
deliver(&mut app, request, "red.svg", image());
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.assets.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn closing_earlier_tab_does_not_shift_import_target() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
app.run("file.new", json!({"title": "target"})).unwrap();
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
app.run("file.new", json!({"title": "other"})).unwrap();
|
|
|
|
|
app.run("file.close", json!({"index": 0})).unwrap();
|
|
|
|
|
deliver(&mut app, request, "red.svg", image());
|
|
|
|
|
assert_eq!(app.session.doc().unwrap().doc.title, "target");
|
|
|
|
|
assert_eq!(app.session.documents()[0].doc.assets.len(), 1);
|
|
|
|
|
assert!(app.session.documents()[1].doc.assets.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn layout_place_and_open_preserve_explicit_intent() {
|
|
|
|
|
let mut source = Session::new();
|
|
|
|
|
source.execute("file.new", &json!({"title": "source"})).unwrap();
|
|
|
|
|
source.execute("frame.create", &json!({"rect": [10, 10, 50, 50], "content": "text", "text": "placed"})).unwrap();
|
|
|
|
|
let native = designcraft_engine::cmd::to_bytes(&source.doc().unwrap().doc);
|
|
|
|
|
let idml = designcraft_idml::export_idml(&source.doc().unwrap().doc);
|
|
|
|
|
for (name, bytes) in [("source.designcraft", native), ("source.idml", idml)] {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
deliver(&mut app, request, name, bytes.clone());
|
|
|
|
|
assert_eq!(app.session.documents().len(), 1);
|
|
|
|
|
assert_eq!(app.session.doc().unwrap().doc.title, "A");
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.stories.values().any(|s| s.text == "placed"));
|
|
|
|
|
app.run("edit.undo", json!({})).unwrap();
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.stories.is_empty());
|
|
|
|
|
app.run("edit.redo", json!({})).unwrap();
|
|
|
|
|
assert!(!app.session.doc().unwrap().doc.stories.is_empty());
|
|
|
|
|
let request = app.import_request("open");
|
|
|
|
|
deliver(&mut app, request, name, bytes);
|
|
|
|
|
assert_eq!(app.session.documents().len(), 2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pdf_options_keep_bytes_target_and_selected_page() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let bytes = pdf();
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
deliver(&mut app, request, "three.pdf", bytes.clone());
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.assets.is_empty());
|
|
|
|
|
assert_eq!(app.ui.dialog.as_ref().unwrap().id, "pdfImport");
|
|
|
|
|
app.run("file.new", json!({"title": "B"})).unwrap();
|
|
|
|
|
app.ui.dialog.as_mut().unwrap().fields.insert("page".into(), json!("2"));
|
|
|
|
|
dialogs::confirm(&mut app).unwrap();
|
|
|
|
|
assert_eq!(app.session.active_index(), Some(0));
|
|
|
|
|
let asset = app.session.doc().unwrap().doc.assets.values().next().unwrap();
|
|
|
|
|
assert_eq!(asset.page, 1);
|
|
|
|
|
assert_eq!(*asset.data, bytes);
|
|
|
|
|
assert!(app.pending_pdf.is_none());
|
|
|
|
|
assert!(app.session.documents()[1].doc.assets.is_empty());
|
|
|
|
|
let exported = app.run("file.exportPdf", json!({})).unwrap();
|
|
|
|
|
let exported = designcraft_engine::cmd::base64_decode(exported["base64"].as_str().unwrap());
|
|
|
|
|
let pixels = designcraft_render::render_pdf_page(&exported, 0, 400).unwrap();
|
|
|
|
|
assert!(pixels.data().iter().filter(|p| p.g > 240 && p.r < 10 && p.b < 10).count() > 1000);
|
|
|
|
|
assert_eq!(pixels.data().iter().filter(|p| p.r > 240 && p.g < 10 && p.b < 10).count(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pdf_options_reject_oversized_page_without_editing() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
for page in [0_u64, 4, 4_294_967_296, 4_294_967_297, u64::MAX] {
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
deliver(&mut app, request, "three.pdf", pdf());
|
|
|
|
|
app.ui.dialog.as_mut().unwrap().fields.insert("page".into(), json!(page.to_string()));
|
|
|
|
|
// The native dialog clamps zero to one; positive invalid pages must fail.
|
|
|
|
|
if page == 0 {
|
|
|
|
|
assert!(app.confirm_pdf_import(page, "crop".into()).is_err());
|
|
|
|
|
} else {
|
|
|
|
|
assert!(dialogs::confirm(&mut app).is_err());
|
|
|
|
|
}
|
|
|
|
|
app.ui.dialog = None;
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.assets.is_empty());
|
|
|
|
|
assert!(app.pending_pdf.is_none());
|
|
|
|
|
}
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
deliver(&mut app, request, "three.pdf", pdf());
|
|
|
|
|
app.ui.dialog.as_mut().unwrap().fields.insert("page".into(), json!("2"));
|
|
|
|
|
dialogs::confirm(&mut app).unwrap();
|
|
|
|
|
assert_eq!(app.session.doc().unwrap().doc.assets.values().next().unwrap().page, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn drop_routing_and_nonlayout_open_remain_compatible() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let native = designcraft_engine::cmd::to_bytes(&app.session.doc().unwrap().doc);
|
|
|
|
|
let request = app.import_request("drop");
|
|
|
|
|
deliver(&mut app, request, "document.designcraft", native);
|
|
|
|
|
assert_eq!(app.session.documents().len(), 2);
|
|
|
|
|
let request = app.import_request("drop");
|
|
|
|
|
app.run("file.activate", json!({"index": 0})).unwrap();
|
|
|
|
|
deliver(&mut app, request, "red.svg", image());
|
|
|
|
|
assert_eq!(app.session.active_index(), Some(1));
|
|
|
|
|
assert_eq!(app.session.doc().unwrap().doc.assets.len(), 1);
|
|
|
|
|
let request = app.import_request("open");
|
|
|
|
|
deliver(&mut app, request, "single.pdf", pdf_pages(1));
|
|
|
|
|
assert_eq!(app.session.documents().len(), 2);
|
|
|
|
|
assert!(app.pending_pdf.is_none());
|
|
|
|
|
assert!(app.ui.dialog.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ase_swatches_keep_purpose_and_target() {
|
|
|
|
|
let mut source = Session::new();
|
|
|
|
|
source.execute("file.new", &json!({})).unwrap();
|
|
|
|
|
source.execute("swatch.create", &json!({"name": "Import Teal", "color": "#108080"})).unwrap();
|
|
|
|
|
let data = source.execute("swatch.save", &json!({"names": ["Import Teal"]})).unwrap();
|
|
|
|
|
let bytes = designcraft_engine::cmd::base64_decode(data["base64"].as_str().unwrap());
|
|
|
|
|
for (purpose, name) in [("swatches", "picker-file"), ("drop", "palette.ase")] {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let request = app.import_request(purpose);
|
|
|
|
|
app.run("file.new", json!({"title": "B"})).unwrap();
|
|
|
|
|
deliver(&mut app, request, name, bytes.clone());
|
|
|
|
|
assert_eq!(app.session.active_index(), Some(0));
|
|
|
|
|
assert!(app.session.documents()[0].doc.swatches.iter().any(|s| s.name == "Import Teal"));
|
|
|
|
|
assert!(!app.session.documents()[1].doc.swatches.iter().any(|s| s.name == "Import Teal"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn native_pdf_dialog_keeps_path_and_uid_at_confirmation() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let path = std::env::temp_dir().join(format!("designcraft-native-import-{}-{}.pdf", std::process::id(), app.session.doc().unwrap().uid));
|
|
|
|
|
std::fs::write(&path, pdf()).unwrap();
|
|
|
|
|
let pick_path = path.to_string_lossy().to_string();
|
|
|
|
|
app.services.pick_open = Some(Box::new(move |_| Some(pick_path.clone())));
|
|
|
|
|
app.services.read = Some(Box::new(|path| std::fs::read(path).map_err(|e| e.to_string())));
|
|
|
|
|
app.pick_and_open("place").unwrap();
|
|
|
|
|
assert_eq!(app.ui.dialog.as_ref().unwrap().id, "pdfImport");
|
|
|
|
|
app.run("file.new", json!({"title": "B"})).unwrap();
|
|
|
|
|
app.ui.dialog.as_mut().unwrap().fields.insert("page".into(), json!("2"));
|
|
|
|
|
dialogs::confirm(&mut app).unwrap();
|
|
|
|
|
assert_eq!(app.session.active_index(), Some(0));
|
|
|
|
|
assert_eq!(app.session.doc().unwrap().doc.assets.values().next().unwrap().page, 1);
|
|
|
|
|
assert!(app.session.documents()[1].doc.assets.is_empty());
|
|
|
|
|
std::fs::remove_file(path).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pending_pdf_cancel_close_supersession_and_second_delivery_release_bytes() {
|
|
|
|
|
let mut app = app();
|
|
|
|
|
let request = app.import_request("place");
|
|
|
|
|
deliver(&mut app, request.clone(), "first.pdf", pdf());
|
|
|
|
|
deliver(&mut app, request.clone(), "second.svg", image());
|
|
|
|
|
assert_eq!(app.pending_pdf.as_ref().unwrap().name, "first.pdf");
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.assets.is_empty());
|
|
|
|
|
app.ui.dialog = None;
|
|
|
|
|
app.drain_inbox();
|
|
|
|
|
assert!(app.pending_pdf.is_none());
|
|
|
|
|
deliver(&mut app, request.clone(), "first.pdf", pdf());
|
|
|
|
|
app.ui.dialog = Some(dialogs::Dialog::new("newDocument", json!({})));
|
|
|
|
|
app.drain_inbox();
|
|
|
|
|
assert!(app.pending_pdf.is_none());
|
|
|
|
|
app.ui.dialog = None;
|
|
|
|
|
deliver(&mut app, request, "first.pdf", pdf());
|
|
|
|
|
app.run("file.close", json!({})).unwrap();
|
|
|
|
|
app.run("file.new", json!({})).unwrap();
|
|
|
|
|
assert!(dialogs::confirm(&mut app).is_err());
|
|
|
|
|
assert!(app.pending_pdf.is_none());
|
|
|
|
|
assert!(app.session.doc().unwrap().doc.assets.is_empty());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|