Skip to main content

open_gpui_canvas/
runtime.rs

1use crate::{
2    CanvasDefaultEdgeRouter, CanvasDocument, CanvasDocumentDiff, CanvasEdgeRouter,
3    CanvasGeometryFacts, CanvasGraphIndex, CanvasIndexedGraph, CanvasKindRegistry, CanvasRecordId,
4    CanvasResolvedEdgeGeometry, EdgeId, HitOptions, HitRecord, mutation::CanvasCommittedMutation,
5    runtime_query::CanvasRuntimeQuery,
6};
7use indexmap::IndexMap;
8use open_gpui::{Bounds, Pixels, Point};
9
10#[derive(Clone, Debug, Default, PartialEq)]
11pub struct CanvasRuntime {
12    query: CanvasRuntimeQuery,
13    graph_index: CanvasGraphIndex,
14    edge_geometries: IndexMap<EdgeId, CanvasResolvedEdgeGeometry>,
15}
16
17impl CanvasRuntime {
18    pub fn rebuild(document: &CanvasDocument) -> Self {
19        Self::rebuild_with_router(document, &CanvasDefaultEdgeRouter)
20    }
21
22    pub fn rebuild_with_kind_registry(
23        document: &CanvasDocument,
24        kind_registry: &CanvasKindRegistry,
25    ) -> Self {
26        Self::rebuild_with_router_and_optional_kind_registry(
27            document,
28            &CanvasDefaultEdgeRouter,
29            Some(kind_registry),
30        )
31    }
32
33    pub fn rebuild_with_router<R>(document: &CanvasDocument, router: &R) -> Self
34    where
35        R: CanvasEdgeRouter + ?Sized,
36    {
37        Self::rebuild_with_router_and_optional_kind_registry(document, router, None)
38    }
39
40    pub fn rebuild_with_router_and_kind_registry<R>(
41        document: &CanvasDocument,
42        router: &R,
43        kind_registry: &CanvasKindRegistry,
44    ) -> Self
45    where
46        R: CanvasEdgeRouter + ?Sized,
47    {
48        Self::rebuild_with_router_and_optional_kind_registry(document, router, Some(kind_registry))
49    }
50
51    fn rebuild_with_router_and_optional_kind_registry<R>(
52        document: &CanvasDocument,
53        router: &R,
54        kind_registry: Option<&CanvasKindRegistry>,
55    ) -> Self
56    where
57        R: CanvasEdgeRouter + ?Sized,
58    {
59        let query = match kind_registry {
60            Some(kind_registry) => CanvasRuntimeQuery::rebuild_with_router_and_kind_registry(
61                document,
62                router,
63                kind_registry,
64            ),
65            None => CanvasRuntimeQuery::rebuild_with_router(document, router),
66        };
67
68        Self {
69            query,
70            graph_index: CanvasGraphIndex::rebuild(document),
71            edge_geometries: resolve_edge_geometries(document, router, kind_registry),
72        }
73    }
74
75    pub fn apply_committed_mutation(
76        &mut self,
77        document: &CanvasDocument,
78        committed: &CanvasCommittedMutation,
79    ) {
80        self.apply_committed_mutation_with_router(document, committed, &CanvasDefaultEdgeRouter);
81    }
82
83    pub fn apply_committed_mutation_with_kind_registry(
84        &mut self,
85        document: &CanvasDocument,
86        committed: &CanvasCommittedMutation,
87        kind_registry: &CanvasKindRegistry,
88    ) {
89        self.apply_committed_mutation_with_router_and_kind_registry(
90            document,
91            committed,
92            &CanvasDefaultEdgeRouter,
93            kind_registry,
94        );
95    }
96
97    pub fn apply_committed_mutation_with_router<R>(
98        &mut self,
99        document: &CanvasDocument,
100        committed: &CanvasCommittedMutation,
101        router: &R,
102    ) where
103        R: CanvasEdgeRouter + ?Sized,
104    {
105        self.apply_diff_with_router_and_optional_kind_registry(
106            document,
107            committed.diff(),
108            router,
109            None,
110        );
111    }
112
113    pub fn apply_committed_mutation_with_router_and_kind_registry<R>(
114        &mut self,
115        document: &CanvasDocument,
116        committed: &CanvasCommittedMutation,
117        router: &R,
118        kind_registry: &CanvasKindRegistry,
119    ) where
120        R: CanvasEdgeRouter + ?Sized,
121    {
122        self.apply_diff_with_router_and_optional_kind_registry(
123            document,
124            committed.diff(),
125            router,
126            Some(kind_registry),
127        );
128    }
129
130    fn apply_diff_with_router_and_optional_kind_registry<R>(
131        &mut self,
132        document: &CanvasDocument,
133        diff: &CanvasDocumentDiff,
134        router: &R,
135        kind_registry: Option<&CanvasKindRegistry>,
136    ) where
137        R: CanvasEdgeRouter + ?Sized,
138    {
139        if diff.is_empty() {
140            return;
141        }
142
143        match kind_registry {
144            Some(kind_registry) => {
145                self.query
146                    .apply_diff_with_graph_index_router_and_kind_registry(
147                        document,
148                        diff,
149                        &self.graph_index,
150                        router,
151                        kind_registry,
152                    );
153            }
154            None => {
155                self.query.apply_diff_with_graph_index_and_router(
156                    document,
157                    diff,
158                    &self.graph_index,
159                    router,
160                );
161            }
162        }
163        self.graph_index.apply_diff(document, diff);
164        self.apply_edge_geometry_diff(document, diff, router, kind_registry);
165    }
166
167    pub fn edge_geometry(&self, id: &EdgeId) -> Option<&CanvasResolvedEdgeGeometry> {
168        self.edge_geometries.get(id)
169    }
170
171    pub fn graph<'a>(&'a self, document: &'a CanvasDocument) -> CanvasIndexedGraph<'a> {
172        self.graph_index.graph(document)
173    }
174
175    pub fn query(&self, viewport: Bounds<Pixels>) -> impl Iterator<Item = &HitRecord> {
176        self.query.query(viewport)
177    }
178
179    pub fn query_with_options(
180        &self,
181        viewport: Bounds<Pixels>,
182        options: HitOptions,
183    ) -> impl Iterator<Item = &HitRecord> {
184        self.query.query_with_options(viewport, options)
185    }
186
187    pub fn hit_test(
188        &self,
189        point: Point<Pixels>,
190        options: HitOptions,
191    ) -> impl Iterator<Item = &HitRecord> {
192        self.query.hit_test(point, options)
193    }
194
195    pub fn precise_hit_test<'a>(
196        &'a self,
197        document: &'a CanvasDocument,
198        point: Point<Pixels>,
199        options: HitOptions,
200    ) -> impl Iterator<Item = &'a HitRecord> + 'a {
201        self.precise_hit_test_with_facts(CanvasGeometryFacts::new(document), point, options)
202    }
203
204    pub fn precise_hit_test_with_kind_registry<'a>(
205        &'a self,
206        document: &'a CanvasDocument,
207        kind_registry: &'a CanvasKindRegistry,
208        point: Point<Pixels>,
209        options: HitOptions,
210    ) -> impl Iterator<Item = &'a HitRecord> + 'a {
211        self.precise_hit_test_with_facts(
212            CanvasGeometryFacts::with_kind_registry(document, kind_registry),
213            point,
214            options,
215        )
216    }
217
218    pub fn precise_hit_test_with_facts<'a, R>(
219        &'a self,
220        facts: CanvasGeometryFacts<'a, R>,
221        point: Point<Pixels>,
222        options: HitOptions,
223    ) -> impl Iterator<Item = &'a HitRecord> + 'a
224    where
225        R: CanvasEdgeRouter + 'a,
226    {
227        self.query
228            .precise_hit_test_with_facts(facts, &self.edge_geometries, point, options)
229    }
230
231    fn apply_edge_geometry_diff<R>(
232        &mut self,
233        document: &CanvasDocument,
234        diff: &CanvasDocumentDiff,
235        router: &R,
236        kind_registry: Option<&CanvasKindRegistry>,
237    ) where
238        R: CanvasEdgeRouter + ?Sized,
239    {
240        for record_id in &diff.removed {
241            self.remove_edge_geometry(document, record_id);
242        }
243
244        for record_id in diff.updated.iter().chain(&diff.inserted) {
245            self.refresh_edge_geometry(document, record_id, router, kind_registry);
246        }
247    }
248
249    fn refresh_edge_geometry<R>(
250        &mut self,
251        document: &CanvasDocument,
252        record_id: &CanvasRecordId,
253        router: &R,
254        kind_registry: Option<&CanvasKindRegistry>,
255    ) where
256        R: CanvasEdgeRouter + ?Sized,
257    {
258        match record_id {
259            CanvasRecordId::Node(id) => {
260                let edge_ids = self
261                    .graph_index
262                    .incident_edge_ids(id)
263                    .cloned()
264                    .collect::<Vec<_>>();
265                for edge_id in edge_ids {
266                    self.refresh_edge_geometry(
267                        document,
268                        &CanvasRecordId::Edge(edge_id),
269                        router,
270                        kind_registry,
271                    );
272                }
273            }
274            CanvasRecordId::Edge(id) => {
275                let Some(edge) = document.edge(id) else {
276                    self.edge_geometries.shift_remove(id);
277                    return;
278                };
279                let facts = CanvasGeometryFacts::with_router_and_kind_registry(
280                    document,
281                    router,
282                    kind_registry,
283                );
284                match facts.edge_geometry(edge) {
285                    Ok(geometry) => {
286                        self.edge_geometries.insert(id.clone(), geometry);
287                    }
288                    Err(_) => {
289                        self.edge_geometries.shift_remove(id);
290                    }
291                }
292            }
293            CanvasRecordId::Shape(_) => {}
294        }
295    }
296
297    fn remove_edge_geometry(&mut self, document: &CanvasDocument, record_id: &CanvasRecordId) {
298        match record_id {
299            CanvasRecordId::Node(id) => {
300                self.edge_geometries.retain(|edge_id, _| {
301                    document.edge(edge_id).is_some_and(|edge| {
302                        edge.source.node_id != *id && edge.target.node_id != *id
303                    })
304                });
305            }
306            CanvasRecordId::Edge(id) => {
307                self.edge_geometries.shift_remove(id);
308            }
309            CanvasRecordId::Shape(_) => {}
310        }
311    }
312}
313
314fn resolve_edge_geometries<R>(
315    document: &CanvasDocument,
316    router: &R,
317    kind_registry: Option<&CanvasKindRegistry>,
318) -> IndexMap<EdgeId, CanvasResolvedEdgeGeometry>
319where
320    R: CanvasEdgeRouter + ?Sized,
321{
322    let facts = CanvasGeometryFacts::with_router_and_kind_registry(document, router, kind_registry);
323    document
324        .edges()
325        .filter_map(|edge| {
326            facts
327                .edge_geometry(edge)
328                .ok()
329                .map(|geometry| (edge.id.clone(), geometry))
330        })
331        .collect()
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::test_support::document_fixture;
338    use crate::{
339        CanvasEdge, CanvasEndpoint, CanvasHandle, CanvasKindRegistry, CanvasNode,
340        CanvasNodeGeometryPolicy, CanvasNodeHitTest, CanvasNodeInteractionPolicy, CanvasNodeKind,
341        CanvasRoutePath, CanvasRouteRequest, CanvasTransaction, DocumentCommand, EdgeId, NodeId,
342    };
343    use open_gpui::{Bounds, point, px, size};
344
345    #[test]
346    fn runtime_rebuilds_spatial_and_graph_indexes() {
347        let document = connected_document();
348        let runtime = CanvasRuntime::rebuild(&document);
349
350        assert!(runtime.graph_index.contains_edge(&EdgeId::from("a-b")));
351        assert!(
352            runtime
353                .hit_test(point(px(1.0), px(1.0)), HitOptions::default())
354                .any(|record| matches!(&record.target, crate::HitTarget::Node(id) if id == &NodeId::from("a")))
355        );
356    }
357
358    #[test]
359    fn runtime_applies_diff_to_spatial_and_graph_indexes() {
360        let mut document = connected_document();
361        let mut runtime = CanvasRuntime::rebuild(&document);
362        let mut moved = document.node(&NodeId::from("a")).unwrap().clone();
363        moved.position = point(px(100.0), px(0.0));
364
365        let committed = document
366            .commit_transaction(CanvasTransaction::single(DocumentCommand::UpdateNode(
367                moved,
368            )))
369            .unwrap();
370        runtime.apply_committed_mutation(&document, &committed);
371
372        assert!(
373            runtime
374                .hit_test(point(px(101.0), px(1.0)), HitOptions::default())
375                .any(|record| matches!(&record.target, crate::HitTarget::Node(id) if id == &NodeId::from("a")))
376        );
377        assert!(
378            runtime
379                .hit_test(point(px(1.0), px(1.0)), HitOptions::default())
380                .next()
381                .is_none()
382        );
383        assert_eq!(
384            runtime
385                .graph(&document)
386                .incident_edge_count(&NodeId::from("a")),
387            1
388        );
389    }
390
391    #[test]
392    fn runtime_removes_incident_edges_from_graph_after_node_removal() {
393        let mut document = connected_document();
394        let mut runtime = CanvasRuntime::rebuild(&document);
395
396        let committed = document
397            .commit_transaction(CanvasTransaction::single(DocumentCommand::RemoveNode(
398                NodeId::from("a"),
399            )))
400            .unwrap();
401        runtime.apply_committed_mutation(&document, &committed);
402
403        assert!(!runtime.graph_index.contains_edge(&EdgeId::from("a-b")));
404        assert_eq!(
405            runtime
406                .graph(&document)
407                .incident_edge_count(&NodeId::from("b")),
408            0
409        );
410        assert!(
411            runtime
412                .hit_test(point(px(1.0), px(1.0)), HitOptions::default())
413                .next()
414                .is_none()
415        );
416    }
417
418    #[test]
419    fn runtime_caches_edge_geometry_from_custom_router() {
420        let document = connected_document();
421        let runtime = CanvasRuntime::rebuild_with_router(&document, &VerticalDetourRouter);
422        let geometry = runtime.edge_geometry(&EdgeId::from("a-b")).unwrap();
423
424        assert_eq!(
425            geometry.path.document_points(),
426            vec![
427                point(px(5.0), px(5.0)),
428                point(px(5.0), px(80.0)),
429                point(px(25.0), px(5.0)),
430            ]
431        );
432        assert_eq!(geometry.bounds.origin, point(px(-1.0), px(-1.0)));
433        assert_eq!(geometry.bounds.size, size(px(32.0), px(87.0)));
434    }
435
436    #[test]
437    fn runtime_updates_edge_geometry_with_router_after_node_diff() {
438        let mut document = connected_document();
439        let mut runtime = CanvasRuntime::rebuild_with_router(&document, &VerticalDetourRouter);
440        let mut moved = document.node(&NodeId::from("b")).unwrap().clone();
441        moved.position = point(px(40.0), px(0.0));
442
443        let committed = document
444            .commit_transaction(CanvasTransaction::single(DocumentCommand::UpdateNode(
445                moved,
446            )))
447            .unwrap();
448        runtime.apply_committed_mutation_with_router(&document, &committed, &VerticalDetourRouter);
449
450        assert_eq!(
451            runtime
452                .edge_geometry(&EdgeId::from("a-b"))
453                .unwrap()
454                .path
455                .document_points(),
456            vec![
457                point(px(5.0), px(5.0)),
458                point(px(5.0), px(80.0)),
459                point(px(45.0), px(5.0)),
460            ]
461        );
462    }
463
464    #[test]
465    fn runtime_uses_kind_registry_geometry_for_index_and_routes() {
466        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
467        source.kind = "wide".to_string();
468        source
469            .handles
470            .push(CanvasHandle::new("out", point(px(10.0), px(5.0))));
471        let document = document_fixture()
472            .node(source)
473            .node(CanvasNode::new(
474                "b",
475                point(px(20.0), px(0.0)),
476                size(px(10.0), px(10.0)),
477            ))
478            .edge(CanvasEdge::new(
479                "a-b",
480                CanvasEndpoint::new("a", Some("out")),
481                CanvasEndpoint::new("b", None::<&str>),
482            ))
483            .build();
484        let registry = geometry_registry();
485        let runtime = CanvasRuntime::rebuild_with_kind_registry(&document, &registry);
486
487        assert!(runtime.hit_test(point(px(-4.0), px(5.0)), HitOptions::default()).any(
488            |record| matches!(&record.target, crate::HitTarget::Node(id) if id == &NodeId::from("a"))
489        ));
490        assert!(
491            runtime
492                .hit_test(
493                    point(px(30.0), px(5.0)),
494                    HitOptions {
495                        include_handles: true,
496                        ..HitOptions::default()
497                    }
498                )
499                .any(|record| matches!(
500                    &record.target,
501                    crate::HitTarget::Handle { node_id, handle_id }
502                        if node_id == &NodeId::from("a") && handle_id.as_str() == "out"
503                ))
504        );
505        assert_eq!(
506            runtime
507                .edge_geometry(&EdgeId::from("a-b"))
508                .unwrap()
509                .path
510                .document_points(),
511            vec![point(px(30.0), px(5.0)), point(px(25.0), px(5.0))]
512        );
513    }
514
515    #[test]
516    fn runtime_precise_hit_test_uses_kind_policy() {
517        let mut node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
518        node.kind = "right-half".to_string();
519        let document = document_fixture().node(node).build();
520        let mut registry = CanvasKindRegistry::open();
521        registry.register_node_kind(
522            "right-half",
523            CanvasNodeKind::new().with_interaction_policy(RightHalfNodeKind),
524        );
525        let runtime = CanvasRuntime::rebuild_with_kind_registry(&document, &registry);
526
527        assert!(
528            runtime
529                .hit_test(point(px(25.0), px(25.0)), HitOptions::default())
530                .any(|record| matches!(&record.target, crate::HitTarget::Node(id) if id == &NodeId::from("a")))
531        );
532        assert!(
533            runtime
534                .precise_hit_test_with_kind_registry(
535                    &document,
536                    &registry,
537                    point(px(25.0), px(25.0)),
538                    HitOptions::default(),
539                )
540                .next()
541                .is_none()
542        );
543        assert!(
544            runtime
545                .precise_hit_test_with_kind_registry(
546                    &document,
547                    &registry,
548                    point(px(75.0), px(25.0)),
549                    HitOptions::default(),
550                )
551                .any(|record| matches!(&record.target, crate::HitTarget::Node(id) if id == &NodeId::from("a")))
552        );
553    }
554
555    #[test]
556    fn runtime_precise_hit_test_uses_cached_edge_geometry() {
557        let document = connected_document();
558        let router = VerticalDetourRouter;
559        let runtime = CanvasRuntime::rebuild_with_router(&document, &router);
560
561        assert!(
562            runtime
563                .hit_test(point(px(25.0), px(80.0)), HitOptions::default())
564                .any(|record| matches!(&record.target, crate::HitTarget::Edge(id) if id == &EdgeId::from("a-b")))
565        );
566        assert!(
567            runtime
568                .precise_hit_test_with_facts(
569                    CanvasGeometryFacts::with_router(&document, &router),
570                    point(px(25.0), px(80.0)),
571                    HitOptions::default(),
572                )
573                .next()
574                .is_none()
575        );
576        assert!(
577            runtime
578                .precise_hit_test_with_facts(
579                    CanvasGeometryFacts::with_router(&document, &router),
580                    point(px(5.0), px(80.0)),
581                    HitOptions::default(),
582                )
583                .any(|record| matches!(&record.target, crate::HitTarget::Edge(id) if id == &EdgeId::from("a-b")))
584        );
585    }
586
587    #[test]
588    fn runtime_applies_kind_registry_geometry_after_diff() {
589        let mut document = connected_registry_document();
590        let registry = geometry_registry();
591        let mut runtime = CanvasRuntime::rebuild_with_kind_registry(&document, &registry);
592        let mut moved = document.node(&NodeId::from("a")).unwrap().clone();
593        moved.position = point(px(10.0), px(0.0));
594
595        let committed = document
596            .commit_transaction(CanvasTransaction::single(DocumentCommand::UpdateNode(
597                moved,
598            )))
599            .unwrap();
600        runtime.apply_committed_mutation_with_kind_registry(&document, &committed, &registry);
601
602        assert_eq!(
603            runtime
604                .edge_geometry(&EdgeId::from("a-b"))
605                .unwrap()
606                .path
607                .document_points(),
608            vec![point(px(40.0), px(5.0)), point(px(25.0), px(5.0))]
609        );
610        assert!(
611            runtime
612                .hit_test(
613                    point(px(40.0), px(5.0)),
614                    HitOptions {
615                        include_handles: true,
616                        ..HitOptions::default()
617                    }
618                )
619                .any(|record| matches!(
620                    &record.target,
621                    crate::HitTarget::Handle { node_id, handle_id }
622                        if node_id == &NodeId::from("a") && handle_id.as_str() == "out"
623                ))
624        );
625    }
626
627    fn connected_document() -> CanvasDocument {
628        document_fixture()
629            .node(CanvasNode::new(
630                "a",
631                point(px(0.0), px(0.0)),
632                size(px(10.0), px(10.0)),
633            ))
634            .node(CanvasNode::new(
635                "b",
636                point(px(20.0), px(0.0)),
637                size(px(10.0), px(10.0)),
638            ))
639            .edge(CanvasEdge::new(
640                "a-b",
641                CanvasEndpoint::new("a", None::<&str>),
642                CanvasEndpoint::new("b", None::<&str>),
643            ))
644            .build()
645    }
646
647    fn connected_registry_document() -> CanvasDocument {
648        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
649        source.kind = "wide".to_string();
650        source
651            .handles
652            .push(CanvasHandle::new("out", point(px(10.0), px(5.0))));
653        document_fixture()
654            .node(source)
655            .node(CanvasNode::new(
656                "b",
657                point(px(20.0), px(0.0)),
658                size(px(10.0), px(10.0)),
659            ))
660            .edge(CanvasEdge::new(
661                "a-b",
662                CanvasEndpoint::new("a", Some("out")),
663                CanvasEndpoint::new("b", None::<&str>),
664            ))
665            .build()
666    }
667
668    fn geometry_registry() -> CanvasKindRegistry {
669        let mut registry = CanvasKindRegistry::open();
670        registry.register_node_kind(
671            "wide",
672            CanvasNodeKind::new().with_geometry_policy(WideNodeKind),
673        );
674        registry
675    }
676
677    struct WideNodeKind;
678
679    impl CanvasNodeGeometryPolicy for WideNodeKind {
680        fn node_bounds(&self, node: &CanvasNode) -> Option<Bounds<open_gpui::Pixels>> {
681            Some(node.bounds().dilate(px(5.0)))
682        }
683
684        fn handle_position(
685            &self,
686            node: &CanvasNode,
687            handle_id: &crate::HandleId,
688        ) -> Option<open_gpui::Point<open_gpui::Pixels>> {
689            (handle_id.as_str() == "out").then(|| {
690                point(
691                    node.position.x + node.size.width + px(20.0),
692                    node.position.y + px(5.0),
693                )
694            })
695        }
696    }
697
698    struct RightHalfNodeKind;
699
700    impl CanvasNodeInteractionPolicy for RightHalfNodeKind {
701        fn node_contains_point(&self, hit: CanvasNodeHitTest<'_>) -> Option<bool> {
702            Some(hit.point.x >= hit.bounds.center().x)
703        }
704    }
705
706    struct VerticalDetourRouter;
707
708    impl CanvasEdgeRouter for VerticalDetourRouter {
709        fn route_edge(&self, request: CanvasRouteRequest<'_>) -> CanvasRoutePath {
710            CanvasRoutePath::polyline([
711                request.source,
712                point(request.source.x, px(80.0)),
713                request.target,
714            ])
715        }
716    }
717}