1use crate::{
2 BindingId, CanvasDocument, CanvasEdge, CanvasEndpoint, CanvasNode, CanvasRecordId,
3 CanvasRecordRelations, CanvasSelection, CanvasShape, CanvasTransaction, DocumentCommand,
4 EdgeId, NodeId, ShapeId,
5 record_scope::{CanvasRecordScopeOptions, resolve_selection_scope_with_predicates},
6};
7use indexmap::IndexMap;
8use open_gpui::{Pixels, Point};
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct CanvasClipboardPayload {
13 #[serde(default)]
14 pub nodes: Vec<CanvasNode>,
15 #[serde(default)]
16 pub edges: Vec<CanvasEdge>,
17 #[serde(default)]
18 pub shapes: Vec<CanvasShape>,
19 #[serde(default)]
20 pub selection: CanvasSelection,
21 #[serde(default)]
22 pub relations: CanvasRecordRelations,
23}
24
25impl CanvasClipboardPayload {
26 pub fn from_document_selection(document: &CanvasDocument, selection: &CanvasSelection) -> Self {
27 let scope = resolve_selection_scope_with_predicates(
28 document,
29 selection,
30 CanvasRecordScopeOptions::structural_with_internal_edges(),
31 |record_id| document.contains_record(record_id),
32 |record_id| is_copyable_record(document, record_id),
33 );
34 let copied_records = scope.action_records();
35 let selection = copied_selection(scope.normalized_selection(), copied_records);
36
37 let mut nodes = Vec::new();
38 let mut edges = Vec::new();
39 let mut shapes = Vec::new();
40 for record_id in copied_records.records() {
41 match record_id {
42 CanvasRecordId::Node(id) => {
43 if let Some(node) = document.node(id) {
44 nodes.push(node.clone());
45 }
46 }
47 CanvasRecordId::Edge(id) => {
48 if let Some(edge) = document.edge(id) {
49 edges.push(edge.clone());
50 }
51 }
52 CanvasRecordId::Shape(id) => {
53 if let Some(shape) = document.shape(id) {
54 shapes.push(shape.clone());
55 }
56 }
57 }
58 }
59
60 Self {
61 nodes,
62 edges,
63 shapes,
64 selection,
65 relations: document
66 .relations()
67 .subset_for_records(|record_id| copied_records.contains(record_id)),
68 }
69 }
70
71 pub fn is_empty(&self) -> bool {
72 self.nodes.is_empty() && self.edges.is_empty() && self.shapes.is_empty()
73 }
74
75 pub fn paste_transaction(
76 &self,
77 document: &CanvasDocument,
78 offset: Point<Pixels>,
79 ) -> CanvasPasteTransaction {
80 let node_ids = remap_ids(
81 self.nodes.iter().map(|node| node.id.as_str()),
82 |id| document.contains_node(&NodeId::from(id.to_owned())),
83 unique_node_id,
84 );
85 let edge_ids = remap_ids(
86 self.edges.iter().map(|edge| edge.id.as_str()),
87 |id| document.contains_edge(&EdgeId::from(id.to_owned())),
88 unique_edge_id,
89 );
90 let shape_ids = remap_ids(
91 self.shapes.iter().map(|shape| shape.id.as_str()),
92 |id| document.contains_shape(&ShapeId::from(id.to_owned())),
93 unique_shape_id,
94 );
95 let binding_ids = remap_ids(
96 self.relations.bindings().map(|binding| binding.id.as_str()),
97 |id| {
98 document
99 .relations()
100 .binding(&BindingId::from(id.to_owned()))
101 .is_some()
102 },
103 unique_binding_id,
104 );
105
106 let mut fallback_selection = CanvasSelection::default();
107 let mut commands = Vec::new();
108
109 for node in &self.nodes {
110 let mut node = node.clone();
111 node.id = node_ids[&node.id].clone();
112 node.position += offset;
113 fallback_selection.insert_node(node.id.clone());
114 commands.push(DocumentCommand::InsertNode(node));
115 }
116
117 for shape in &self.shapes {
118 let mut shape = shape.clone();
119 shape.id = shape_ids[&shape.id].clone();
120 shape.bounds.origin += offset;
121 fallback_selection.insert_shape(shape.id.clone());
122 commands.push(DocumentCommand::InsertShape(shape));
123 }
124
125 let mut pasted_edge_ids = IndexMap::new();
126 for edge in &self.edges {
127 let Some(source) = remap_endpoint(&edge.source, &node_ids) else {
128 continue;
129 };
130 let Some(target) = remap_endpoint(&edge.target, &node_ids) else {
131 continue;
132 };
133 let mut edge = edge.clone();
134 let original_edge_id = edge.id.clone();
135 edge.id = edge_ids[&original_edge_id].clone();
136 edge.source = source;
137 edge.target = target;
138 fallback_selection.insert_edge(edge.id.clone());
139 pasted_edge_ids.insert(original_edge_id, edge.id.clone());
140 commands.push(DocumentCommand::InsertEdge(edge));
141 }
142
143 for relation in self.relations.parents() {
144 let Some(child) =
145 remap_record_id(&relation.child, &node_ids, &pasted_edge_ids, &shape_ids)
146 else {
147 continue;
148 };
149 let Some(parent) =
150 remap_record_id(&relation.parent, &node_ids, &pasted_edge_ids, &shape_ids)
151 else {
152 continue;
153 };
154 commands.push(DocumentCommand::SetRecordParent { child, parent });
155 }
156
157 for relation in self.relations.groups() {
158 let Some(group) =
159 remap_record_id(&relation.group, &node_ids, &pasted_edge_ids, &shape_ids)
160 else {
161 continue;
162 };
163 let Some(member) =
164 remap_record_id(&relation.member, &node_ids, &pasted_edge_ids, &shape_ids)
165 else {
166 continue;
167 };
168 commands.push(DocumentCommand::AddRecordToGroup { group, member });
169 }
170
171 for relation in self.relations.bindings() {
172 let Some(source) =
173 remap_record_id(&relation.source, &node_ids, &pasted_edge_ids, &shape_ids)
174 else {
175 continue;
176 };
177 let Some(target) =
178 remap_record_id(&relation.target, &node_ids, &pasted_edge_ids, &shape_ids)
179 else {
180 continue;
181 };
182 let mut binding = relation.clone();
183 binding.id = binding_ids[&binding.id].clone();
184 binding.source = source;
185 binding.target = target;
186 commands.push(DocumentCommand::SetRecordBinding(binding));
187 }
188
189 let selection = remap_selection(&self.selection, &node_ids, &pasted_edge_ids, &shape_ids)
190 .filter(|selection| !selection.is_empty())
191 .unwrap_or(fallback_selection);
192
193 CanvasPasteTransaction {
194 transaction: CanvasTransaction::new(commands),
195 selection,
196 }
197 }
198}
199
200#[derive(Clone, Debug, PartialEq)]
201pub struct CanvasPasteTransaction {
202 pub transaction: CanvasTransaction,
203 pub selection: CanvasSelection,
204}
205
206fn is_copyable_record(document: &CanvasDocument, record_id: &CanvasRecordId) -> bool {
207 match record_id {
208 CanvasRecordId::Node(id) => document.node(id).is_some_and(|node| !node.locked),
209 CanvasRecordId::Edge(id) => document.edge(id).is_some_and(|edge| !edge.locked),
210 CanvasRecordId::Shape(id) => document.shape(id).is_some_and(|shape| !shape.locked),
211 }
212}
213
214fn copied_selection(
215 selection: &CanvasSelection,
216 copied_records: &crate::CanvasRecordScope,
217) -> CanvasSelection {
218 let mut copied_selection = CanvasSelection::default();
219 for record_id in selection
220 .selected_records()
221 .filter(|record_id| copied_records.contains(record_id))
222 {
223 copied_selection.insert_record(record_id);
224 }
225 for endpoint in selection.selected_handles() {
226 if copied_records.contains(&CanvasRecordId::Node(endpoint.node_id.clone())) {
227 copied_selection.insert_handle(endpoint.clone());
228 }
229 }
230 copied_selection
231}
232
233fn remap_ids<'a, I, T, Exists, Unique>(ids: I, exists: Exists, unique: Unique) -> IndexMap<T, T>
234where
235 I: IntoIterator<Item = &'a str>,
236 T: Clone + From<String> + std::hash::Hash + Eq,
237 Exists: Fn(&str) -> bool,
238 Unique: Fn(&str, &mut dyn FnMut(&str) -> bool) -> T,
239{
240 let mut remapped = IndexMap::new();
241 for id in ids {
242 let mut taken = |candidate: &str| {
243 exists(candidate)
244 || remapped
245 .values()
246 .any(|mapped: &T| mapped.clone() == T::from(candidate.to_owned()))
247 };
248 remapped.insert(T::from(id.to_owned()), unique(id, &mut taken));
249 }
250 remapped
251}
252
253fn unique_node_id(base: &str, taken: &mut dyn FnMut(&str) -> bool) -> NodeId {
254 NodeId::new(unique_id(base, taken))
255}
256
257fn unique_edge_id(base: &str, taken: &mut dyn FnMut(&str) -> bool) -> EdgeId {
258 EdgeId::new(unique_id(base, taken))
259}
260
261fn unique_shape_id(base: &str, taken: &mut dyn FnMut(&str) -> bool) -> ShapeId {
262 ShapeId::new(unique_id(base, taken))
263}
264
265fn unique_binding_id(base: &str, taken: &mut dyn FnMut(&str) -> bool) -> BindingId {
266 BindingId::new(unique_id(base, taken))
267}
268
269fn unique_id(base: &str, taken: &mut dyn FnMut(&str) -> bool) -> String {
270 let copy = format!("{base}-copy");
271 if !taken(©) {
272 return copy;
273 }
274
275 for index in 2.. {
276 let candidate = format!("{base}-copy-{index}");
277 if !taken(&candidate) {
278 return candidate;
279 }
280 }
281
282 unreachable!("unbounded id search should always return")
283}
284
285fn remap_endpoint(
286 endpoint: &CanvasEndpoint,
287 node_ids: &IndexMap<NodeId, NodeId>,
288) -> Option<CanvasEndpoint> {
289 node_ids
290 .get(&endpoint.node_id)
291 .cloned()
292 .map(|node_id| CanvasEndpoint {
293 node_id,
294 handle_id: endpoint.handle_id.clone(),
295 })
296}
297
298fn remap_selection(
299 selection: &CanvasSelection,
300 node_ids: &IndexMap<NodeId, NodeId>,
301 edge_ids: &IndexMap<EdgeId, EdgeId>,
302 shape_ids: &IndexMap<ShapeId, ShapeId>,
303) -> Option<CanvasSelection> {
304 let mut remapped = CanvasSelection::default();
305 for record_id in selection.selected_records() {
306 remapped.insert_record(remap_record_id(&record_id, node_ids, edge_ids, shape_ids)?);
307 }
308 for endpoint in selection.selected_handles() {
309 remapped.insert_handle(remap_endpoint(endpoint, node_ids)?);
310 }
311
312 Some(remapped)
313}
314
315fn remap_record_id(
316 id: &CanvasRecordId,
317 node_ids: &IndexMap<NodeId, NodeId>,
318 edge_ids: &IndexMap<EdgeId, EdgeId>,
319 shape_ids: &IndexMap<ShapeId, ShapeId>,
320) -> Option<CanvasRecordId> {
321 match id {
322 CanvasRecordId::Node(id) => node_ids.get(id).cloned().map(CanvasRecordId::Node),
323 CanvasRecordId::Edge(id) => edge_ids.get(id).cloned().map(CanvasRecordId::Edge),
324 CanvasRecordId::Shape(id) => shape_ids.get(id).cloned().map(CanvasRecordId::Shape),
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::test_support::document_fixture;
332 use crate::{
333 BindingId, CanvasEdge, CanvasEndpoint, CanvasNode, CanvasRecordBindingRelation, CanvasShape,
334 };
335 use open_gpui::{Bounds, point, px, size};
336
337 #[test]
338 fn copy_selection_includes_internal_edges() {
339 let document = connected_document();
340 let mut selection = CanvasSelection::default();
341 selection.insert_node(NodeId::from("a"));
342 selection.insert_node(NodeId::from("b"));
343
344 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
345
346 assert_eq!(
347 payload
348 .nodes
349 .iter()
350 .map(|node| &node.id)
351 .collect::<Vec<_>>(),
352 vec![&NodeId::from("a"), &NodeId::from("b")]
353 );
354 assert_eq!(
355 payload
356 .edges
357 .iter()
358 .map(|edge| &edge.id)
359 .collect::<Vec<_>>(),
360 vec![&EdgeId::from("a-b")]
361 );
362 }
363
364 #[test]
365 fn copy_selection_omits_external_edges() {
366 let document = connected_document();
367 let mut selection = CanvasSelection::default();
368 selection.insert_node(NodeId::from("a"));
369
370 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
371
372 assert_eq!(payload.nodes.len(), 1);
373 assert!(payload.edges.is_empty());
374 }
375
376 #[test]
377 fn paste_payload_remaps_ids_and_offsets_records() {
378 let mut document = connected_document();
379 document
380 .insert_shape(CanvasShape::new(
381 "note",
382 Bounds::new(point(px(0.0), px(20.0)), size(px(30.0), px(30.0))),
383 ))
384 .unwrap();
385 let mut selection = CanvasSelection::default();
386 selection.insert_node(NodeId::from("a"));
387 selection.insert_node(NodeId::from("b"));
388 selection.insert_shape(ShapeId::from("note"));
389 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
390
391 let pasted = payload.paste_transaction(&document, point(px(16.0), px(24.0)));
392
393 assert_eq!(
394 pasted
395 .selection
396 .selected_nodes()
397 .cloned()
398 .collect::<Vec<_>>(),
399 vec![NodeId::from("a-copy"), NodeId::from("b-copy")]
400 );
401 assert_eq!(
402 pasted
403 .selection
404 .selected_edges()
405 .cloned()
406 .collect::<Vec<_>>(),
407 Vec::<EdgeId>::new()
408 );
409 assert_eq!(
410 pasted
411 .selection
412 .selected_shapes()
413 .cloned()
414 .collect::<Vec<_>>(),
415 vec![ShapeId::from("note-copy")]
416 );
417
418 let mut draft = document.clone();
419 draft.apply_transaction(pasted.transaction).unwrap();
420 assert!(draft.contains_edge(&EdgeId::from("a-b-copy")));
421 assert_eq!(
422 draft.node(&NodeId::from("a-copy")).unwrap().position,
423 point(px(16.0), px(24.0))
424 );
425 assert_eq!(
426 draft
427 .edge(&EdgeId::from("a-b-copy"))
428 .unwrap()
429 .source
430 .node_id,
431 NodeId::from("a-copy")
432 );
433 assert_eq!(
434 draft
435 .shape(&ShapeId::from("note-copy"))
436 .unwrap()
437 .bounds
438 .origin,
439 point(px(16.0), px(44.0))
440 );
441 }
442
443 #[test]
444 fn copy_selection_includes_internal_relations() {
445 let document = related_document();
446 let mut selection = CanvasSelection::default();
447 selection.insert_node(NodeId::from("child"));
448 selection.insert_shape(ShapeId::from("group"));
449
450 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
451
452 let child = CanvasRecordId::Node(NodeId::from("child"));
453 let group = CanvasRecordId::Shape(ShapeId::from("group"));
454 assert_eq!(payload.relations.parent_of(&child), Some(&group));
455 assert_eq!(
456 payload
457 .relations
458 .members_of(&group)
459 .cloned()
460 .collect::<Vec<_>>(),
461 vec![child.clone()]
462 );
463 assert_eq!(
464 payload.relations.binding(&BindingId::from("binding")),
465 Some(&CanvasRecordBindingRelation::new(
466 "binding",
467 child,
468 group.clone()
469 ))
470 );
471 }
472
473 #[test]
474 fn copy_selection_expands_related_descendants() {
475 let document = related_tree_document();
476 let mut selection = CanvasSelection::default();
477 selection.insert_shape(ShapeId::from("frame"));
478
479 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
480
481 assert_eq!(
482 payload
483 .nodes
484 .iter()
485 .map(|node| &node.id)
486 .collect::<Vec<_>>(),
487 vec![&NodeId::from("child"), &NodeId::from("peer")]
488 );
489 assert_eq!(
490 payload
491 .edges
492 .iter()
493 .map(|edge| &edge.id)
494 .collect::<Vec<_>>(),
495 vec![&EdgeId::from("child-peer")]
496 );
497 assert_eq!(
498 payload
499 .shapes
500 .iter()
501 .map(|shape| &shape.id)
502 .collect::<Vec<_>>(),
503 vec![&ShapeId::from("frame"), &ShapeId::from("group")]
504 );
505
506 let frame = CanvasRecordId::Shape(ShapeId::from("frame"));
507 let group = CanvasRecordId::Shape(ShapeId::from("group"));
508 let child = CanvasRecordId::Node(NodeId::from("child"));
509 let peer = CanvasRecordId::Node(NodeId::from("peer"));
510 assert_eq!(payload.relations.parent_of(&group), Some(&frame));
511 assert_eq!(
512 payload
513 .relations
514 .members_of(&group)
515 .cloned()
516 .collect::<Vec<_>>(),
517 vec![child.clone(), peer.clone()]
518 );
519 assert_eq!(
520 payload.relations.binding(&BindingId::from("binding")),
521 Some(&CanvasRecordBindingRelation::new("binding", child, group))
522 );
523 assert_eq!(
524 payload
525 .selection
526 .selected_shapes()
527 .cloned()
528 .collect::<Vec<_>>(),
529 vec![ShapeId::from("frame")]
530 );
531 assert!(payload.selection.selected_nodes().next().is_none());
532 assert!(payload.selection.selected_edges().next().is_none());
533 }
534
535 #[test]
536 fn paste_payload_selects_remapped_explicit_roots() {
537 let document = related_tree_document();
538 let mut selection = CanvasSelection::default();
539 selection.insert_shape(ShapeId::from("frame"));
540 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
541
542 let pasted = payload.paste_transaction(&document, point(px(16.0), px(24.0)));
543
544 assert_eq!(
545 pasted
546 .selection
547 .selected_shapes()
548 .cloned()
549 .collect::<Vec<_>>(),
550 vec![ShapeId::from("frame-copy")]
551 );
552 assert!(pasted.selection.selected_nodes().next().is_none());
553 assert!(pasted.selection.selected_edges().next().is_none());
554 }
555
556 #[test]
557 fn copy_selection_filters_payload_selection_to_copied_records() {
558 let mut locked =
559 CanvasNode::new("locked", point(px(40.0), px(0.0)), size(px(10.0), px(10.0)));
560 locked.locked = true;
561 let document = document_fixture()
562 .node(CanvasNode::new(
563 "copyable",
564 point(px(0.0), px(0.0)),
565 size(px(10.0), px(10.0)),
566 ))
567 .node(locked)
568 .build();
569 let mut selection = CanvasSelection::default();
570 selection.insert_node(NodeId::from("copyable"));
571 selection.insert_node(NodeId::from("locked"));
572
573 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
574 let pasted = payload.paste_transaction(&document, point(px(16.0), px(24.0)));
575
576 assert_eq!(
577 payload
578 .nodes
579 .iter()
580 .map(|node| &node.id)
581 .collect::<Vec<_>>(),
582 vec![&NodeId::from("copyable")]
583 );
584 assert_eq!(
585 payload
586 .selection
587 .selected_nodes()
588 .cloned()
589 .collect::<Vec<_>>(),
590 vec![NodeId::from("copyable")]
591 );
592 assert_eq!(
593 pasted
594 .selection
595 .selected_nodes()
596 .cloned()
597 .collect::<Vec<_>>(),
598 vec![NodeId::from("copyable-copy")]
599 );
600 }
601
602 #[test]
603 fn copy_selection_omits_external_relations() {
604 let document = related_document();
605 let mut selection = CanvasSelection::default();
606 selection.insert_node(NodeId::from("child"));
607
608 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
609
610 assert!(payload.relations.is_empty());
611 }
612
613 #[test]
614 fn paste_payload_remaps_internal_relations() {
615 let document = related_document();
616 let mut selection = CanvasSelection::default();
617 selection.insert_node(NodeId::from("child"));
618 selection.insert_shape(ShapeId::from("group"));
619 let payload = CanvasClipboardPayload::from_document_selection(&document, &selection);
620
621 let pasted = payload.paste_transaction(&document, point(px(16.0), px(24.0)));
622
623 let mut draft = document.clone();
624 draft.apply_transaction(pasted.transaction).unwrap();
625
626 let child = CanvasRecordId::Node(NodeId::from("child-copy"));
627 let group = CanvasRecordId::Shape(ShapeId::from("group-copy"));
628 assert_eq!(draft.relations().parent_of(&child), Some(&group));
629 assert_eq!(
630 draft
631 .relations()
632 .members_of(&group)
633 .cloned()
634 .collect::<Vec<_>>(),
635 vec![child.clone()]
636 );
637 assert_eq!(
638 draft.relations().binding(&BindingId::from("binding-copy")),
639 Some(&CanvasRecordBindingRelation::new(
640 "binding-copy",
641 child,
642 group
643 ))
644 );
645 }
646
647 #[test]
648 fn deserializes_clipboard_payload_without_relations() {
649 let payload = serde_json::from_str::<CanvasClipboardPayload>(
650 r#"{"nodes":[],"edges":[],"shapes":[],"selection":{"nodes":[],"edges":[],"shapes":[],"handles":[]}}"#,
651 )
652 .unwrap();
653
654 assert!(payload.relations.is_empty());
655 }
656
657 fn connected_document() -> CanvasDocument {
658 document_fixture()
659 .node(CanvasNode::new(
660 "a",
661 point(px(0.0), px(0.0)),
662 size(px(10.0), px(10.0)),
663 ))
664 .node(CanvasNode::new(
665 "b",
666 point(px(40.0), px(0.0)),
667 size(px(10.0), px(10.0)),
668 ))
669 .edge(CanvasEdge::new(
670 "a-b",
671 CanvasEndpoint::new("a", None::<&str>),
672 CanvasEndpoint::new("b", None::<&str>),
673 ))
674 .build()
675 }
676
677 fn related_document() -> CanvasDocument {
678 let mut document = document_fixture()
679 .node(CanvasNode::new(
680 "child",
681 point(px(0.0), px(0.0)),
682 size(px(10.0), px(10.0)),
683 ))
684 .shape(CanvasShape::new(
685 "group",
686 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
687 ))
688 .build();
689 document
690 .apply_transaction(CanvasTransaction::new([
691 DocumentCommand::SetRecordParent {
692 child: CanvasRecordId::Node(NodeId::from("child")),
693 parent: CanvasRecordId::Shape(ShapeId::from("group")),
694 },
695 DocumentCommand::AddRecordToGroup {
696 group: CanvasRecordId::Shape(ShapeId::from("group")),
697 member: CanvasRecordId::Node(NodeId::from("child")),
698 },
699 DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
700 "binding",
701 CanvasRecordId::Node(NodeId::from("child")),
702 CanvasRecordId::Shape(ShapeId::from("group")),
703 )),
704 ]))
705 .unwrap();
706 document
707 }
708
709 fn related_tree_document() -> CanvasDocument {
710 let mut document = document_fixture()
711 .shape(CanvasShape::new(
712 "frame",
713 Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(200.0))),
714 ))
715 .shape(CanvasShape::new(
716 "group",
717 Bounds::new(point(px(20.0), px(20.0)), size(px(100.0), px(100.0))),
718 ))
719 .node(CanvasNode::new(
720 "child",
721 point(px(40.0), px(40.0)),
722 size(px(10.0), px(10.0)),
723 ))
724 .node(CanvasNode::new(
725 "peer",
726 point(px(80.0), px(40.0)),
727 size(px(10.0), px(10.0)),
728 ))
729 .edge(CanvasEdge::new(
730 "child-peer",
731 CanvasEndpoint::new("child", None::<&str>),
732 CanvasEndpoint::new("peer", None::<&str>),
733 ))
734 .build();
735 document
736 .apply_transaction(CanvasTransaction::new([
737 DocumentCommand::SetRecordParent {
738 child: CanvasRecordId::Shape(ShapeId::from("group")),
739 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
740 },
741 DocumentCommand::AddRecordToGroup {
742 group: CanvasRecordId::Shape(ShapeId::from("group")),
743 member: CanvasRecordId::Node(NodeId::from("child")),
744 },
745 DocumentCommand::AddRecordToGroup {
746 group: CanvasRecordId::Shape(ShapeId::from("group")),
747 member: CanvasRecordId::Node(NodeId::from("peer")),
748 },
749 DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
750 "binding",
751 CanvasRecordId::Node(NodeId::from("child")),
752 CanvasRecordId::Shape(ShapeId::from("group")),
753 )),
754 ]))
755 .unwrap();
756 document
757 }
758}