Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
FrontendMessage::TriggerClipboardRead => {
dispatcher.respond(DesktopFrontendMessage::ClipboardRead);
}
FrontendMessage::TriggerClipboardWrite { content } => {
dispatcher.respond(DesktopFrontendMessage::ClipboardWrite { content });
FrontendMessage::TriggerClipboardSvgAndJsonWrite { graphite_json, .. } => {
// TODO: Add support for svg after clipboard API change in desktop.
dispatcher.respond(DesktopFrontendMessage::ClipboardWrite { content: graphite_json });
Comment thread
VimYoung marked this conversation as resolved.
}
FrontendMessage::WindowPointerLock => {
dispatcher.respond(DesktopFrontendMessage::PointerLock);
Expand Down
29 changes: 13 additions & 16 deletions editor/src/messages/clipboard/clipboard_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,18 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
responses.add(ClipboardMessage::CopyLayers);
}
}
ClipboardMessage::Write { content } => {
let text = match content {
ClipboardContent::Svg(_) => {
log::error!("SVG copying is not yet supported");
return;
}
ClipboardContent::Image { .. } => {
log::error!("Image copying is not yet supported");
return;
}
ClipboardContent::Graphite(graphite) => format!("{CLIPBOARD_PREFIX}{graphite}"),
ClipboardContent::Text(text) => text,
};
responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
}
ClipboardMessage::Write { content } => match content {
ClipboardContent::Image { .. } => {
log::error!("Image copying is not yet supported");
}
ClipboardContent::Graphite(graphite) => {
let graphite_json = format!("{CLIPBOARD_PREFIX}{graphite}");
responses.add(PortfolioMessage::RequestSvgTextCopy { graphite_json });
}
ClipboardContent::Text(graphite_json) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, 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.

P1: Copying a text selection breaks on Chrome/Edge: the frontend receives svg_string as null (serde serializes Rust Option::None as JSON null), so data.svg_string !== undefined is true, and it builds new ClipboardItem({ "image/svg+xml": null, ... }), which throws a TypeError and never writes the text. The guard must treat null as absent too (data.svg_string != null), or the text case must not be routed through TriggerClipboardSvgAndJsonWrite.

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 91:

<comment>Copying a text selection breaks on Chrome/Edge: the frontend receives `svg_string` as `null` (serde serializes Rust `Option::None` as JSON null), so `data.svg_string !== undefined` is true, and it builds `new ClipboardItem({ "image/svg+xml": null, ... })`, which throws a TypeError and never writes the text. The guard must treat `null` as absent too (`data.svg_string != null`), or the text case must not be routed through `TriggerClipboardSvgAndJsonWrite`.</comment>

<file context>
@@ -87,8 +87,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
-				ClipboardContent::Text(text) => {
-					responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
+				ClipboardContent::Text(graphite_json) => {
+					responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });
 				}
 			},
</file context>

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.

The None does become an undefined in js.

}
Comment on lines +90 to +92

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: ClipboardContent::Text is only ever constructed from ReadSelection { content }, which carries the plain text read at the caret (readAtCaret), not a serialized Graphite payload. Renaming the binding to graphite_json and passing it through the graphite_json field of TriggerClipboardSvgAndJsonWrite is misleading: the value written to the system clipboard here is plain text (the frontend's else branch does writeText when svg_string is None). A future maintainer will reasonably assume this field holds a graphite:-prefixed JSON payload. Use a name that reflects the value, e.g. text, and note clearly that plain text copy intentionally sends svg_string: None with the text in the graphite_json slot.

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 90:

<comment>`ClipboardContent::Text` is only ever constructed from `ReadSelection { content }`, which carries the plain text read at the caret (`readAtCaret`), not a serialized Graphite payload. Renaming the binding to `graphite_json` and passing it through the `graphite_json` field of `TriggerClipboardSvgAndJsonWrite` is misleading: the value written to the system clipboard here is plain text (the frontend's else branch does `writeText` when `svg_string` is None). A future maintainer will reasonably assume this field holds a `graphite:`-prefixed JSON payload. Use a name that reflects the value, e.g. `text`, and note clearly that plain text copy intentionally sends `svg_string: None` with the text in the graphite_json slot.</comment>

<file context>
@@ -87,8 +87,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
 				}
-				ClipboardContent::Text(text) => {
-					responses.add(FrontendMessage::TriggerClipboardWrite { content: text });
+				ClipboardContent::Text(graphite_json) => {
+					responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });
 				}
</file context>
Suggested change
ClipboardContent::Text(graphite_json) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json });
}
ClipboardContent::Text(text) => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json: text });
}

},

ClipboardMessage::CopyLayers => {
if current_tool == &ToolType::Path {
Expand Down Expand Up @@ -526,7 +523,7 @@ mod test {
.await
.into_iter()
.find_map(|message| match message {
FrontendMessage::TriggerClipboardWrite { content } => Some(content),
FrontendMessage::TriggerClipboardSvgAndJsonWrite { graphite_json, .. } => Some(graphite_json),
_ => None,
})
.expect("copying layers should write a payload to the clipboard")
Expand Down
1 change: 0 additions & 1 deletion editor/src/messages/clipboard/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ pub enum ClipboardContentRaw {
pub enum ClipboardContent {
Graphite(String),
Text(String),
Svg(String),
Image { data: Vec<u8>, width: u32, height: u32 },
}

Expand Down
5 changes: 3 additions & 2 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ pub enum FrontendMessage {
url: String,
},
TriggerClipboardRead,
TriggerClipboardWrite {
content: String,
TriggerClipboardSvgAndJsonWrite {
svg_string: Option<String>,
graphite_json: String,
},
TriggerSelectionRead {
cut: bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4333,7 +4333,7 @@ mod document_message_handler_tests {
})
.await;

let instrumented = editor.eval_graph().await.unwrap();
let (instrumented, _) = editor.eval_graph().await.unwrap();

// The emptiness guards keep these assertions honest: a wrong `Output` type on `grab_all_input` yields no records at all, which would otherwise pass without checking anything
let base_lengths: Vec<usize> = instrumented
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ mod network_interface_tests {
let clipboard = frontend_messages
.into_iter()
.find_map(|msg| match msg {
FrontendMessage::TriggerClipboardWrite { content } => Some(content),
FrontendMessage::TriggerClipboardSvgAndJsonWrite { graphite_json, .. } => Some(graphite_json),
_ => None,
})
.expect("copy message should be dispatched");
Expand Down
3 changes: 3 additions & 0 deletions editor/src/messages/portfolio/portfolio_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ pub enum PortfolioMessage {
/// New sizes for the children at that split node.
sizes: Vec<f64>,
},
RequestSvgTextCopy {
graphite_json: String,
},
}

/// Clone helper for the non-serializable `gdd` payload: a cloned mount message carries no `Gdd`.
Expand Down
8 changes: 8 additions & 0 deletions editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
}
}
PortfolioMessage::RequestSvgTextCopy { graphite_json } => {
if let Some(active_document) = self.active_document() {
let selected_nodes: Vec<NodeId> = active_document.network_interface.shallowest_unique_layers(&[]).map(|layer| layer.to_node()).collect();
self.executor.copy_svg_clipboard(graphite_json, selected_nodes);
} else {
self.executor.copy_svg_clipboard(graphite_json, Vec::new());
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ mod test_ellipse {

async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Ok((instrumented, _)) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};

Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/tool/tool_messages/artboard_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ mod test_artboard {
use graphene_std::list::List;

async fn get_artboards(editor: &mut EditorTestUtils) -> List<Artboard> {
let instrumented = match editor.eval_graph().await {
let (instrumented, _) = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/tool/tool_messages/fill_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ mod test_fill {

// The Fill tool writes solid colors, whose stored values the input monitor records as `Item<Color>` wires
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Item<Color>> {
let instrumented = match editor.eval_graph().await {
let (instrumented, _) = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
Expand Down
13 changes: 13 additions & 0 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub enum NodeGraphUpdate {
CompilationResponse(CompilationResponse),
EyedropperPreview(Raster<CPU>),
NodeGraphUpdateMessage(NodeGraphUpdateMessage),
SvgTextCopyClipboard { svg_string: String, graphite_json: String },
}

#[derive(Debug, Default)]
Expand Down Expand Up @@ -466,6 +467,12 @@ impl NodeGraphExecutor {
responses.add(EyedropperToolMessage::PreviewImage { data, width, height });
}
NodeGraphUpdate::NodeGraphUpdateMessage(_) => {}
NodeGraphUpdate::SvgTextCopyClipboard { svg_string, graphite_json } => {
responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite {
svg_string: Some(svg_string),
graphite_json,
});
}
}
}

Expand Down Expand Up @@ -812,6 +819,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))
.expect("Failed to send runtime request");
Comment thread
VimYoung marked this conversation as resolved.
}
}

// TODO: Eventually remove this document upgrade code
Expand Down
73 changes: 62 additions & 11 deletions editor/src/node_graph_executor/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub enum GraphRuntimeRequest {
GraphUpdate(GraphUpdate),
ExecutionRequest(ExecutionRequest),
EditorPreferencesUpdate(EditorPreferences),
CopySvgTextClipboard(String, Vec<NodeId>),
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -108,6 +109,10 @@ impl InternalNodeGraphUpdateSender {
fn send_eyedropper_preview(&self, raster: Raster<CPU>) {
self.0.send(NodeGraphUpdate::EyedropperPreview(raster)).expect("Failed to send response")
}

fn send_svg_text_clipboard(&self, svg_string: String, graphite_json: String) {
self.0.send(NodeGraphUpdate::SvgTextCopyClipboard { svg_string, graphite_json }).expect("Failed to send response")
}
}

impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender {
Expand Down Expand Up @@ -162,6 +167,7 @@ impl NodeRuntime {
let mut graph = None;
let mut eyedropper = None;
let mut execution = None;
let mut svg_clipboard = None;
for request in self.receiver.try_iter() {
match request {
GraphRuntimeRequest::GraphUpdate(_) => graph = Some(request),
Expand All @@ -182,6 +188,7 @@ impl NodeRuntime {
}
}
GraphRuntimeRequest::EditorPreferencesUpdate(_) => preferences = Some(request),
GraphRuntimeRequest::CopySvgTextClipboard(..) => svg_clipboard = Some(request),
}
}

Expand All @@ -193,7 +200,7 @@ impl NodeRuntime {
eyedropper.render_config.pointer = execution.render_config.pointer;
}

let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
let requests = [preferences, graph, eyedropper, svg_clipboard, execution].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.

P2: When a copy request and a graph evaluation are coalesced in one runtime pass, this line introspects monitor nodes before the pending execution refreshes them, so the clipboard can contain stale or empty SVG. Ensure the copy path reads output from the relevant completed execution, and avoid relying only on request reordering because the execution branch currently returns before later requests.

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 copy request and a graph evaluation are coalesced in one runtime pass, this line introspects monitor nodes before the pending execution refreshes them, so the clipboard can contain stale or empty SVG. Ensure the copy path reads output from the relevant completed execution, and avoid relying only on request reordering because the execution branch currently returns before later requests.</comment>

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

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.

This is technically true however it probably isn't very relevant since the user would be copying an SVG that is not yet rendered into the viewport.

You could fix this by having a result variable instead of an early return:

let mut result = None;
for request in requests {
	match request {
		// ...
		GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => {
			// ...
			result = Some(texture);
		}
	}
}
result

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: Moving svg_clipboard ahead of execution lets a pending CopySvgTextClipboard request short-circuit a pending render. The CopySvgTextClipboard branch returns None early when combined_graphics.is_empty() (the case for copy_svg_clipboard(graphite_json, Vec::new()) in portfolio_message_handler.rs:1697 and for an empty selection). Because the loop processes svg_clipboard first, that return None now skips the ExecutionRequest branch entirely, so the queued viewport render is dropped and its ExecutionResponse is never sent. Before this reorder the execution ran first and the copy was skipped instead. Consider processing the SVG copy without the early return None (e.g., continue after sending the clipboard) so a concurrent execution request still runs.

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>Moving `svg_clipboard` ahead of `execution` lets a pending `CopySvgTextClipboard` request short-circuit a pending render. The `CopySvgTextClipboard` branch returns `None` early when `combined_graphics.is_empty()` (the case for `copy_svg_clipboard(graphite_json, Vec::new())` in portfolio_message_handler.rs:1697 and for an empty selection). Because the loop processes `svg_clipboard` first, that `return None` now skips the `ExecutionRequest` branch entirely, so the queued viewport render is dropped and its `ExecutionResponse` is never sent. Before this reorder the execution ran first and the copy was skipped instead. Consider processing the SVG copy without the early `return None` (e.g., `continue` after sending the clipboard) so a concurrent execution request still runs.</comment>

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


for request in requests {
match 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.

I think the control flow would be much easier if these were made into functions:

match request {
	GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => self.editor_preferences_update(preferences),
	GraphRuntimeRequest::GraphUpdate(graph_update) => self.graph_update(graph_update),
	...
}

Expand Down Expand Up @@ -340,6 +347,28 @@ impl NodeRuntime {
});
return texture;
}
GraphRuntimeRequest::CopySvgTextClipboard(text_string_clipboard, selected_node_ids) => {
let combined_graphics = self.collect_graphics(&selected_node_ids);

if combined_graphics.is_empty() {
self.sender.send_svg_text_clipboard(String::new(), text_string_clipboard);
return None;

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.

continue don't return.

}

let bounds = graphene_std::renderer::graphic_list_bounding_box(&combined_graphics, DAffine2::IDENTITY);
let final_bounds = match bounds {
RenderBoundingBox::Rectangle(bounds) if (bounds[1] - bounds[0]) != DVec2::ZERO => bounds,
_ => [DVec2::ZERO, DVec2::ONE],
};

let footprint = Footprint::from_bounds(final_bounds, RenderQuality::Full);
let render_params = RenderParams { footprint, ..Default::default() };
let mut render = SvgRender::new();
combined_graphics.render_svg(&mut render, &render_params);
render.format_svg(final_bounds[0], final_bounds[1]);

self.sender.send_svg_text_clipboard(render.svg.to_svg_string(), text_string_clipboard);
}
}
}
None
Expand Down Expand Up @@ -392,11 +421,7 @@ impl NodeRuntime {

for monitor_node_path in &self.monitor_nodes {
// Skip the inspect monitor node
if self
.inspect_state
.as_ref()
.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node))
{
if self.is_insepect_monitor_node(monitor_node_path) {
continue;
}

Expand Down Expand Up @@ -498,11 +523,7 @@ impl NodeRuntime {
};
let bounds = expand_to_thumbnail_aspect(raw_bounds);
let new_thumbnail_svg = {
let footprint = Footprint {
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32),
quality: RenderQuality::Full,
};
let footprint = Footprint::from_bounds(bounds, RenderQuality::Full);

// Render the thumbnail from a `Graphic` into an SVG string
let render_params = RenderParams {
Expand All @@ -529,6 +550,36 @@ impl NodeRuntime {
*old_thumbnail_svg = new_thumbnail_svg;
}
}

fn collect_graphics(&self, selected_node_ids: &Vec<NodeId>) -> List<Graphic> {
let mut combined_graphics = List::<Graphic>::new();
for monitor_node_path in &self.monitor_nodes {
// Skip inspect monitor node if active
if self.is_insepect_monitor_node(monitor_node_path) {
continue;
}

let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else {
continue;
};

if selected_node_ids.contains(&parent_network_node_id) {
// Introspect using the full monitor node path
if let Ok(introspected_data) = self.executor.introspect(monitor_node_path)
&& let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>()
{
combined_graphics.extend(io.output.clone());
} else {
warn!("No graphic type is matched while extracting svg");
}
Comment on lines +568 to +574

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 selected layer's monitor output is Item<Graphic>, collect_graphics drops it and CopySvgTextClipboard sends an empty SVG. Preserve the rank-0 branch by pushing io.output alongside the existing List<Graphic> handling.

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 568:

<comment>When a selected layer's monitor output is `Item<Graphic>`, `collect_graphics` drops it and `CopySvgTextClipboard` sends an empty SVG. Preserve the rank-0 branch by pushing `io.output` alongside the existing `List<Graphic>` handling.</comment>

<file context>
@@ -589,6 +550,36 @@ impl NodeRuntime {
+
+			if selected_node_ids.contains(&parent_network_node_id) {
+				// Introspect using the full monitor node path
+				if let Ok(introspected_data) = self.executor.introspect(monitor_node_path)
+					&& let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>()
+				{
</file context>
Suggested change
if let Ok(introspected_data) = self.executor.introspect(monitor_node_path)
&& let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>()
{
combined_graphics.extend(io.output.clone());
} else {
warn!("No graphic type is matched while extracting svg");
}
if let Ok(introspected_data) = self.executor.introspect(monitor_node_path) {
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>() {
combined_graphics.extend(io.output.clone());
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<Graphic>>>() {
combined_graphics.push(io.output.clone());
} else {
warn!("No graphic type is matched while extracting svg");
}
} else {
warn!("No graphic type is matched while extracting svg");
}

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.

This doesn't happen as it will always be made into a group.

}
}
combined_graphics
}

fn is_insepect_monitor_node(&self, monitor_node_path: &Vec<NodeId>) -> bool {
self.inspect_state.as_ref().is_some_and(|state| monitor_node_path.last().copied() == Some(state.monitor_node))
}
}

/// Returns the union of the artboards' clipping rectangles, used as the thumbnail bounds for an artboard layer so the
Expand Down
17 changes: 9 additions & 8 deletions editor/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ impl EditorTestUtils {
Self { editor, runtime }
}

pub fn eval_graph<'a>(&'a mut self) -> impl std::future::Future<Output = Result<Instrumented, String>> + 'a {
pub fn eval_graph<'a>(&'a mut self) -> impl std::future::Future<Output = Result<(Instrumented, Vec<FrontendMessage>), String>> + 'a {
// An inner function is required since async functions in traits are a bit weird
async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Result<Instrumented, String> {
async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Result<(Instrumented, Vec<FrontendMessage>), String> {
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
let document_id = portfolio.active_document_id.unwrap();
let (executor, documents) = (&mut portfolio.executor, &mut portfolio.documents);
Expand All @@ -55,24 +55,25 @@ impl EditorTestUtils {
if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) {
return Err(format!("Graph should render\n\n{e}"));
}
let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message));
let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message)).collect::<Vec<_>>();

for message in frontend_messages {
for message in &frontend_messages {
message.check_node_graph_error();
}

Ok(instrumented)
Ok((instrumented, frontend_messages))
}

run(&mut self.editor, &mut self.runtime)
}

pub async fn handle_message(&mut self, message: impl Into<Message>) -> Vec<FrontendMessage> {
let frontend_messages_from_msg = self.editor.handle_message(message);
let mut frontend_messages_from_msg = self.editor.handle_message(message);

// Required to process any buffered messages
if let Err(e) = self.eval_graph().await {
panic!("Failed to evaluate graph: {e}");
match self.eval_graph().await {
Ok((_, new_messages)) => frontend_messages_from_msg.extend(new_messages),
Err(e) => panic!("Failed to evaluate graph: {e}"),
}

// Sweep the network interface's structural invariants so any desync fails at the message that caused it
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/managers/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,34 @@ export function createClipboardManager(subscriptions: SubscriptionsRouter, edito
subscriptionsRouter = subscriptions;
editorWrapper = editor;

subscriptions.subscribeFrontendMessage("TriggerClipboardWrite", (data) => {
// If the Clipboard API is supported in the browser, copy text to the clipboard
navigator.clipboard?.writeText?.(data.content);
});

subscriptions.subscribeFrontendMessage("TriggerSelectionRead", async (data) => {
editor.readSelection(readAtCaret(data.cut), data.cut);
});

subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
insertAtCaret(data.content);
});

subscriptions.subscribeFrontendMessage("TriggerClipboardSvgAndJsonWrite", (data) => {
// Adopted from https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem#browser_compatibility
if (ClipboardItem.supports("image/svg+xml") && data.svg_string !== undefined) {
navigator.clipboard?.write?.([
new ClipboardItem({
"image/svg+xml": data.svg_string,
"text/plain": data.graphite_json,
}),
]);
} else {
navigator.clipboard?.writeText?.(data.graphite_json);
}
});
}

export function destroyClipboardManager() {
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;

subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
subscriptions.unsubscribeFrontendMessage("TriggerClipboardSvgAndJsonWrite");
subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
}
Expand Down
Loading