Place files dropped together on the canvas side by side

Fixes #886

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-19 19:17:28 +00:00
co-authored by Claude Haiku 5.5
parent a1369310d4
commit 7d0ecd2294
2 changed files with 47 additions and 2 deletions
+27
View File
@@ -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::<Vec<_>>(), [2, 1]);
}
#[test]
fn dropping_several_files_on_canvas_places_them_side_by_side() {
let dir = Temp::new();
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10"><rect width="20" height="10" fill="#f00"/></svg>"##;
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<egui::DroppedFileHandle> = 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();
+20 -2
View File
@@ -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<f64> = 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;
}