Merge pull request #7480 from dotCooCoo/fix/document-drag-profile-switch

Document tabs: art dragged onto a document with other assigned profiles drops there
This commit is contained in:
BFlat
2026-11-14 16:49:54 +07:00
committed by GitHub
2 changed files with 113 additions and 6 deletions
+13 -6
View File
@@ -1,8 +1,10 @@
//! A selection dragged across document tabs remains editable and leaves its source intact.
use std::sync::Arc;
use egui::{Context, Id, Response};
use serde_json::json;
use vectorcraft_doc::NodeId;
use vectorcraft_doc::{Document, NodeId};
use vectorcraft_geom::Point;
use crate::{VectorcraftApp, canvas, widgets};
@@ -19,7 +21,11 @@ struct Origin {
struct Artwork {
origin: Origin,
ids: Vec<NodeId>,
revision: u64,
/// The source document when the drag left it. The drag ends when the source's content differs
/// from it ([`vectorcraft_engine::DocState::same_content`]). A redraw that raises its revision
/// (another document's assigned color profiles brought to the front) and a change that is not
/// an edit (its type measured again) keep it.
source: Arc<Document>,
target: u64,
}
@@ -75,7 +81,7 @@ pub(crate) fn cleanup(app: &VectorcraftApp, ctx: &Context) {
}
});
if let Some(art) = egui::DragAndDrop::payload::<Artwork>(ctx) {
let valid = app.session.documents().iter().any(|d| d.uid == art.origin.uid && d.revision == art.revision)
let valid = app.session.documents().iter().any(|d| d.uid == art.origin.uid && d.same_content(&art.source))
&& app.session.active().is_some_and(|d| d.uid == art.target);
let cancel = ctx.input(|i| {
!i.focused
@@ -143,13 +149,14 @@ pub(crate) fn hover_tab(app: &mut VectorcraftApp, resp: &Response, win: crate::d
return true;
}
// Alt-drag previews use temporary duplicate ids; only the restored originals travel.
let ids = app.session.active().map(|d| d.selection.objects.clone()).unwrap_or_default();
let Some((ids, source)) = app.session.active().map(|d| (d.selection.objects.clone(), d.doc.clone())) else {
return true;
};
if ids.is_empty() {
return true;
}
canvas::end_document_drag(ctx);
let revision = app.session.active().map(|d| d.revision).unwrap_or_default();
egui::DragAndDrop::set_payload(ctx, Artwork { origin, ids, revision, target: uid });
egui::DragAndDrop::set_payload(ctx, Artwork { origin, ids, source, target: uid });
}
if transfer && let Some(art) = egui::DragAndDrop::payload::<Artwork>(ctx) {
let mut art = (*art).clone();
+100
View File
@@ -291,6 +291,54 @@ fn hovering_back_to_source_resumes_move_instead_of_copying() {
assert!((bounds.x0 - 150.0).abs() < 0.001 && (bounds.y0 - 150.0).abs() < 0.001, "{bounds:?}");
}
/// Back in the source tab, the move resumes even when coming to the front measured the source's
/// type again in its own composite fonts, which replaces the document without editing it.
#[test]
fn hovering_back_to_a_source_whose_type_is_measured_again_resumes_move() {
let font = |roman: u32| {
json!({
"name": "CF Tab Drag",
"parts": {"kanji": {"font": "Source Sans 3"}, "roman": {"font": "JetBrains Mono", "size": roman}},
})
};
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width":400,"height":300,"name":"Source"})).unwrap();
app.run("type.compositeFonts.set", json!({"fonts": [font(100)]})).unwrap();
app.run("text.create", json!({"x": 200, "y": 250, "text": "Ab12", "size": 20, "font": "CF Tab Drag"})).unwrap();
let id = vectorcraft_doc::NodeId(app.run("shape.rectangle", json!({"x":50,"y":50,"width":100,"height":100})).unwrap()["id"].as_u64().unwrap());
app.run("file.new", json!({"width":400,"height":300,"name":"Target"})).unwrap();
app.run("type.compositeFonts.set", json!({"fonts": [font(50)]})).unwrap();
app.session.set_active(0);
let undo = app.session.active().unwrap().history.undo.len();
app.select_tool("selection");
app.ui.task_bar = false;
app.ui.view.rulers = false;
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
frame(&mut app, &ctx, 0.0, vec![]);
let tabs = frame(&mut app, &ctx, 0.01, vec![]);
let grab = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(75.0, 80.0));
frame(&mut app, &ctx, 0.05, vec![Event::PointerMoved(grab), button(grab, true)]);
frame(&mut app, &ctx, 0.1, vec![Event::PointerMoved(grab + egui::vec2(20.0, 10.0))]);
frame(&mut app, &ctx, 0.2, vec![Event::PointerMoved(tabs[1].center())]);
frame(&mut app, &ctx, 0.25, vec![]);
frame(&mut app, &ctx, 0.8, vec![]);
assert_eq!(app.session.active_index(), Some(1));
let left = app.session.documents()[0].doc.clone();
frame(&mut app, &ctx, 0.9, vec![Event::PointerMoved(tabs[0].center())]);
frame(&mut app, &ctx, 1.4, vec![]);
assert_eq!(app.session.active_index(), Some(0));
assert!(!std::sync::Arc::ptr_eq(&app.session.documents()[0].doc, &left), "the source's type is measured again");
let drop = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(175.0, 180.0));
frame(&mut app, &ctx, 1.5, vec![Event::PointerMoved(drop)]);
frame(&mut app, &ctx, 1.6, vec![button(drop, false)]);
let st = app.session.active().unwrap();
assert_eq!(st.selection.objects, vec![id]);
assert_eq!(st.history.undo.len(), undo + 1);
let bounds = st.doc.node(id).unwrap().geometric_bounds().unwrap();
assert!((bounds.x0 - 150.0).abs() < 0.001 && (bounds.y0 - 150.0).abs() < 0.001, "{bounds:?}");
}
/// Verify Alt-drag transfers restored originals across tabs without retaining preview duplicates.
#[test]
fn alt_drag_can_continue_across_multiple_tabs_without_copying_the_source_preview() {
@@ -380,6 +428,58 @@ fn source_edits_target_closure_switches_and_focus_loss_cancel_stale_transfers()
}
}
/// Artwork held over the tab of a document with other assigned color profiles switches to it and
/// drops there: bringing those profiles to the front redraws the source without editing it.
#[test]
fn a_drop_on_a_document_with_other_assigned_profiles_copies_the_artwork() {
const CHILD: &str = "VECTORCRAFT_TEST_DRAG_ASSIGNED_PROFILES";
if std::env::var_os(CHILD).is_none() {
// Color settings are process-wide: change them in a separate test process.
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "tests::document_drag::a_drop_on_a_document_with_other_assigned_profiles_copies_the_artwork", "--nocapture"])
.env(CHILD, "1")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
"isolated drag test failed:\n{stdout}\n{}",
String::from_utf8_lossy(&output.stderr),
);
return;
}
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width":400,"height":300,"name":"Source"})).unwrap();
app.run("shape.rectangle", json!({"x":50,"y":50,"width":100,"height":100})).unwrap();
app.run("file.new", json!({"width":400,"height":300,"name":"Target"})).unwrap();
app.run("edit.assignProfile", json!({"cmyk": vectorcraft_color::cms::DEVICE_CMYK})).unwrap();
app.session.set_active(0);
let source = app.session.active().unwrap().doc.clone();
app.select_tool("selection");
app.ui.task_bar = false;
app.ui.view.rulers = false;
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
frame(&mut app, &ctx, 0.0, vec![]);
let tabs = frame(&mut app, &ctx, 0.01, vec![]);
let grab = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(75.0, 80.0));
frame(&mut app, &ctx, 0.05, vec![Event::PointerMoved(grab), button(grab, true)]);
frame(&mut app, &ctx, 0.1, vec![Event::PointerMoved(grab + egui::vec2(20.0, 10.0))]);
assert!(app.session.in_interaction());
frame(&mut app, &ctx, 0.2, vec![Event::PointerMoved(tabs[1].center())]);
frame(&mut app, &ctx, 0.25, vec![]);
frame(&mut app, &ctx, 0.8, vec![]);
assert_eq!(app.session.active_index(), Some(1));
let undo = app.session.active().unwrap().history.undo.len();
let drop = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(175.0, 180.0));
frame(&mut app, &ctx, 0.9, vec![Event::PointerMoved(drop)]);
frame(&mut app, &ctx, 1.0, vec![button(drop, false)]);
let placed = app.session.active().unwrap().selection.objects.len();
let steps = app.session.active().unwrap().history.undo.len();
assert_eq!((placed, steps), (1, undo + 1), "the drop places the artwork");
assert_eq!(app.session.documents()[0].doc, source);
}
/// Verify tab-strip relayout under a stationary pointer cannot trigger another document switch.
#[test]
fn scrolling_tabs_under_a_stationary_drag_does_not_cascade_to_other_documents() {