From 7d0ecd2294e0bd1a4afe301cc2d116ea582c40b9 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:17:28 +0000 Subject: [PATCH] Place files dropped together on the canvas side by side Fixes #886 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/home_tests.rs | 27 +++++++++++++++++++++++++++ crates/ui-egui/src/pending_open.rs | 22 ++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/home_tests.rs b/crates/ui-egui/src/home_tests.rs index 0606c2e5..c74c35bf 100644 --- a/crates/ui-egui/src/home_tests.rs +++ b/crates/ui-egui/src/home_tests.rs @@ -699,6 +699,33 @@ fn dropping_a_native_file_on_canvas_keeps_its_receiving_document_after_tab_switc assert_eq!(u.app.session.docs.iter().map(|doc| doc.doc.pages.len()).collect::>(), [2, 1]); } +#[test] +fn dropping_several_files_on_canvas_places_them_side_by_side() { + let dir = Temp::new(); + let svg = r##""##; + let paths: Vec<_> = ["a.svg", "b.svg", "c.svg"].iter().map(|n| dir.path(n)).collect(); + for path in &paths { + std::fs::write(path, svg).unwrap(); + } + let mut u = Ui::new(Session::new()); + u.app.ui.home = false; + u.frame(vec![]); + let pos = u.app.canvas.rect.center(); + let files: Vec = paths.iter().map(|p| std::sync::Arc::new(Dropped(p.clone())) as egui::DroppedFileHandle).collect(); + u.frame_raw(egui::RawInput { events: vec![Event::PointerMoved(pos)], dropped_files: files, ..Default::default() }); + for _ in 0..4 { + u.frame(vec![]); + } + assert!(u.app.ui.pending_opens.is_empty()); + let doc = u.app.session.doc().unwrap(); + let page = u.app.session.page().unwrap(); + let rects: Vec<_> = doc.node(page).unwrap().children.iter().map(|c| doc.world_bounds(*c).unwrap()).collect(); + assert_eq!(rects.len(), 3); + for pair in rects.windows(2) { + assert!(pair[1].x0 > pair[0].x1, "left to right with a gap: {rects:?}"); + } +} + #[test] fn canvas_import_uses_queued_page_and_restores_the_page_selected_while_waiting() { let dir = Temp::new(); diff --git a/crates/ui-egui/src/pending_open.rs b/crates/ui-egui/src/pending_open.rs index 9575a1ea..501367c0 100644 --- a/crates/ui-egui/src/pending_open.rs +++ b/crates/ui-egui/src/pending_open.rs @@ -75,6 +75,9 @@ fn cancel(app: &mut FigApp, message: String) { app.show_error(message); } +/// Space between files dropped together on the canvas. +const IMPORT_GAP: f64 = 20.0; + /// Frames the card is drawn before the files open: egui lays a new card out invisibly on its first /// frame, so the second one is the first the window shows. const FRAMES_BEFORE_OPENING: u8 = 2; @@ -122,11 +125,26 @@ pub fn pending_opens(app: &mut FigApp, ctx: &egui::Context) { ctx.request_repaint(); if app.ui.pending_open_frames >= FRAMES_BEFORE_OPENING && !reading { app.ui.pending_open_frames = 0; - for pending in std::mem::take(&mut app.ui.pending_opens) { + // Files dropped together land side by side: each starts where the previous one ends. + let mut next_x: Option = None; + for mut pending in std::mem::take(&mut app.ui.pending_opens) { let command = pending.command; - if run_pending(app, pending).is_some() && command == "file.open" { + let canvas = command == "file.import" && pending.params.get("x").is_some(); + if canvas && let Some(x) = next_x { + pending.params["x"] = json!(x); + } + let page = pending.target.as_ref().map(|t| (t.uid, t.page)); + let result = run_pending(app, pending); + if result.is_some() && command == "file.open" { app.ui.home = false; } + if canvas { + let right = result.as_ref().and_then(|r| r.get("id")?.as_u64()).and_then(|id| { + let doc = &app.session.docs.iter().find(|d| Some(d.uid) == page.map(|p| p.0))?.doc; + doc.world_bounds(NodeId(id)).map(|b| b.x1) + }); + next_x = right.map(|r| r + IMPORT_GAP).or(next_x); + } } return; }