Skip to content

Clipboard: Cross-application copy-paste support - #4499

Open
VimYoung wants to merge 8 commits into
GraphiteEditor:masterfrom
VimYoung:clipboard-svg-fix
Open

Clipboard: Cross-application copy-paste support#4499
VimYoung wants to merge 8 commits into
GraphiteEditor:masterfrom
VimYoung:clipboard-svg-fix

Conversation

@VimYoung

@VimYoung VimYoung commented Sep 3, 2026

Copy link
Copy Markdown

Description

This PR aims to resolve #2373. Solution's approach has been thoroughly discussed in discord's development channel. The PR will fix this issue by introducing copy of both text and svg+xml mime types when copying a selection to the clipboard. Essentially making it possible for other apps to take the svg representation while graphite picks up the internal json representation for copy pasting.

Notes

  1. As mentioned by hypercube, image/svg+xml is not supported by firefox and safari Stable so that is handled.
  2. The intersecting by desktop still doesn't copy the svg and simply ignores it.

Following example showcase the same copy paste not working across apps (krita and inkscape shown as the other apps) in current build vs it working in this branch while still being compatible across graphite tabs.

Before(Only across graphite tabs, not across apps):

editor_before.mp4

After(Across tabs and other apps):

editor_after.mp4

@VimYoung
VimYoung marked this pull request as ready for review September 6, 2026 10:12

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 8 files

Confidence score: 2/5

  • In editor/src/node_graph_executor/runtime.rs, selecting an artboard produces an empty SVG because its monitor output is ignored, risking incorrect or missing clipboard/rendered output; render artboards while preserving location, dimensions, and clipping.
  • In editor/src/node_graph_executor/runtime.rs, run() can silently drop CopySvgTextClipboard when it is batched with an ExecutionRequest, so clipboard copying becomes unreliable; process both requests without returning before the clipboard operation.
  • The layer-copy path in editor/src/messages/clipboard/clipboard_message_handler.rs now routes through a runtime-generated frontend message that EditorTestUtils::handle_message discards, breaking the existing clipboard-copy test flow; preserve or explicitly handle the clipboard write message in tests.
  • In frontend/src/managers/clipboard.ts, calling ClipboardItem.supports() without checking that the method exists breaks browsers such as Safari before 18.4; guard the capability check and keep the fallback path. Separately, editor/src/node_graph_executor.rs can panic on clipboard-send failure via .expect(...); handle the error with logging instead.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/node_graph_executor.rs">

<violation number="1" location="editor/src/node_graph_executor.rs:820">
P3: `copy_svg_clipboard` panics the whole application on send failure via `.expect(...)`. Match the codebase's preferred non-panicking style for this new public entry point: handle the send result with a log (e.g., `if let Err(e) = self.runtime_io.send(...) { log::error!(...) }`) instead of asserting, so a runtime-IO failure doesn't crash the editor.</violation>
</file>

<file name="editor/src/node_graph_executor/runtime.rs">

<violation number="1" location="editor/src/node_graph_executor/runtime.rs:203">
P1: When a `CopySvgTextClipboard` request is batched with an `ExecutionRequest` in the same `run()` pass, the copy is silently dropped. The `ExecutionRequest` arm ends with `return texture;`, and `svg_clipboard` is ordered after `execution` in the requests array, so the early return exits before the copy is processed. Since `run()` drains all queued requests each frame, a copy issued while a viewport render is queued loses the clipboard write with no response sent. Process `svg_clipboard` before the execution arm's early return (e.g., place it earlier in the array), or handle it before the `return`.</violation>

<violation number="2" location="editor/src/node_graph_executor/runtime.rs:351">
P1: When the selection contains an artboard, this branch ignores its `List<Artboard>` monitor output and sends an empty SVG. Render artboard outputs as SVG too, preserving their location, dimensions, and clipping.</violation>
</file>

<file name="editor/src/messages/clipboard/clipboard_message_handler.rs">

<violation number="1" location="editor/src/messages/clipboard/clipboard_message_handler.rs:93">
P2: This changes layer copy from a synchronous `TriggerClipboardWrite` into a runtime request whose generated frontend message is discarded by `EditorTestUtils::handle_message`. As a result, the existing clipboard copy tests fail at their `.expect`; propagate the asynchronous clipboard response through the test/runtime API or preserve a directly observable write result.</violation>
</file>

<file name="frontend/src/managers/clipboard.ts">

<violation number="1" location="frontend/src/managers/clipboard.ts:29">
P2: On browsers where the `ClipboardItem` interface exists but the static `supports()` method is unavailable (e.g. Safari before 18.4, which shipped `ClipboardItem` in 13.1 but `supports()` only in 18.4), `ClipboardItem.supports(...)` throws a TypeError before the `else` fallback runs, so even the `writeText` fallback never executes and the copy fails entirely. Guard the feature detection (e.g., `ClipboardItem.supports?.(...)` or wrap in try/catch, as the linked MDN example does) so non-supporting browsers fall back to `writeText`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

return texture;
}
GraphRuntimeRequest::CopySvgTextClipboard(text_string_clipboard, selected_node_ids) => {
let mut combined_graphics = List::<Graphic>::new();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When the selection contains an artboard, this branch ignores its List<Artboard> monitor output and sends an empty SVG. Render artboard outputs as SVG too, preserving their location, dimensions, and clipping.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/node_graph_executor/runtime.rs, line 351:

<comment>When the selection contains an artboard, this branch ignores its `List<Artboard>` monitor output and sends an empty SVG. Render artboard outputs as SVG too, preserving their location, dimensions, and clipping.</comment>

<file context>
@@ -340,6 +347,59 @@ impl NodeRuntime {
 					return texture;
 				}
+				GraphRuntimeRequest::CopySvgTextClipboard(text_string_clipboard, selected_node_ids) => {
+					let mut combined_graphics = List::<Graphic>::new();
+
+					for monitor_node_path in &self.monitor_nodes {
</file context>

}

let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
let requests = [preferences, graph, eyedropper, execution, svg_clipboard].into_iter().flatten();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a CopySvgTextClipboard request is batched with an ExecutionRequest in the same run() pass, the copy is silently dropped. The ExecutionRequest arm ends with return texture;, and svg_clipboard is ordered after execution in the requests array, so the early return exits before the copy is processed. Since run() drains all queued requests each frame, a copy issued while a viewport render is queued loses the clipboard write with no response sent. Process svg_clipboard before the execution arm's early return (e.g., place it earlier in the array), or handle it before the return.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/node_graph_executor/runtime.rs, line 203:

<comment>When a `CopySvgTextClipboard` request is batched with an `ExecutionRequest` in the same `run()` pass, the copy is silently dropped. The `ExecutionRequest` arm ends with `return texture;`, and `svg_clipboard` is ordered after `execution` in the requests array, so the early return exits before the copy is processed. Since `run()` drains all queued requests each frame, a copy issued while a viewport render is queued loses the clipboard write with no response sent. Process `svg_clipboard` before the execution arm's early return (e.g., place it earlier in the array), or handle it before the `return`.</comment>

<file context>
@@ -193,7 +200,7 @@ impl NodeRuntime {
 		}
 
-		let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
+		let requests = [preferences, graph, eyedropper, execution, svg_clipboard].into_iter().flatten();
 
 		for request in requests {
</file context>
Suggested change
let requests = [preferences, graph, eyedropper, execution, svg_clipboard].into_iter().flatten();
let requests = [preferences, graph, svg_clipboard, eyedropper, execution].into_iter().flatten();

responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
ClipboardContent::Graphite(graphite) => {
let graphite_json = format!("{CLIPBOARD_PREFIX}{graphite}");
responses.add(PortfolioMessage::RequestSvgTextCopy { graphite_json });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This changes layer copy from a synchronous TriggerClipboardWrite into a runtime request whose generated frontend message is discarded by EditorTestUtils::handle_message. As a result, the existing clipboard copy tests fail at their .expect; propagate the asynchronous clipboard response through the test/runtime API or preserve a directly observable write result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/clipboard/clipboard_message_handler.rs, line 93:

<comment>This changes layer copy from a synchronous `TriggerClipboardWrite` into a runtime request whose generated frontend message is discarded by `EditorTestUtils::handle_message`. As a result, the existing clipboard copy tests fail at their `.expect`; propagate the asynchronous clipboard response through the test/runtime API or preserve a directly observable write result.</comment>

<file context>
@@ -80,19 +80,22 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
-				responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
+					ClipboardContent::Graphite(graphite) => {
+						let graphite_json = format!("{CLIPBOARD_PREFIX}{graphite}");
+						responses.add(PortfolioMessage::RequestSvgTextCopy { graphite_json });
+					}
+					ClipboardContent::Text(text) => {
</file context>


subscriptions.subscribeFrontendMessage("TriggerClipboardSvgWrite", (data) => {
// Adopted from https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem#browser_compatibility
if (ClipboardItem.supports("image/svg+xml")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: On browsers where the ClipboardItem interface exists but the static supports() method is unavailable (e.g. Safari before 18.4, which shipped ClipboardItem in 13.1 but supports() only in 18.4), ClipboardItem.supports(...) throws a TypeError before the else fallback runs, so even the writeText fallback never executes and the copy fails entirely. Guard the feature detection (e.g., ClipboardItem.supports?.(...) or wrap in try/catch, as the linked MDN example does) so non-supporting browsers fall back to writeText.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/managers/clipboard.ts, line 29:

<comment>On browsers where the `ClipboardItem` interface exists but the static `supports()` method is unavailable (e.g. Safari before 18.4, which shipped `ClipboardItem` in 13.1 but `supports()` only in 18.4), `ClipboardItem.supports(...)` throws a TypeError before the `else` fallback runs, so even the `writeText` fallback never executes and the copy fails entirely. Guard the feature detection (e.g., `ClipboardItem.supports?.(...)` or wrap in try/catch, as the linked MDN example does) so non-supporting browsers fall back to `writeText`.</comment>

<file context>
@@ -23,13 +23,28 @@ export function createClipboardManager(subscriptions: SubscriptionsRouter, edito
+
+	subscriptions.subscribeFrontendMessage("TriggerClipboardSvgWrite", (data) => {
+		// Adopted from https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem#browser_compatibility
+		if (ClipboardItem.supports("image/svg+xml")) {
+			navigator.clipboard?.write?.([
+				new ClipboardItem({
</file context>

Comment on lines +820 to +823
pub fn copy_svg_clipboard(&self, graphite_json: String, selected_nodes: Vec<NodeId>) {
self.runtime_io
.send(GraphRuntimeRequest::CopySvgTextClipboard(graphite_json, selected_nodes))
.expect("Failed to send runtime request");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: copy_svg_clipboard panics the whole application on send failure via .expect(...). Match the codebase's preferred non-panicking style for this new public entry point: handle the send result with a log (e.g., if let Err(e) = self.runtime_io.send(...) { log::error!(...) }) instead of asserting, so a runtime-IO failure doesn't crash the editor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/node_graph_executor.rs, line 820:

<comment>`copy_svg_clipboard` panics the whole application on send failure via `.expect(...)`. Match the codebase's preferred non-panicking style for this new public entry point: handle the send result with a log (e.g., `if let Err(e) = self.runtime_io.send(...) { log::error!(...) }`) instead of asserting, so a runtime-IO failure doesn't crash the editor.</comment>

<file context>
@@ -812,6 +816,12 @@ impl NodeGraphExecutor {
 		Ok(())
 	}
+
+	pub fn copy_svg_clipboard(&self, graphite_json: String, selected_nodes: Vec<NodeId>) {
+		self.runtime_io
+			.send(GraphRuntimeRequest::CopySvgTextClipboard(graphite_json, selected_nodes))
</file context>
Suggested change
pub fn copy_svg_clipboard(&self, graphite_json: String, selected_nodes: Vec<NodeId>) {
self.runtime_io
.send(GraphRuntimeRequest::CopySvgTextClipboard(graphite_json, selected_nodes))
.expect("Failed to send runtime request");
pub fn copy_svg_clipboard(&self, graphite_json: String, selected_nodes: Vec<NodeId>) {
if let Err(error) = self.runtime_io.send(GraphRuntimeRequest::CopySvgTextClipboard(graphite_json, selected_nodes)) {
log::error!("Failed to send runtime request: {error:?}");
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copy/pasting vector and raster content between other graphics apps

1 participant