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, ®istry);
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, ®istry);
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 ®istry,
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 ®istry,
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, ®istry);
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, ®istry);
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}