mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Fix #359: web drops land in the document they were dropped on
The web reads dropped files asynchronously, and the place inbox only kept the drop point, so a file dropped on document A was placed into whatever document was active when its bytes arrived. A drop on a canvas now carries place::DropAt (the document's uid, the point, embed), and drop_files makes that document active again before placing; when it has closed meanwhile, nothing is placed and the status bar says so. The native drop path is synchronous and goes through the same code, so it is unchanged in effect. Test: a drop pending while another document opens lands in the first one at the drop point; one whose document closes is not placed anywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7485dac6e8
commit
accfec7146
+33
-13
@@ -18,20 +18,30 @@ const THUMB: u32 = 64;
|
||||
const THUMB_OFFSET: egui::Vec2 = egui::vec2(14.0, 14.0);
|
||||
|
||||
/// A file to place that arrived asynchronously (web): picked for File → Place (`drop: None`), or
|
||||
/// dropped on the canvas at a point, embedded or not.
|
||||
/// dropped on a canvas.
|
||||
pub struct PlaceArrival {
|
||||
pub name: String,
|
||||
pub bytes: Vec<u8>,
|
||||
pub drop: Option<(Point, bool)>,
|
||||
pub drop: Option<DropAt>,
|
||||
}
|
||||
|
||||
pub type PlaceInbox = Arc<Mutex<Vec<PlaceArrival>>>;
|
||||
|
||||
/// Where files dropped on a canvas land: document `doc` ([`vectorcraft_engine::DocState::uid`]) at
|
||||
/// `at` (document coordinates), embedded when Shift was held. The document is kept because the web
|
||||
/// reads dropped files asynchronously: another document may have become active when they arrive.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct DropAt {
|
||||
pub doc: u64,
|
||||
pub at: Point,
|
||||
pub embed: bool,
|
||||
}
|
||||
|
||||
/// Where files dropped on the window go.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum DropTarget {
|
||||
/// The active document's canvas at `at` (document coordinates); `embed` when Shift is held.
|
||||
Place { at: Point, embed: bool },
|
||||
/// The canvas of the document they were dropped on.
|
||||
Place(DropAt),
|
||||
/// Opened as documents: no document is open, or the drop missed the canvas (the tab bar).
|
||||
Open,
|
||||
}
|
||||
@@ -270,21 +280,31 @@ impl VectorcraftApp {
|
||||
/// doesn't tell) go: onto the canvas of the open document (Shift embeds them), else opened. An
|
||||
/// unknown position counts as the middle of the view.
|
||||
pub fn drop_target(&self, pos: Option<egui::Pos2>, shift: bool) -> DropTarget {
|
||||
let (Some(rect), Some(v)) = (self.canvas_rect.filter(|_| self.session.active().is_some()), self.view()) else { return DropTarget::Open };
|
||||
match pos {
|
||||
Some(p) if !rect.contains(p) => DropTarget::Open,
|
||||
Some(p) => DropTarget::Place { at: crate::canvas::Xf::new(rect, v).to_doc(p), embed: shift },
|
||||
None => DropTarget::Place { at: v.center, embed: shift },
|
||||
}
|
||||
let (Some(rect), Some(st), Some(v)) = (self.canvas_rect, self.session.active(), self.view()) else { return DropTarget::Open };
|
||||
let at = match pos {
|
||||
Some(p) if !rect.contains(p) => return DropTarget::Open,
|
||||
Some(p) => crate::canvas::Xf::new(rect, v).to_doc(p),
|
||||
None => v.center,
|
||||
};
|
||||
DropTarget::Place(DropAt { doc: st.uid, at, embed: shift })
|
||||
}
|
||||
}
|
||||
|
||||
/// Files dropped on the window (`(name, path, bytes)`): placed on the canvas or opened (and added
|
||||
/// to Open Recent Files), as `target` says.
|
||||
/// to Open Recent Files), as `target` says. Files for a canvas go to the document they were
|
||||
/// dropped on, which becomes active again; none are placed when it has closed since.
|
||||
pub fn drop_files(app: &mut VectorcraftApp, files: Vec<(String, Option<String>, Vec<u8>)>, target: DropTarget) {
|
||||
if let DropTarget::Place(d) = target {
|
||||
let Some(i) = app.session.documents().iter().position(|st| st.uid == d.doc) else {
|
||||
let names: Vec<&str> = files.iter().map(|(name, ..)| name.as_str()).collect();
|
||||
app.status(format!("Couldn't place {}: the document it was dropped on was closed", names.join(", ")));
|
||||
return;
|
||||
};
|
||||
app.session.set_active(i);
|
||||
}
|
||||
for (name, path, bytes) in files {
|
||||
let r = match target {
|
||||
DropTarget::Place { at, embed } => {
|
||||
DropTarget::Place(DropAt { at, embed, .. }) => {
|
||||
let mut p = match &path {
|
||||
Some(path) => json!({ "path": path }),
|
||||
None => json!({ "name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes) }),
|
||||
@@ -313,7 +333,7 @@ pub fn drain(app: &mut VectorcraftApp) {
|
||||
let mut picked = vec![];
|
||||
for a in arrived {
|
||||
match a.drop {
|
||||
Some((at, embed)) => drop_files(app, vec![(a.name, None, a.bytes)], DropTarget::Place { at, embed }),
|
||||
Some(d) => drop_files(app, vec![(a.name, None, a.bytes)], DropTarget::Place(d)),
|
||||
None => {
|
||||
picked.push(json!({ "name": a.name }));
|
||||
app.place.picked.retain(|(n, _)| *n != a.name);
|
||||
|
||||
@@ -115,6 +115,53 @@ fn files_dropped_off_the_canvas_or_with_no_document_open_and_are_recent() {
|
||||
assert_eq!(app.session.documents().len(), 2);
|
||||
}
|
||||
|
||||
/// The images in open document `i`.
|
||||
fn images(app: &VectorcraftApp, i: usize) -> usize {
|
||||
let mut n = 0;
|
||||
app.session.documents()[i].doc.walk(|node| n += usize::from(matches!(node.kind, NodeKind::Image(_))));
|
||||
n
|
||||
}
|
||||
|
||||
/// The web reads dropped files asynchronously (#359): a file that arrives after another document
|
||||
/// became active still lands where it was dropped, and is dropped itself when that document closed.
|
||||
#[test]
|
||||
fn a_file_read_after_another_document_became_active_lands_where_it_was_dropped() {
|
||||
let mut app = app();
|
||||
let inbox = crate::place::PlaceInbox::default();
|
||||
app.services.place_inbox = Some(inbox.clone());
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![], &[], false);
|
||||
frame(&mut app, &ctx, vec![], &[], false);
|
||||
let rect = app.canvas_rect.expect("the canvas is laid out");
|
||||
let pos = rect.center() + vec2(60.0, -40.0);
|
||||
let drop_on_active = |app: &VectorcraftApp| match app.drop_target(Some(pos), false) {
|
||||
crate::place::DropTarget::Place(d) => d,
|
||||
t => panic!("{t:?}"),
|
||||
};
|
||||
let arrive =
|
||||
|name: &str, drop| inbox.lock().unwrap().push(crate::place::PlaceArrival { name: name.into(), bytes: png(20, 10, 72.0), drop: Some(drop) });
|
||||
// Dropped on A; another document opens while the file is read.
|
||||
let on_a = drop_on_active(&app);
|
||||
let want = Xf::new(rect, app.view().unwrap()).to_doc(pos);
|
||||
app.run("file.new", json!({"width": 600, "height": 500})).unwrap();
|
||||
assert_eq!(app.session.active_index(), Some(1));
|
||||
arrive("late.png", on_a);
|
||||
frame(&mut app, &ctx, vec![], &[], false);
|
||||
assert_eq!(app.session.active_index(), Some(0), "the document it was dropped on is active again");
|
||||
assert_eq!((images(&app, 0), images(&app, 1)), (1, 0));
|
||||
let c = selected_image(&app).geometric_bounds().unwrap().center();
|
||||
assert!((c.x - want.x).abs() < 1e-6 && (c.y - want.y).abs() < 1e-6, "where it was dropped: {c:?} vs {want:?}");
|
||||
// Dropped on B, which closes before the file arrives: nothing is placed.
|
||||
app.run("document.activate", json!({"index": 1})).unwrap();
|
||||
let on_b = drop_on_active(&app);
|
||||
app.run("file.close", json!({})).unwrap();
|
||||
assert_eq!(app.session.documents().len(), 1);
|
||||
arrive("orphan.png", on_b);
|
||||
frame(&mut app, &ctx, vec![], &[], false);
|
||||
assert_eq!(images(&app, 0), 1, "not placed in the document left open");
|
||||
assert!(app.ui.status.contains("orphan.png") && app.ui.status.contains("closed"), "{}", app.ui.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_place_dialog_lists_the_files_and_loads_the_cursor_with_several() {
|
||||
let mut app = app();
|
||||
|
||||
Reference in New Issue
Block a user