From ad64852255f226dd7ae2482b785e9c5ebb895da8 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:54:02 +0300 Subject: [PATCH] Guide clipboard: guides alone stay off the system clipboard; test two pastes and layers Copying only ruler guides published an empty SVG and text to the system clipboard, replacing what other apps put there. The clipboard now offers other apps nothing without art. Tests cover two pastes adding guides of their own on the current layer (following its visibility) and the SVG carrying no guides. --- crates/engine/src/cmd/clipboard.rs | 8 +++- crates/engine/src/cmd/clipboard/flavours.rs | 2 +- crates/engine/src/tests/guide_clipboard.rs | 41 +++++++++++++++++++++ crates/ui-egui/src/sysclip.rs | 5 +++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index c487425d3..cf6718d8e 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -103,6 +103,12 @@ impl Clipboard { self.nodes.is_empty() && self.artboards.is_empty() && self.guides.is_empty() } + /// Does it hold art (objects or artboards) for other apps? Ruler guides stay in VectorCraft: + /// guides alone put nothing on the system clipboard (#3226). + pub fn has_art(&self) -> bool { + !self.nodes.is_empty() || !self.artboards.is_empty() + } + /// Objects `roots` of `st`'s document with their resources, source artboard and layers. pub(crate) fn copy(st: &DocState, roots: &[NodeId]) -> Self { let d = &*st.doc; @@ -198,7 +204,7 @@ impl Clipboard { /// The objects as standalone SVG (`None` when there are none). pub fn svg(&self) -> Option { - if self.is_empty() { + if !self.has_art() { return None; } let d = self.to_document(); diff --git a/crates/engine/src/cmd/clipboard/flavours.rs b/crates/engine/src/cmd/clipboard/flavours.rs index b2e1114e1..f4c096d9b 100644 --- a/crates/engine/src/cmd/clipboard/flavours.rs +++ b/crates/engine/src/cmd/clipboard/flavours.rs @@ -286,7 +286,7 @@ impl Clipboard { /// The MIME types of [`Self::flavours`], without making them. pub fn flavour_types(&self, svg: bool, pdf: bool) -> Vec<&'static str> { - if self.is_empty() { + if !self.has_art() { return vec![]; } let text = (svg && self.image_bytes() <= COPY_TEXT_IMAGE_BYTES) || self.nodes.iter().all(type_only); diff --git a/crates/engine/src/tests/guide_clipboard.rs b/crates/engine/src/tests/guide_clipboard.rs index d71c1dd42..b6fceead0 100644 --- a/crates/engine/src/tests/guide_clipboard.rs +++ b/crates/engine/src/tests/guide_clipboard.rs @@ -88,3 +88,44 @@ fn guides_paste_in_place_onto_another_artboard_and_cut_removes_them() { run(&mut s, "select.none", json!({})); assert!(s.execute("edit.copy", &json!({})).is_err()); } + +/// #3226 review: each paste adds guides of its own (a guide is its place and layer; there is no +/// id to share), on the layer new art goes to, so they take that layer's colour, visibility and +/// lock; and the SVG put on the system clipboard carries no guides. +#[test] +fn two_pastes_add_their_own_guides_on_the_current_layer_and_svg_has_none() { + let mut s = Session::new(); + run(&mut s, "file.new", json!({"width": 400, "height": 300})); + run(&mut s, "guide.add", json!({"vertical": true, "pos": 120})); + run(&mut s, "guide.select", json!({"indexes": [0]})); + run(&mut s, "edit.copy", json!({})); + // Guides alone: no art for the system clipboard's SVG. + assert!(s.clipboard_svg().is_none(), "guides alone put no SVG on the system clipboard"); + assert!(s.clipboard.flavour_types(true, true).is_empty(), "nor any other format (it keeps what other apps put there)"); + let layer = NodeId(run(&mut s, "layer.new", json!({}))["id"].as_u64().unwrap()); + run(&mut s, "edit.paste", json!({})); + assert_eq!(s.doc().unwrap().selection.guides, [1], "the first paste selects its own guide"); + run(&mut s, "edit.paste", json!({})); + assert_eq!(s.doc().unwrap().selection.guides, [2], "the second paste selects its own guide"); + let g = guides(&s); + assert_eq!(g.len(), 3, "two pastes, two more guides: {g:?}"); + assert_eq!((g[1].2, g[2].2), (Some(layer), Some(layer)), "on the layer new art goes to"); + // Moving the second pasted guide leaves the first where it is. + run(&mut s, "guide.move", json!({"index": 2, "pos": 200})); + assert_eq!(guides(&s).iter().map(|g| g.1).collect::>(), [120.0, 120.0, 200.0]); + // Hiding that layer hides the guides pasted on it: they follow its visibility (and lock). + run(&mut s, "layer.setProps", json!({"id": layer.0, "visible": false})); + let st = s.doc().unwrap(); + let shown: Vec = st.doc.guides.iter().map(|g| st.doc.guide_shown(g)).collect(); + assert_eq!(shown, [true, false, false]); + // With an object, the SVG holds the object and no guide. + run(&mut s, "layer.setProps", json!({"id": layer.0, "visible": true})); + let rect = run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}))["id"].as_u64().unwrap(); + run(&mut s, "select.set", json!({"ids": [rect]})); + run(&mut s, "guide.select", json!({"indexes": [0], "toggle": true})); + let r = run(&mut s, "edit.copy", json!({})); + assert_eq!(r["guides"], 1); + let svg = s.clipboard_svg().unwrap(); + assert!(svg.contains("