1use crate::{CanvasDocument, CanvasRecordId, CanvasSelection};
2use indexmap::IndexSet;
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
6pub struct CanvasRecordScopeOptions {
7 pub include_structural_descendants: bool,
8 pub include_internal_edges: bool,
9}
10
11impl Default for CanvasRecordScopeOptions {
12 fn default() -> Self {
13 Self::structural()
14 }
15}
16
17impl CanvasRecordScopeOptions {
18 pub const fn explicit() -> Self {
19 Self {
20 include_structural_descendants: false,
21 include_internal_edges: false,
22 }
23 }
24
25 pub const fn structural() -> Self {
26 Self {
27 include_structural_descendants: true,
28 include_internal_edges: false,
29 }
30 }
31
32 pub const fn structural_with_internal_edges() -> Self {
33 Self {
34 include_structural_descendants: true,
35 include_internal_edges: true,
36 }
37 }
38
39 pub const fn with_structural_descendants(
40 mut self,
41 include_structural_descendants: bool,
42 ) -> Self {
43 self.include_structural_descendants = include_structural_descendants;
44 self
45 }
46
47 pub const fn with_internal_edges(mut self, include_internal_edges: bool) -> Self {
48 self.include_internal_edges = include_internal_edges;
49 self
50 }
51}
52
53#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
54#[serde(transparent)]
55pub struct CanvasRecordScope {
56 records: IndexSet<CanvasRecordId>,
57}
58
59impl CanvasRecordScope {
60 pub fn new(records: impl IntoIterator<Item = CanvasRecordId>) -> Self {
61 Self {
62 records: records.into_iter().collect(),
63 }
64 }
65
66 pub fn is_empty(&self) -> bool {
67 self.records.is_empty()
68 }
69
70 pub fn len(&self) -> usize {
71 self.records.len()
72 }
73
74 pub fn contains(&self, record_id: &CanvasRecordId) -> bool {
75 self.records.contains(record_id)
76 }
77
78 pub fn records(&self) -> impl Iterator<Item = &CanvasRecordId> {
79 self.records.iter()
80 }
81
82 pub fn into_records(self) -> Vec<CanvasRecordId> {
83 self.records.into_iter().collect()
84 }
85
86 pub(crate) fn into_index_set(self) -> IndexSet<CanvasRecordId> {
87 self.records
88 }
89}
90
91#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
92pub struct CanvasResolvedSelectionScope {
93 normalized_selection: CanvasSelection,
94 explicit_records: CanvasRecordScope,
95 structural_records: CanvasRecordScope,
96 action_records: CanvasRecordScope,
97}
98
99impl CanvasResolvedSelectionScope {
100 pub fn normalized_selection(&self) -> &CanvasSelection {
101 &self.normalized_selection
102 }
103
104 pub fn explicit_records(&self) -> &CanvasRecordScope {
105 &self.explicit_records
106 }
107
108 pub fn structural_records(&self) -> &CanvasRecordScope {
109 &self.structural_records
110 }
111
112 pub fn action_records(&self) -> &CanvasRecordScope {
113 &self.action_records
114 }
115
116 pub(crate) fn contains_action_record(&self, record_id: &CanvasRecordId) -> bool {
117 self.action_records.contains(record_id)
118 }
119
120 pub(crate) fn contains_paint_structural_record(&self, record_id: &CanvasRecordId) -> bool {
121 !self.explicit_records.contains(record_id) && self.action_records.contains(record_id)
122 }
123
124 pub(crate) fn paint_structural_records(&self) -> impl Iterator<Item = &CanvasRecordId> {
125 let explicit_records = &self.explicit_records;
126 self.action_records
127 .records()
128 .filter(move |record_id| !explicit_records.contains(record_id))
129 }
130
131 pub fn into_action_records(self) -> CanvasRecordScope {
132 self.action_records
133 }
134}
135
136pub fn normalize_selection(
137 document: &CanvasDocument,
138 selection: &CanvasSelection,
139) -> CanvasSelection {
140 let explicit_records = normalize_record_candidates(document, selection.selected_records());
141 let mut normalized = CanvasSelection::default();
142 for record_id in explicit_records {
143 normalized.insert_record(record_id);
144 }
145 for endpoint in selection.selected_handles() {
146 if document.validate_endpoint(endpoint).is_ok() {
147 normalized.insert_handle(endpoint.clone());
148 }
149 }
150 normalized
151}
152
153pub(crate) fn normalize_record_candidates(
154 document: &CanvasDocument,
155 candidates: impl IntoIterator<Item = CanvasRecordId>,
156) -> IndexSet<CanvasRecordId> {
157 let mut candidates = candidates
158 .into_iter()
159 .filter(|record_id| document.contains_record(record_id))
160 .collect::<IndexSet<_>>();
161 let snapshot = candidates.clone();
162 candidates.retain(|record_id| !has_candidate_ancestor(document, record_id, &snapshot));
163 if candidates
164 .iter()
165 .any(|record_id| matches!(record_id, CanvasRecordId::Edge(_)))
166 {
167 suppress_internal_edge_candidates(document, &mut candidates);
168 }
169 candidates
170}
171
172pub fn resolve_selection_scope(
173 document: &CanvasDocument,
174 selection: &CanvasSelection,
175 options: CanvasRecordScopeOptions,
176) -> CanvasResolvedSelectionScope {
177 resolve_selection_scope_with_predicates(
178 document,
179 selection,
180 options,
181 |record_id| document.contains_record(record_id),
182 |record_id| document.contains_record(record_id),
183 )
184}
185
186pub(crate) fn resolve_selection_scope_with_predicates(
187 document: &CanvasDocument,
188 selection: &CanvasSelection,
189 options: CanvasRecordScopeOptions,
190 mut can_traverse: impl FnMut(&CanvasRecordId) -> bool,
191 mut can_include: impl FnMut(&CanvasRecordId) -> bool,
192) -> CanvasResolvedSelectionScope {
193 let normalized_selection = normalize_selection(document, selection);
194 let explicit_records = normalized_selection
195 .selected_records()
196 .collect::<IndexSet<_>>();
197 let structural_records = if options.include_structural_descendants {
198 document
199 .relations()
200 .collect_descendant_records(explicit_records.iter().cloned(), |record_id| {
201 can_traverse(record_id)
202 })
203 } else {
204 IndexSet::new()
205 };
206
207 let mut action_records = IndexSet::new();
208 for record_id in explicit_records.iter().chain(structural_records.iter()) {
209 if can_include(record_id) {
210 action_records.insert(record_id.clone());
211 }
212 }
213
214 if options.include_internal_edges {
215 include_internal_edges(document, &mut action_records, &mut can_include);
216 }
217
218 CanvasResolvedSelectionScope {
219 normalized_selection,
220 explicit_records: CanvasRecordScope::new(explicit_records),
221 structural_records: CanvasRecordScope::new(structural_records),
222 action_records: CanvasRecordScope::new(action_records),
223 }
224}
225
226pub(crate) fn collect_selection_record_scope(
227 document: &CanvasDocument,
228 selection: &CanvasSelection,
229 options: CanvasRecordScopeOptions,
230 mut can_include: impl FnMut(&CanvasRecordId) -> bool,
231) -> IndexSet<CanvasRecordId> {
232 resolve_selection_scope_with_predicates(
233 document,
234 selection,
235 options,
236 |record_id| document.contains_record(record_id),
237 |record_id| can_include(record_id),
238 )
239 .into_action_records()
240 .into_index_set()
241}
242
243pub fn selection_record_scope(
244 document: &CanvasDocument,
245 selection: &CanvasSelection,
246 options: CanvasRecordScopeOptions,
247) -> CanvasRecordScope {
248 resolve_selection_scope(document, selection, options).into_action_records()
249}
250
251pub(crate) fn include_internal_edges(
252 document: &CanvasDocument,
253 records: &mut IndexSet<CanvasRecordId>,
254 can_include: &mut impl FnMut(&CanvasRecordId) -> bool,
255) {
256 let selected_node_ids = records
257 .iter()
258 .filter_map(|record_id| match record_id {
259 CanvasRecordId::Node(id) => Some(id.clone()),
260 CanvasRecordId::Edge(_) | CanvasRecordId::Shape(_) => None,
261 })
262 .collect::<IndexSet<_>>();
263
264 for edge in document.edges().filter(|edge| {
265 selected_node_ids.contains(&edge.source.node_id)
266 && selected_node_ids.contains(&edge.target.node_id)
267 }) {
268 let record_id = CanvasRecordId::Edge(edge.id.clone());
269 if can_include(&record_id) {
270 records.insert(record_id);
271 }
272 }
273}
274
275fn has_candidate_ancestor(
276 document: &CanvasDocument,
277 record_id: &CanvasRecordId,
278 candidates: &IndexSet<CanvasRecordId>,
279) -> bool {
280 let mut pending = document
281 .relations()
282 .parent_of(record_id)
283 .cloned()
284 .into_iter()
285 .chain(document.relations().groups_for(record_id).cloned())
286 .collect::<Vec<_>>();
287 let mut visited = IndexSet::new();
288
289 while let Some(current) = pending.pop() {
290 if !visited.insert(current.clone()) {
291 continue;
292 }
293 if candidates.contains(¤t) {
294 return true;
295 }
296 pending.extend(document.relations().parent_of(¤t).cloned());
297 pending.extend(document.relations().groups_for(¤t).cloned());
298 }
299
300 false
301}
302
303fn suppress_internal_edge_candidates(
304 document: &CanvasDocument,
305 candidates: &mut IndexSet<CanvasRecordId>,
306) {
307 let node_scope = document.relations().collect_related_records(
308 candidates
309 .iter()
310 .filter(|record_id| !matches!(record_id, CanvasRecordId::Edge(_)))
311 .cloned(),
312 |record_id| document.contains_record(record_id),
313 );
314 let selected_node_ids = node_scope
315 .iter()
316 .filter_map(|record_id| match record_id {
317 CanvasRecordId::Node(id) => Some(id.clone()),
318 CanvasRecordId::Edge(_) | CanvasRecordId::Shape(_) => None,
319 })
320 .collect::<IndexSet<_>>();
321
322 candidates.retain(|record_id| match record_id {
323 CanvasRecordId::Edge(id) => document.edge(id).is_none_or(|edge| {
324 !selected_node_ids.contains(&edge.source.node_id)
325 || !selected_node_ids.contains(&edge.target.node_id)
326 }),
327 CanvasRecordId::Node(_) | CanvasRecordId::Shape(_) => true,
328 });
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::test_support::document_fixture;
335 use crate::{
336 CanvasEdge, CanvasEndpoint, CanvasNode, CanvasShape, CanvasTransaction, DocumentCommand,
337 EdgeId, NodeId, ShapeId,
338 };
339 use open_gpui::{Bounds, point, px, size};
340
341 #[test]
342 fn selection_candidates_normalize_descendants_to_selected_ancestor() {
343 let mut document = document_fixture()
344 .shape(CanvasShape::new(
345 "frame",
346 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
347 ))
348 .node(CanvasNode::new(
349 "child",
350 point(px(10.0), px(10.0)),
351 size(px(10.0), px(10.0)),
352 ))
353 .node(CanvasNode::new(
354 "outside",
355 point(px(120.0), px(10.0)),
356 size(px(10.0), px(10.0)),
357 ))
358 .build();
359 document
360 .apply_transaction(CanvasTransaction::single(
361 DocumentCommand::SetRecordParent {
362 child: CanvasRecordId::Node(NodeId::from("child")),
363 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
364 },
365 ))
366 .unwrap();
367 let mut selection = CanvasSelection::default();
368 selection.insert_shape(ShapeId::from("frame"));
369 selection.insert_node(NodeId::from("child"));
370 selection.insert_node(NodeId::from("outside"));
371
372 let normalized = normalize_selection(&document, &selection);
373
374 assert_eq!(
375 normalized.selected_shapes().cloned().collect::<Vec<_>>(),
376 vec![ShapeId::from("frame")]
377 );
378 assert_eq!(
379 normalized.selected_nodes().cloned().collect::<Vec<_>>(),
380 vec![NodeId::from("outside")]
381 );
382 }
383
384 #[test]
385 fn child_only_selection_stays_explicit() {
386 let mut document = document_fixture()
387 .shape(CanvasShape::new(
388 "frame",
389 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
390 ))
391 .node(CanvasNode::new(
392 "child",
393 point(px(10.0), px(10.0)),
394 size(px(10.0), px(10.0)),
395 ))
396 .build();
397 document
398 .apply_transaction(CanvasTransaction::single(
399 DocumentCommand::SetRecordParent {
400 child: CanvasRecordId::Node(NodeId::from("child")),
401 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
402 },
403 ))
404 .unwrap();
405 let mut selection = CanvasSelection::default();
406 selection.insert_node(NodeId::from("child"));
407
408 let normalized = normalize_selection(&document, &selection);
409
410 assert_eq!(
411 normalized.selected_nodes().cloned().collect::<Vec<_>>(),
412 vec![NodeId::from("child")]
413 );
414 assert!(normalized.selected_shapes().next().is_none());
415 }
416
417 #[test]
418 fn resolved_scope_separates_explicit_structural_and_action_records() {
419 let mut document = document_fixture()
420 .shape(CanvasShape::new(
421 "frame",
422 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
423 ))
424 .node(CanvasNode::new(
425 "child",
426 point(px(10.0), px(10.0)),
427 size(px(10.0), px(10.0)),
428 ))
429 .node(CanvasNode::new(
430 "peer",
431 point(px(30.0), px(10.0)),
432 size(px(10.0), px(10.0)),
433 ))
434 .edge(CanvasEdge::new(
435 "child-peer",
436 CanvasEndpoint::new("child", None::<&str>),
437 CanvasEndpoint::new("peer", None::<&str>),
438 ))
439 .build();
440 document
441 .apply_transaction(CanvasTransaction::new([
442 DocumentCommand::SetRecordParent {
443 child: CanvasRecordId::Node(NodeId::from("child")),
444 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
445 },
446 DocumentCommand::AddRecordToGroup {
447 group: CanvasRecordId::Shape(ShapeId::from("frame")),
448 member: CanvasRecordId::Node(NodeId::from("peer")),
449 },
450 ]))
451 .unwrap();
452 let mut selection = CanvasSelection::default();
453 selection.insert_shape(ShapeId::from("frame"));
454
455 let scope = resolve_selection_scope(
456 &document,
457 &selection,
458 CanvasRecordScopeOptions::structural_with_internal_edges(),
459 );
460
461 assert_eq!(
462 scope
463 .normalized_selection()
464 .selected_shapes()
465 .cloned()
466 .collect::<Vec<_>>(),
467 vec![ShapeId::from("frame")]
468 );
469 assert_eq!(
470 scope
471 .explicit_records()
472 .records()
473 .cloned()
474 .collect::<Vec<_>>(),
475 vec![CanvasRecordId::Shape(ShapeId::from("frame"))]
476 );
477 assert_eq!(
478 scope
479 .structural_records()
480 .records()
481 .cloned()
482 .collect::<Vec<_>>(),
483 vec![
484 CanvasRecordId::Node(NodeId::from("child")),
485 CanvasRecordId::Node(NodeId::from("peer")),
486 ]
487 );
488 assert_eq!(
489 scope
490 .action_records()
491 .records()
492 .cloned()
493 .collect::<Vec<_>>(),
494 vec![
495 CanvasRecordId::Shape(ShapeId::from("frame")),
496 CanvasRecordId::Node(NodeId::from("child")),
497 CanvasRecordId::Node(NodeId::from("peer")),
498 CanvasRecordId::Edge(EdgeId::from("child-peer")),
499 ]
500 );
501 }
502
503 #[test]
504 fn internal_edges_are_only_included_when_requested() {
505 let mut document = document_fixture()
506 .shape(CanvasShape::new(
507 "frame",
508 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
509 ))
510 .node(CanvasNode::new(
511 "child",
512 point(px(10.0), px(10.0)),
513 size(px(10.0), px(10.0)),
514 ))
515 .node(CanvasNode::new(
516 "peer",
517 point(px(30.0), px(10.0)),
518 size(px(10.0), px(10.0)),
519 ))
520 .node(CanvasNode::new(
521 "outside",
522 point(px(60.0), px(10.0)),
523 size(px(10.0), px(10.0)),
524 ))
525 .edge(CanvasEdge::new(
526 "internal",
527 CanvasEndpoint::new("child", None::<&str>),
528 CanvasEndpoint::new("peer", None::<&str>),
529 ))
530 .edge(CanvasEdge::new(
531 "external",
532 CanvasEndpoint::new("child", None::<&str>),
533 CanvasEndpoint::new("outside", None::<&str>),
534 ))
535 .build();
536 document
537 .apply_transaction(CanvasTransaction::new([
538 DocumentCommand::SetRecordParent {
539 child: CanvasRecordId::Node(NodeId::from("child")),
540 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
541 },
542 DocumentCommand::SetRecordParent {
543 child: CanvasRecordId::Node(NodeId::from("peer")),
544 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
545 },
546 ]))
547 .unwrap();
548 let mut selection = CanvasSelection::default();
549 selection.insert_shape(ShapeId::from("frame"));
550
551 let without_edges = selection_record_scope(
552 &document,
553 &selection,
554 CanvasRecordScopeOptions::structural(),
555 );
556 let with_edges = selection_record_scope(
557 &document,
558 &selection,
559 CanvasRecordScopeOptions::structural_with_internal_edges(),
560 );
561
562 assert!(!without_edges.contains(&CanvasRecordId::Edge(EdgeId::from("internal"))));
563 assert!(with_edges.contains(&CanvasRecordId::Edge(EdgeId::from("internal"))));
564 assert!(!with_edges.contains(&CanvasRecordId::Edge(EdgeId::from("external"))));
565 }
566
567 #[test]
568 fn traversal_and_action_predicates_are_separate() {
569 let mut locked_frame = CanvasShape::new(
570 "locked-frame",
571 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
572 );
573 locked_frame.locked = true;
574 let mut document = document_fixture()
575 .shape(locked_frame)
576 .node(CanvasNode::new(
577 "child",
578 point(px(10.0), px(10.0)),
579 size(px(10.0), px(10.0)),
580 ))
581 .build();
582 document
583 .apply_transaction(CanvasTransaction::single(
584 DocumentCommand::SetRecordParent {
585 child: CanvasRecordId::Node(NodeId::from("child")),
586 parent: CanvasRecordId::Shape(ShapeId::from("locked-frame")),
587 },
588 ))
589 .unwrap();
590 let mut selection = CanvasSelection::default();
591 selection.insert_shape(ShapeId::from("locked-frame"));
592
593 let scope = resolve_selection_scope_with_predicates(
594 &document,
595 &selection,
596 CanvasRecordScopeOptions::structural(),
597 |record_id| document.contains_record(record_id),
598 |record_id| match record_id {
599 CanvasRecordId::Shape(id) => document.shape(id).is_some_and(|shape| !shape.locked),
600 CanvasRecordId::Node(id) => document.node(id).is_some_and(|node| !node.locked),
601 CanvasRecordId::Edge(_) => false,
602 },
603 );
604
605 assert!(
606 !scope
607 .action_records()
608 .contains(&CanvasRecordId::Shape(ShapeId::from("locked-frame")))
609 );
610 assert!(
611 scope
612 .action_records()
613 .contains(&CanvasRecordId::Node(NodeId::from("child")))
614 );
615 }
616
617 #[test]
618 fn handles_are_retained_in_selection_but_excluded_from_record_scope() {
619 let mut node = CanvasNode::new("node", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
620 node.handles.push(crate::CanvasHandle::new(
621 "handle",
622 point(px(50.0), px(50.0)),
623 ));
624 let document = document_fixture().node(node).build();
625 let mut selection = CanvasSelection::default();
626 selection.insert_target(crate::HitTarget::Handle {
627 node_id: NodeId::from("node"),
628 handle_id: crate::HandleId::from("handle"),
629 });
630
631 let normalized = normalize_selection(&document, &selection);
632 let scope = selection_record_scope(
633 &document,
634 &normalized,
635 CanvasRecordScopeOptions::structural_with_internal_edges(),
636 );
637
638 assert_eq!(normalized.selected_handles().count(), 1);
639 assert!(scope.is_empty());
640 }
641
642 #[test]
643 fn selection_scope_expands_related_descendants_and_internal_edges() {
644 let mut document = document_fixture()
645 .shape(CanvasShape::new(
646 "frame",
647 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
648 ))
649 .node(CanvasNode::new(
650 "child",
651 point(px(10.0), px(10.0)),
652 size(px(10.0), px(10.0)),
653 ))
654 .node(CanvasNode::new(
655 "peer",
656 point(px(30.0), px(10.0)),
657 size(px(10.0), px(10.0)),
658 ))
659 .edge(CanvasEdge::new(
660 "child-peer",
661 CanvasEndpoint::new("child", None::<&str>),
662 CanvasEndpoint::new("peer", None::<&str>),
663 ))
664 .build();
665 document
666 .apply_transaction(CanvasTransaction::new([
667 DocumentCommand::SetRecordParent {
668 child: CanvasRecordId::Node(NodeId::from("child")),
669 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
670 },
671 DocumentCommand::AddRecordToGroup {
672 group: CanvasRecordId::Shape(ShapeId::from("frame")),
673 member: CanvasRecordId::Node(NodeId::from("peer")),
674 },
675 ]))
676 .unwrap();
677 let mut selection = CanvasSelection::default();
678 selection.insert_shape(ShapeId::from("frame"));
679
680 let records = collect_selection_record_scope(
681 &document,
682 &selection,
683 CanvasRecordScopeOptions::structural_with_internal_edges(),
684 |_| true,
685 );
686
687 assert!(records.contains(&CanvasRecordId::Shape(ShapeId::from("frame"))));
688 assert!(records.contains(&CanvasRecordId::Node(NodeId::from("child"))));
689 assert!(records.contains(&CanvasRecordId::Node(NodeId::from("peer"))));
690 assert!(records.contains(&CanvasRecordId::Edge(EdgeId::from("child-peer"))));
691 }
692}