mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 15:57:49 +00:00
Document tabs: art dragged onto a document with other assigned profiles drops there
A drag of art across the document tabs ended whenever the source document's revision changed. Since #7173, bringing a document with other assigned color profiles to the front raises every open document's revision so that each redraws in the new profiles, so holding art over such a tab switched to it and then canceled the drag before the drop. The drag now keeps the source document it left and ends only when that document's content changes (DocState::same_content). A redraw keeps it, and so does type measured again when the source comes back to the front, which replaces the document without editing it.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user