Skip to main content

open_gpui_canvas/
record_scope.rs

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(&current) {
294            return true;
295        }
296        pending.extend(document.relations().parent_of(&current).cloned());
297        pending.extend(document.relations().groups_for(&current).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}