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
@@ -1247,6 +1247,7 @@ dependencies = [ "designcraft-geom", "designcraft-idml", "designcraft-render",+ "designcraft-textimport", "designcraft-tools", "egui", "egui_kittest",@@ -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 }@@ -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()?;@@ -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());