collective

Browse / github.com/storytold/designcraft / patch

Fill the empty or graphic frame under the pointer when a file is dropped on it (#1646)

Bug: Drag-and-drop of JPG from File Explorer always creates a new object instead of placing it into the frame Description: When dropping a .jpg file from Windows File Explorer onto an existing graphic frame, DesignCraft always creates a completely new independent object instead of inserting the image into the target frame. Note: Placing the image via Ctrl + D works as expected and successfully inserts the image into the frame, but using the shortcut every time is highly inconvenient for the workflow. Steps to Reproduce: 1. Create a graphic frame using the Rectangle Frame Tool (F). 2. Drag a .jpg file from Windows File Explorer. 3. Hover strictly over the created frame and drop the file. Expected B

Status
open
Score
0.00
Adopters
0 other operators
Author
MarekSpodBiedry (imported from pull request #1666, open)
Base
2e36dcf909627c8f0e5662fe6b4825b1c1c24420
Size
+57 −2 in 4 files
Review
pending
Shared
2026-10-28
Id
a54ce12ec38bd81ef3c6131b

How it went

No other operator has reported using it yet.

Diff

raw · Cargo.lock crates/ui-egui/Cargo.toml crates/ui-egui/src/canvas.rs crates/ui-egui/src/lib.rs

diff --git a/Cargo.lock b/Cargo.lock--- a/Cargo.lock+++ b/Cargo.lock@@ -1247,6 +1247,7 @@ dependencies = [  "designcraft-geom",  "designcraft-idml",  "designcraft-render",+ "designcraft-textimport",  "designcraft-tools",  "egui",  "egui_kittest",diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml--- a/crates/ui-egui/Cargo.toml+++ b/crates/ui-egui/Cargo.toml@@ -19,6 +19,7 @@ designcraft-compose = { workspace = true } designcraft-render = { workspace = true } designcraft-tools = { workspace = true } designcraft-engine = { workspace = true }+designcraft-textimport = { workspace = true } unicode-bidi = { workspace = true } unicode-bidi-mirroring = { workspace = true } unicode-segmentation = { workspace = true }diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs--- a/crates/ui-egui/src/canvas.rs+++ b/crates/ui-egui/src/canvas.rs@@ -192,6 +192,17 @@ pub fn drop_point(app: &DesignApp, screen: Option<Pos2>) -> Option<(SpreadRef, P     layout.spread_at(Xf::new(rect, app.view()?).to_canvas(pos)) } +/// The empty or graphic frame under a pointer over the canvas: a graphic dropped there fills it+/// instead of becoming a new object. `None` when the pointer isn't over the canvas or over such a frame.+pub fn drop_frame(app: &DesignApp, screen: Option<Pos2>) -> Option<designcraft_doc::ItemId> {+    let rect = app.canvas_rect?;+    let pos = screen.filter(|p| rect.contains(*p))?;+    let v = app.view()?;+    let (id, _) = item_at(app, Xf::new(rect, v).to_canvas(pos), v.zoom)?;+    let it = app.session.active()?.doc.item(id)?;+    matches!(it.content, designcraft_doc::Content::Unassigned | designcraft_doc::Content::Graphic(_)).then_some(id)+}+ /// Absolute page index under the view centre. pub fn current_page(app: &DesignApp) -> Option<usize> {     let st = app.session.active()?;diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs--- a/crates/ui-egui/src/lib.rs+++ b/crates/ui-egui/src/lib.rs@@ -1333,8 +1333,9 @@ impl DesignApp {         self.open_dropped_at(path, None)     } -    /// [`Self::open_dropped`] at the pointer: a graphic's top left lands where the file was-    /// dropped on the canvas, text goes to a new frame on the document page there.+    /// [`Self::open_dropped`] at the pointer: a graphic dropped on an empty or graphic frame fills+    /// it, elsewhere its top left lands where the file was dropped on the canvas; text goes to a+    /// new frame on the document page there.     pub fn open_dropped_at(&mut self, path: &str, pointer: Option<egui::Pos2>) -> Result<Value, String> {         if path.to_ascii_lowercase().ends_with(".ase") {             return self.run("swatch.load", json!({"path": path}));@@ -1347,6 +1348,12 @@ impl DesignApp {             return self.open_file("file.open", json!({"path": path}));         }         let mut params = json!({"path": path});+        // A graphic dropped on an empty or graphic frame fills it, as File > Place does. Text keeps+        // getting a frame of its own, so it never replaces the image in a graphic frame.+        let frame = if designcraft_textimport::is_text_file(path) { None } else { canvas::drop_frame(self, pointer) };+        if let (Some(frame), Some(o)) = (frame, params.as_object_mut()) {+            o.insert("frame".into(), json!(frame.0));+        }         if let (Some((sr, at)), Some(o)) = (canvas::drop_point(self, pointer), params.as_object_mut()) {             o.insert("spread".into(), serde_json::to_value(sr).unwrap_or(Value::Null));             o.insert("x".into(), json!(at.x));@@ -2521,6 +2528,41 @@ mod tests {         }     } +    #[test]+    fn a_graphic_dropped_on_a_frame_fills_it() {+        let mut app = DesignApp::new(Session::new(), Services::default());+        app.run("file.new", json!({})).unwrap();+        let frame = app.run("frame.create", json!({"rect": [36, 36, 136, 136]})).unwrap();+        app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0)));+        let _ = app.view_mut();+        let (view, rect) = (app.view().unwrap(), app.canvas_rect.unwrap());+        let st = app.session.active().unwrap();+        let layout = designcraft_tools::CanvasLayout::new(&st.doc, false);+        let slot = layout.slot(designcraft_doc::SpreadRef::Doc(0)).unwrap();+        let xf = canvas::Xf::new(rect, view);+        let inside = xf.to_screen(slot.to_canvas(designcraft_geom::Point::new(80.0, 80.0)));+        let outside = xf.to_screen(slot.to_canvas(designcraft_geom::Point::new(300.0, 300.0)));+        let path = std::env::temp_dir().join(format!("designcraft-ui-{}-dropframe.png", std::process::id()));+        std::fs::write(&path, designcraft_render::Rendered { width: 10, height: 10, pixels: vec![200; 400] }.to_png()).unwrap();+        let r = app.open_dropped_at(&path.to_string_lossy(), Some(inside));+        let _ = std::fs::remove_file(&path);+        let placed = r.unwrap();+        assert_eq!(placed["id"], frame["id"], "the frame under the pointer is filled");+        let d = &app.session.doc().unwrap().doc;+        assert_eq!(d.spreads[0].items.len(), 1, "no new object");+        assert!(matches!(d.item(designcraft_doc::ItemId(frame["id"].as_u64().unwrap())).unwrap().content, designcraft_doc::Content::Graphic(_)));+        assert!(canvas::drop_frame(&app, Some(outside)).is_none(), "an empty spot on the page is no frame");+        // Text dropped there gets a frame of its own and leaves the image in place.+        let text = std::env::temp_dir().join(format!("designcraft-ui-{}-dropframe.txt", std::process::id()));+        std::fs::write(&text, "Dropped words").unwrap();+        let r = app.open_dropped_at(&text.to_string_lossy(), Some(inside));+        let _ = std::fs::remove_file(&text);+        r.unwrap();+        let d = &app.session.doc().unwrap().doc;+        assert_eq!(d.spreads[0].items.len(), 2, "the text has a new frame");+        assert!(matches!(d.item(designcraft_doc::ItemId(frame["id"].as_u64().unwrap())).unwrap().content, designcraft_doc::Content::Graphic(_)));+    }+     #[test]     fn a_file_dropped_while_a_parent_is_open_lands_on_the_parent() {         let mut app = DesignApp::new(Session::new(), Services::default());