diff --git a/ROADMAP.md b/ROADMAP.md index 2a5d8a75..b684216f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-13** · Dropping an `.ase` file on the window loads its swatches, as Load Swatches does (the drop failed with "Can't Place the File"). No readiness percentage change. - **2026-10-13** · Text takes gradient swatches: a gradient fill or stroke on characters spans the text frame's box (on a path: the path's), on screen and in PDF. No readiness percentage change. - **2026-10-13** · Gradient swatches work on strokes: drawn across the path's box on screen and in PDF (they drew nothing), set with `object.gradient {target: stroke}` and in the Gradient panel when the Stroke box is in front; the stroke's gradient vector moves with the shape and round-trips through IDML. No readiness percentage change. - **2026-10-13** · The Gradient panel follows InDesign's layout (thumbnail, Type, Location, Angle, Reverse, ramp with midpoint diamonds) and takes swatches dragged from the Swatches panel: dropped on a stop they colour it, elsewhere they add a stop. Stops keep their CMYK through panel edits; `object.gradient` stops accept a `swatch`. No readiness percentage change. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3b5e44b4..a7294905 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1237,9 +1237,13 @@ impl DesignApp { } } - /// A file dropped on the window: documents open, anything else is placed. A file without an - /// extension opens when its content is a DesignCraft or IDML document. + /// A file dropped on the window: documents open, the swatches in a swatch exchange (.ase) file + /// are loaded, and anything else is placed. A file without an extension opens when its content + /// is a DesignCraft or IDML document. pub fn open_dropped(&mut self, path: &str) -> Result { + if path.to_ascii_lowercase().ends_with(".ase") { + return self.run("swatch.load", json!({"path": path})); + } let document = opens_as_document(path) || (std::path::Path::new(path).extension().is_none() && std::fs::read(path).is_ok_and(|b| designcraft_engine::cmd::is_document(&b))); let cmd = if document { "file.open" } else { "file.place" }; @@ -2320,6 +2324,28 @@ mod tests { assert!(app.session.documents().is_empty()); } + #[test] + fn dropping_an_ase_file_loads_its_swatches() { + let mut source = Session::new(); + source.execute("file.new", &json!({})).unwrap(); + source.execute("swatch.create", &json!({"name": "Dropped Teal", "color": "#108080"})).unwrap(); + let data = source.execute("swatch.save", &json!({"names": ["Dropped Teal"]})).unwrap(); + let bytes = designcraft_engine::cmd::base64_decode(data["base64"].as_str().unwrap()); + for name in ["palette.ase", "PALETTE.ASE"] { + let path = std::env::temp_dir().join(format!("designcraft-ui-{}-{name}", std::process::id())); + std::fs::write(&path, &bytes).unwrap(); + let mut app = DesignApp::new(Session::new(), Services::default()); + app.run("file.new", json!({})).unwrap(); + let r = app.open_dropped(&path.to_string_lossy()); + let _ = std::fs::remove_file(&path); + assert!(app.ui.dialog.is_none(), "{name}: {:?}", app.ui.dialog.as_ref().map(|d| &d.fields)); + assert_eq!(r, Ok(json!({"added": 1, "replaced": 0})), "{name}"); + let doc = &app.session.doc().unwrap().doc; + assert!(doc.swatches.iter().any(|s| s.name == "Dropped Teal"), "{name}"); + assert!(doc.assets.is_empty(), "{name} isn't placed"); + } + } + #[test] fn file_open_failures_show_an_alert() { let path = unreadable_file("open.designcraft");