From 267917b8de223df327e336e79c4ca646eda4de1e Mon Sep 17 00:00:00 2001 From: texjer Date: Wed, 4 Nov 2026 19:56:22 -0500 Subject: [PATCH] Place: drops that arrive before one UI pass stay separate All pending drops went into one batch, so two single images counted as several: both queued and neither filled the frame it was dropped on. Files of one raw input now share a batch, and each batch is opened on its own. --- crates/ui-egui/src/app/input.rs | 13 +++++++------ crates/ui-egui/src/lib.rs | 3 +++ crates/ui-egui/src/tests.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/app/input.rs b/crates/ui-egui/src/app/input.rs index 465eeaaff..fe5fb673b 100644 --- a/crates/ui-egui/src/app/input.rs +++ b/crates/ui-egui/src/app/input.rs @@ -62,10 +62,11 @@ impl DesignApp { // A drop without pointer events (dragged in from another application) asks the // system now, while the pointer is still where the files were let go. let system = if pointer.is_none() && !raw.dropped_files.is_empty() { self.system_pointer(ctx, raw) } else { None }; + let batch = self.pending_drops.last().map_or(0, |d| d.batch.wrapping_add(1)); for file in std::mem::take(&mut raw.dropped_files) { let path = file.path().to_string_lossy().to_string(); if !path.is_empty() { - self.pending_drops.push(PendingDrop { path, pointer, system, generation }); + self.pending_drops.push(PendingDrop { path, pointer, system, batch, generation }); } } } @@ -154,12 +155,12 @@ impl DesignApp { // suppress later files that belong to the same captured batch. let current = self.input_generation(); let drops: Vec<_> = std::mem::take(&mut self.pending_drops).into_iter().filter(|d| d.generation == current).collect(); - if drops.is_empty() { - return; - } // Dropped together, they go together: several images load the place cursor at once. Each - // file keeps the pointer it was dropped at, or the system's from when it arrived. - self.open_dropped_batch(drops.into_iter().map(|d| (d.path, d.pointer.or(d.system))).collect()); + // file keeps the pointer it was dropped at, or the system's from when it arrived. Separate + // drops stay separate, so a single image still fills the frame it was dropped on. + for batch in drops.chunk_by(|a, b| a.batch == b.batch) { + self.open_dropped_batch(batch.iter().map(|d| (d.path.clone(), d.pointer.or(d.system))).collect()); + } } /// Where the pointer is in the window, asked of the system ([`crate::Services::pointer`]), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 531a8dbe7..239127c3b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -115,6 +115,9 @@ struct PendingDrop { /// for a drop that brought no pointer events ([`crate::Services::pointer`]). Each drop keeps /// its own, converted with the window's place and the UI scale of that input. system: Option, + /// Which drop this file came with: files of one raw input share it, so drops that arrive + /// before the same UI pass stay separate. + batch: u64, generation: InputGeneration, } diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs index 5692ba7fe..39534b0eb 100644 --- a/crates/ui-egui/src/tests.rs +++ b/crates/ui-egui/src/tests.rs @@ -1011,6 +1011,37 @@ fn a_pointerless_drop_keeps_the_system_pointer_of_its_own_input() { assert!(d.assets[&g.asset].name.contains("drop-own-input")); } +/// Two drops that arrive before one UI pass stay separate: the first still fills the frame it was +/// dropped on, and the second, off any frame, loads the place cursor. Merged, they'd both queue. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn separate_drops_before_one_pass_are_not_one_batch() { + let (mut app, frame, inside, outside) = app_with_drop_frame(); + let at = std::rc::Rc::new(std::cell::Cell::new(inside)); + let reported = at.clone(); + app.services.pointer = Some(Box::new(move || Some(reported.get()))); + let ctx = egui::Context::default(); + let window = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0)); + let (first, second) = (dropped_png("separate-drop-a"), dropped_png("separate-drop-b")); + let mut raw = pointerless_drop(&first, window); + app.raw_input_hook(&ctx, &mut raw); + at.set(outside); + let mut raw = pointerless_drop(&second, window); + app.raw_input_hook(&ctx, &mut raw); + assert_eq!(app.pending_drops.len(), 2); + ctx.run_ui(raw, |_| app.take_pending_drops()).textures_delta.clear(); + let _ = std::fs::remove_file(&first); + let _ = std::fs::remove_file(&second); + let d = &app.session.doc().unwrap().doc; + let designcraft_doc::Content::Graphic(g) = &d.item(designcraft_doc::ItemId(frame)).unwrap().content else { + panic!("the first drop fills the frame it was dropped on") + }; + assert!(d.assets[&g.asset].name.contains("separate-drop-a")); + let loaded = app.session.loaded.as_ref().expect("the second drop loads the place cursor"); + assert!(d.assets[&loaded.asset].name.contains("separate-drop-b")); + assert!(app.session.loaded_queue.is_empty()); +} + /// A filled compound path (Object > Compound Paths > Make) is a frame a dropped image fills, as /// in InDesign, through its ring and not its hole. #[test]