1use crate::{
2 CanvasConnectionEndpointRole, CanvasEdge, CanvasNode, CanvasRecordId, CanvasShape,
3 CanvasTransaction, CanvasValue, DocumentCommand,
4};
5use indexmap::IndexMap;
6use open_gpui::{Bounds, Pixels, Point, Size, px};
7use std::{fmt, sync::Arc};
8use thiserror::Error;
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
11pub enum CanvasRecordKind {
12 Node,
13 Edge,
14 Shape,
15}
16
17impl CanvasRecordKind {
18 pub fn as_str(self) -> &'static str {
19 match self {
20 Self::Node => "node",
21 Self::Edge => "edge",
22 Self::Shape => "shape",
23 }
24 }
25}
26
27impl fmt::Display for CanvasRecordKind {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 f.write_str(self.as_str())
30 }
31}
32
33#[derive(Clone, Debug, Error, Eq, PartialEq)]
34pub enum CanvasSchemaError {
35 #[error(
36 "{record_kind} `{record_id}` with kind `{kind}` is missing required data field `{field}`"
37 )]
38 MissingRequiredData {
39 record_kind: CanvasRecordKind,
40 record_id: CanvasRecordId,
41 kind: String,
42 field: String,
43 },
44 #[error("{record_kind} `{record_id}` with kind `{kind}` has invalid data: {message}")]
45 InvalidData {
46 record_kind: CanvasRecordKind,
47 record_id: CanvasRecordId,
48 kind: String,
49 message: String,
50 },
51}
52
53impl CanvasSchemaError {
54 pub fn missing_required_data(
55 record_kind: CanvasRecordKind,
56 record_id: impl Into<CanvasRecordId>,
57 kind: impl Into<String>,
58 field: impl Into<String>,
59 ) -> Self {
60 Self::MissingRequiredData {
61 record_kind,
62 record_id: record_id.into(),
63 kind: kind.into(),
64 field: field.into(),
65 }
66 }
67
68 pub fn invalid_data(
69 record_kind: CanvasRecordKind,
70 record_id: impl Into<CanvasRecordId>,
71 kind: impl Into<String>,
72 message: impl Into<String>,
73 ) -> Self {
74 Self::InvalidData {
75 record_kind,
76 record_id: record_id.into(),
77 kind: kind.into(),
78 message: message.into(),
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug, PartialEq)]
84pub struct CanvasNodeResizeProposal<'a> {
85 pub node: &'a CanvasNode,
86 pub bounds: Bounds<Pixels>,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq)]
90pub struct CanvasShapeResizeProposal<'a> {
91 pub shape: &'a CanvasShape,
92 pub bounds: Bounds<Pixels>,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
96pub struct CanvasNodeHitTest<'a> {
97 pub node: &'a CanvasNode,
98 pub point: Point<Pixels>,
99 pub bounds: Bounds<Pixels>,
100 pub margin: Pixels,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq)]
104pub struct CanvasNodeBoundsHitTest<'a> {
105 pub node: &'a CanvasNode,
106 pub bounds: Bounds<Pixels>,
107 pub query_bounds: Bounds<Pixels>,
108 pub margin: Pixels,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq)]
112pub struct CanvasShapeHitTest<'a> {
113 pub shape: &'a CanvasShape,
114 pub point: Point<Pixels>,
115 pub bounds: Bounds<Pixels>,
116 pub margin: Pixels,
117}
118
119#[derive(Clone, Copy, Debug, PartialEq)]
120pub struct CanvasShapeBoundsHitTest<'a> {
121 pub shape: &'a CanvasShape,
122 pub bounds: Bounds<Pixels>,
123 pub query_bounds: Bounds<Pixels>,
124 pub margin: Pixels,
125}
126
127#[derive(Clone, Debug, Default, PartialEq)]
128pub struct CanvasKindPaint {
129 pub fill: Option<String>,
130 pub stroke: Option<String>,
131 pub stroke_width: Option<Pixels>,
132 pub corner_radius: Option<Pixels>,
133}
134
135#[derive(Clone, Debug, PartialEq)]
136pub struct CanvasKindLabel {
137 pub text: String,
138 pub inset: Pixels,
139 pub color: Option<String>,
140 pub visible: bool,
141}
142
143impl CanvasKindLabel {
144 pub fn new(text: impl Into<String>) -> Self {
145 Self {
146 text: text.into(),
147 inset: Pixels::ZERO,
148 color: None,
149 visible: true,
150 }
151 }
152
153 pub fn with_inset(mut self, inset: Pixels) -> Self {
154 self.inset = inset;
155 self
156 }
157
158 pub fn with_color(mut self, color: impl Into<String>) -> Self {
159 self.color = Some(color.into());
160 self
161 }
162
163 pub fn hidden(mut self) -> Self {
164 self.visible = false;
165 self
166 }
167}
168
169pub trait CanvasNodeSchemaPolicy: Send + Sync {
170 fn default_data(&self) -> CanvasValue {
171 CanvasValue::new()
172 }
173
174 fn migrate_node(&self, _node: &mut CanvasNode) -> Result<(), CanvasSchemaError> {
175 Ok(())
176 }
177
178 fn validate_node(&self, _node: &CanvasNode) -> Result<(), CanvasSchemaError> {
179 Ok(())
180 }
181}
182
183pub trait CanvasNodeGeometryPolicy: Send + Sync {
184 fn node_bounds(&self, _node: &CanvasNode) -> Option<Bounds<Pixels>> {
185 None
186 }
187
188 fn handle_position(
189 &self,
190 _node: &CanvasNode,
191 _handle_id: &crate::HandleId,
192 ) -> Option<Point<Pixels>> {
193 None
194 }
195}
196
197pub trait CanvasNodeInteractionPolicy: Send + Sync {
198 fn node_contains_point(&self, _hit: CanvasNodeHitTest<'_>) -> Option<bool> {
199 None
200 }
201
202 fn node_intersects_bounds(&self, _hit: CanvasNodeBoundsHitTest<'_>) -> Option<bool> {
203 None
204 }
205
206 fn node_accepts_connection_endpoint(
207 &self,
208 _node: &CanvasNode,
209 _role: CanvasConnectionEndpointRole,
210 ) -> bool {
211 false
212 }
213}
214
215pub trait CanvasNodeRenderPolicy: Send + Sync {
216 fn node_paint(&self, _node: &CanvasNode) -> Option<CanvasKindPaint> {
217 None
218 }
219
220 fn node_label(&self, _node: &CanvasNode) -> Option<CanvasKindLabel> {
221 None
222 }
223}
224
225pub trait CanvasNodeTransformPolicy: Send + Sync {
226 fn resize_node_bounds(
227 &self,
228 proposal: CanvasNodeResizeProposal<'_>,
229 ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
230 Ok(proposal.bounds)
231 }
232}
233
234pub trait CanvasEdgeSchemaPolicy: Send + Sync {
235 fn default_data(&self) -> CanvasValue {
236 CanvasValue::new()
237 }
238
239 fn migrate_edge(&self, _edge: &mut CanvasEdge) -> Result<(), CanvasSchemaError> {
240 Ok(())
241 }
242
243 fn validate_edge(&self, _edge: &CanvasEdge) -> Result<(), CanvasSchemaError> {
244 Ok(())
245 }
246}
247
248pub trait CanvasEdgeRenderPolicy: Send + Sync {
249 fn edge_paint(&self, _edge: &CanvasEdge) -> Option<CanvasKindPaint> {
250 None
251 }
252}
253
254pub trait CanvasShapeSchemaPolicy: Send + Sync {
255 fn default_data(&self) -> CanvasValue {
256 CanvasValue::new()
257 }
258
259 fn migrate_shape(&self, _shape: &mut CanvasShape) -> Result<(), CanvasSchemaError> {
260 Ok(())
261 }
262
263 fn validate_shape(&self, _shape: &CanvasShape) -> Result<(), CanvasSchemaError> {
264 Ok(())
265 }
266}
267
268pub trait CanvasShapeGeometryPolicy: Send + Sync {
269 fn shape_bounds(&self, _shape: &CanvasShape) -> Option<Bounds<Pixels>> {
270 None
271 }
272}
273
274pub trait CanvasShapeInteractionPolicy: Send + Sync {
275 fn shape_contains_point(&self, _hit: CanvasShapeHitTest<'_>) -> Option<bool> {
276 None
277 }
278
279 fn shape_intersects_bounds(&self, _hit: CanvasShapeBoundsHitTest<'_>) -> Option<bool> {
280 None
281 }
282}
283
284pub trait CanvasShapeRenderPolicy: Send + Sync {
285 fn shape_paint(&self, _shape: &CanvasShape) -> Option<CanvasKindPaint> {
286 None
287 }
288
289 fn shape_label(&self, _shape: &CanvasShape) -> Option<CanvasKindLabel> {
290 None
291 }
292}
293
294pub trait CanvasShapeTransformPolicy: Send + Sync {
295 fn resize_shape_bounds(
296 &self,
297 proposal: CanvasShapeResizeProposal<'_>,
298 ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
299 Ok(proposal.bounds)
300 }
301}
302
303#[derive(Clone, Default)]
304pub struct CanvasNodeKind {
305 schema: Option<Arc<dyn CanvasNodeSchemaPolicy>>,
306 geometry: Option<Arc<dyn CanvasNodeGeometryPolicy>>,
307 interaction: Option<Arc<dyn CanvasNodeInteractionPolicy>>,
308 render: Option<Arc<dyn CanvasNodeRenderPolicy>>,
309 transform: Option<Arc<dyn CanvasNodeTransformPolicy>>,
310}
311
312impl CanvasNodeKind {
313 pub fn new() -> Self {
314 Self::default()
315 }
316
317 pub fn with_schema_policy(mut self, policy: impl CanvasNodeSchemaPolicy + 'static) -> Self {
318 self.schema = Some(Arc::new(policy));
319 self
320 }
321
322 pub fn with_geometry_policy(mut self, policy: impl CanvasNodeGeometryPolicy + 'static) -> Self {
323 self.geometry = Some(Arc::new(policy));
324 self
325 }
326
327 pub fn with_interaction_policy(
328 mut self,
329 policy: impl CanvasNodeInteractionPolicy + 'static,
330 ) -> Self {
331 self.interaction = Some(Arc::new(policy));
332 self
333 }
334
335 pub fn with_render_policy(mut self, policy: impl CanvasNodeRenderPolicy + 'static) -> Self {
336 self.render = Some(Arc::new(policy));
337 self
338 }
339
340 pub fn with_transform_policy(
341 mut self,
342 policy: impl CanvasNodeTransformPolicy + 'static,
343 ) -> Self {
344 self.transform = Some(Arc::new(policy));
345 self
346 }
347
348 fn schema_policy(&self) -> Option<&dyn CanvasNodeSchemaPolicy> {
349 self.schema.as_deref()
350 }
351
352 fn geometry_policy(&self) -> Option<&dyn CanvasNodeGeometryPolicy> {
353 self.geometry.as_deref()
354 }
355
356 fn interaction_policy(&self) -> Option<&dyn CanvasNodeInteractionPolicy> {
357 self.interaction.as_deref()
358 }
359
360 fn render_policy(&self) -> Option<&dyn CanvasNodeRenderPolicy> {
361 self.render.as_deref()
362 }
363
364 fn transform_policy(&self) -> Option<&dyn CanvasNodeTransformPolicy> {
365 self.transform.as_deref()
366 }
367}
368
369impl fmt::Debug for CanvasNodeKind {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 f.debug_struct("CanvasNodeKind")
372 .field("has_schema", &self.schema.is_some())
373 .field("has_geometry", &self.geometry.is_some())
374 .field("has_interaction", &self.interaction.is_some())
375 .field("has_render", &self.render.is_some())
376 .field("has_transform", &self.transform.is_some())
377 .finish()
378 }
379}
380
381#[derive(Clone, Default)]
382pub struct CanvasEdgeKind {
383 schema: Option<Arc<dyn CanvasEdgeSchemaPolicy>>,
384 render: Option<Arc<dyn CanvasEdgeRenderPolicy>>,
385}
386
387impl CanvasEdgeKind {
388 pub fn new() -> Self {
389 Self::default()
390 }
391
392 pub fn with_schema_policy(mut self, policy: impl CanvasEdgeSchemaPolicy + 'static) -> Self {
393 self.schema = Some(Arc::new(policy));
394 self
395 }
396
397 pub fn with_render_policy(mut self, policy: impl CanvasEdgeRenderPolicy + 'static) -> Self {
398 self.render = Some(Arc::new(policy));
399 self
400 }
401
402 fn schema_policy(&self) -> Option<&dyn CanvasEdgeSchemaPolicy> {
403 self.schema.as_deref()
404 }
405
406 fn render_policy(&self) -> Option<&dyn CanvasEdgeRenderPolicy> {
407 self.render.as_deref()
408 }
409}
410
411impl fmt::Debug for CanvasEdgeKind {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 f.debug_struct("CanvasEdgeKind")
414 .field("has_schema", &self.schema.is_some())
415 .field("has_render", &self.render.is_some())
416 .finish()
417 }
418}
419
420#[derive(Clone, Default)]
421pub struct CanvasShapeKind {
422 schema: Option<Arc<dyn CanvasShapeSchemaPolicy>>,
423 geometry: Option<Arc<dyn CanvasShapeGeometryPolicy>>,
424 interaction: Option<Arc<dyn CanvasShapeInteractionPolicy>>,
425 render: Option<Arc<dyn CanvasShapeRenderPolicy>>,
426 transform: Option<Arc<dyn CanvasShapeTransformPolicy>>,
427}
428
429impl CanvasShapeKind {
430 pub fn new() -> Self {
431 Self::default()
432 }
433
434 pub fn with_schema_policy(mut self, policy: impl CanvasShapeSchemaPolicy + 'static) -> Self {
435 self.schema = Some(Arc::new(policy));
436 self
437 }
438
439 pub fn with_geometry_policy(
440 mut self,
441 policy: impl CanvasShapeGeometryPolicy + 'static,
442 ) -> Self {
443 self.geometry = Some(Arc::new(policy));
444 self
445 }
446
447 pub fn with_interaction_policy(
448 mut self,
449 policy: impl CanvasShapeInteractionPolicy + 'static,
450 ) -> Self {
451 self.interaction = Some(Arc::new(policy));
452 self
453 }
454
455 pub fn with_render_policy(mut self, policy: impl CanvasShapeRenderPolicy + 'static) -> Self {
456 self.render = Some(Arc::new(policy));
457 self
458 }
459
460 pub fn with_transform_policy(
461 mut self,
462 policy: impl CanvasShapeTransformPolicy + 'static,
463 ) -> Self {
464 self.transform = Some(Arc::new(policy));
465 self
466 }
467
468 fn schema_policy(&self) -> Option<&dyn CanvasShapeSchemaPolicy> {
469 self.schema.as_deref()
470 }
471
472 fn geometry_policy(&self) -> Option<&dyn CanvasShapeGeometryPolicy> {
473 self.geometry.as_deref()
474 }
475
476 fn interaction_policy(&self) -> Option<&dyn CanvasShapeInteractionPolicy> {
477 self.interaction.as_deref()
478 }
479
480 fn render_policy(&self) -> Option<&dyn CanvasShapeRenderPolicy> {
481 self.render.as_deref()
482 }
483
484 fn transform_policy(&self) -> Option<&dyn CanvasShapeTransformPolicy> {
485 self.transform.as_deref()
486 }
487}
488
489impl fmt::Debug for CanvasShapeKind {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 f.debug_struct("CanvasShapeKind")
492 .field("has_schema", &self.schema.is_some())
493 .field("has_geometry", &self.geometry.is_some())
494 .field("has_interaction", &self.interaction.is_some())
495 .field("has_render", &self.render.is_some())
496 .field("has_transform", &self.transform.is_some())
497 .finish()
498 }
499}
500
501#[derive(Clone, Default)]
502pub struct CanvasKindRegistry {
503 node_kinds: IndexMap<String, Arc<CanvasNodeKind>>,
504 edge_kinds: IndexMap<String, Arc<CanvasEdgeKind>>,
505 shape_kinds: IndexMap<String, Arc<CanvasShapeKind>>,
506}
507
508impl fmt::Debug for CanvasKindRegistry {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 f.debug_struct("CanvasKindRegistry")
511 .field(
512 "node_kinds",
513 &self.node_kinds.keys().cloned().collect::<Vec<_>>(),
514 )
515 .field(
516 "edge_kinds",
517 &self.edge_kinds.keys().cloned().collect::<Vec<_>>(),
518 )
519 .field(
520 "shape_kinds",
521 &self.shape_kinds.keys().cloned().collect::<Vec<_>>(),
522 )
523 .finish()
524 }
525}
526
527impl CanvasKindRegistry {
528 pub const GROUP_SHAPE_KIND: &'static str = "group";
529
530 pub fn open() -> Self {
531 let mut registry = Self::default();
532 registry.register_shape_kind(Self::GROUP_SHAPE_KIND, built_in_group_shape_kind());
533 registry
534 }
535
536 pub fn register_node_kind(&mut self, kind: impl Into<String>, node_kind: CanvasNodeKind) {
537 self.node_kinds.insert(kind.into(), Arc::new(node_kind));
538 }
539
540 pub fn register_edge_kind(&mut self, kind: impl Into<String>, edge_kind: CanvasEdgeKind) {
541 self.edge_kinds.insert(kind.into(), Arc::new(edge_kind));
542 }
543
544 pub fn register_shape_kind(&mut self, kind: impl Into<String>, shape_kind: CanvasShapeKind) {
545 self.shape_kinds.insert(kind.into(), Arc::new(shape_kind));
546 }
547
548 pub fn node_kind(&self, kind: &str) -> Option<&CanvasNodeKind> {
549 self.node_kinds.get(kind).map(Arc::as_ref)
550 }
551
552 pub fn edge_kind(&self, kind: &str) -> Option<&CanvasEdgeKind> {
553 self.edge_kinds.get(kind).map(Arc::as_ref)
554 }
555
556 pub fn shape_kind(&self, kind: &str) -> Option<&CanvasShapeKind> {
557 self.shape_kinds.get(kind).map(Arc::as_ref)
558 }
559
560 pub fn normalize_transaction(
561 &self,
562 transaction: CanvasTransaction,
563 ) -> Result<CanvasTransaction, CanvasSchemaError> {
564 Ok(CanvasTransaction {
565 commands: transaction
566 .commands
567 .into_iter()
568 .map(|command| self.normalize_command(command))
569 .collect::<Result<Vec<_>, _>>()?,
570 metadata: transaction.metadata,
571 })
572 }
573
574 pub fn normalize_command(
575 &self,
576 command: DocumentCommand,
577 ) -> Result<DocumentCommand, CanvasSchemaError> {
578 Ok(match command {
579 DocumentCommand::InsertNode(node) => {
580 DocumentCommand::InsertNode(self.normalize_node(node)?)
581 }
582 DocumentCommand::UpdateNode(node) => {
583 DocumentCommand::UpdateNode(self.normalize_node(node)?)
584 }
585 DocumentCommand::InsertEdge(edge) => {
586 DocumentCommand::InsertEdge(self.normalize_edge(edge)?)
587 }
588 DocumentCommand::UpdateEdge(edge) => {
589 DocumentCommand::UpdateEdge(self.normalize_edge(edge)?)
590 }
591 DocumentCommand::InsertShape(shape) => {
592 DocumentCommand::InsertShape(self.normalize_shape(shape)?)
593 }
594 DocumentCommand::UpdateShape(shape) => {
595 DocumentCommand::UpdateShape(self.normalize_shape(shape)?)
596 }
597 command => command,
598 })
599 }
600
601 pub fn normalize_node(&self, mut node: CanvasNode) -> Result<CanvasNode, CanvasSchemaError> {
602 let Some(schema) = self.node_kind(&node.kind) else {
603 return Ok(node);
604 };
605
606 if let Some(schema) = schema.schema_policy() {
607 schema.migrate_node(&mut node)?;
608 merge_default_data(&mut node.data, schema.default_data());
609 schema.validate_node(&node)?;
610 }
611 Ok(node)
612 }
613
614 pub fn normalize_edge(&self, mut edge: CanvasEdge) -> Result<CanvasEdge, CanvasSchemaError> {
615 let Some(schema) = self.edge_kind(&edge.kind) else {
616 return Ok(edge);
617 };
618
619 if let Some(schema) = schema.schema_policy() {
620 schema.migrate_edge(&mut edge)?;
621 merge_default_data(&mut edge.data, schema.default_data());
622 schema.validate_edge(&edge)?;
623 }
624 Ok(edge)
625 }
626
627 pub fn normalize_shape(
628 &self,
629 mut shape: CanvasShape,
630 ) -> Result<CanvasShape, CanvasSchemaError> {
631 let Some(schema) = self.shape_kind(&shape.kind) else {
632 return Ok(shape);
633 };
634
635 if let Some(schema) = schema.schema_policy() {
636 schema.migrate_shape(&mut shape)?;
637 merge_default_data(&mut shape.data, schema.default_data());
638 schema.validate_shape(&shape)?;
639 }
640 Ok(shape)
641 }
642
643 pub fn validate_document(
644 &self,
645 document: &crate::CanvasDocument,
646 ) -> Result<(), CanvasSchemaError> {
647 for node in document.nodes() {
648 if let Some(schema) = self
649 .node_kind(&node.kind)
650 .and_then(CanvasNodeKind::schema_policy)
651 {
652 schema.validate_node(node)?;
653 }
654 }
655
656 for edge in document.edges() {
657 if let Some(schema) = self
658 .edge_kind(&edge.kind)
659 .and_then(CanvasEdgeKind::schema_policy)
660 {
661 schema.validate_edge(edge)?;
662 }
663 }
664
665 for shape in document.shapes() {
666 if let Some(schema) = self
667 .shape_kind(&shape.kind)
668 .and_then(CanvasShapeKind::schema_policy)
669 {
670 schema.validate_shape(shape)?;
671 }
672 }
673
674 Ok(())
675 }
676
677 pub fn node_bounds(&self, node: &CanvasNode) -> Option<Bounds<Pixels>> {
678 self.node_kind(&node.kind)
679 .and_then(CanvasNodeKind::geometry_policy)
680 .and_then(|geometry| geometry.node_bounds(node))
681 }
682
683 pub fn handle_position(
684 &self,
685 node: &CanvasNode,
686 handle_id: &crate::HandleId,
687 ) -> Option<Point<Pixels>> {
688 self.node_kind(&node.kind)
689 .and_then(CanvasNodeKind::geometry_policy)
690 .and_then(|geometry| geometry.handle_position(node, handle_id))
691 }
692
693 pub fn shape_bounds(&self, shape: &CanvasShape) -> Option<Bounds<Pixels>> {
694 self.shape_kind(&shape.kind)
695 .and_then(CanvasShapeKind::geometry_policy)
696 .and_then(|geometry| geometry.shape_bounds(shape))
697 }
698
699 pub fn node_contains_point(
700 &self,
701 node: &CanvasNode,
702 point: Point<Pixels>,
703 bounds: Bounds<Pixels>,
704 margin: Pixels,
705 ) -> Option<bool> {
706 self.node_kind(&node.kind)
707 .and_then(CanvasNodeKind::interaction_policy)
708 .and_then(|interaction| {
709 interaction.node_contains_point(CanvasNodeHitTest {
710 node,
711 point,
712 bounds,
713 margin,
714 })
715 })
716 }
717
718 pub fn node_intersects_bounds(
719 &self,
720 node: &CanvasNode,
721 bounds: Bounds<Pixels>,
722 query_bounds: Bounds<Pixels>,
723 margin: Pixels,
724 ) -> Option<bool> {
725 self.node_kind(&node.kind)
726 .and_then(CanvasNodeKind::interaction_policy)
727 .and_then(|interaction| {
728 interaction.node_intersects_bounds(CanvasNodeBoundsHitTest {
729 node,
730 bounds,
731 query_bounds,
732 margin,
733 })
734 })
735 }
736
737 pub fn node_accepts_connection_endpoint(
738 &self,
739 node: &CanvasNode,
740 role: CanvasConnectionEndpointRole,
741 ) -> bool {
742 self.node_kind(&node.kind)
743 .and_then(CanvasNodeKind::interaction_policy)
744 .is_some_and(|interaction| interaction.node_accepts_connection_endpoint(node, role))
745 }
746
747 pub fn shape_contains_point(
748 &self,
749 shape: &CanvasShape,
750 point: Point<Pixels>,
751 bounds: Bounds<Pixels>,
752 margin: Pixels,
753 ) -> Option<bool> {
754 self.shape_kind(&shape.kind)
755 .and_then(CanvasShapeKind::interaction_policy)
756 .and_then(|interaction| {
757 interaction.shape_contains_point(CanvasShapeHitTest {
758 shape,
759 point,
760 bounds,
761 margin,
762 })
763 })
764 }
765
766 pub fn shape_intersects_bounds(
767 &self,
768 shape: &CanvasShape,
769 bounds: Bounds<Pixels>,
770 query_bounds: Bounds<Pixels>,
771 margin: Pixels,
772 ) -> Option<bool> {
773 self.shape_kind(&shape.kind)
774 .and_then(CanvasShapeKind::interaction_policy)
775 .and_then(|interaction| {
776 interaction.shape_intersects_bounds(CanvasShapeBoundsHitTest {
777 shape,
778 bounds,
779 query_bounds,
780 margin,
781 })
782 })
783 }
784
785 pub fn node_paint(&self, node: &CanvasNode) -> Option<CanvasKindPaint> {
786 self.node_kind(&node.kind)
787 .and_then(CanvasNodeKind::render_policy)
788 .and_then(|render| render.node_paint(node))
789 }
790
791 pub fn shape_paint(&self, shape: &CanvasShape) -> Option<CanvasKindPaint> {
792 self.shape_kind(&shape.kind)
793 .and_then(CanvasShapeKind::render_policy)
794 .and_then(|render| render.shape_paint(shape))
795 }
796
797 pub fn edge_paint(&self, edge: &CanvasEdge) -> Option<CanvasKindPaint> {
798 self.edge_kind(&edge.kind)
799 .and_then(CanvasEdgeKind::render_policy)
800 .and_then(|render| render.edge_paint(edge))
801 }
802
803 pub fn node_label(&self, node: &CanvasNode) -> Option<CanvasKindLabel> {
804 self.node_kind(&node.kind)
805 .and_then(CanvasNodeKind::render_policy)
806 .and_then(|render| render.node_label(node))
807 .filter(|label| label.visible && !label.text.trim().is_empty())
808 }
809
810 pub fn shape_label(&self, shape: &CanvasShape) -> Option<CanvasKindLabel> {
811 self.shape_kind(&shape.kind)
812 .and_then(CanvasShapeKind::render_policy)
813 .and_then(|render| render.shape_label(shape))
814 .filter(|label| label.visible && !label.text.trim().is_empty())
815 }
816
817 pub fn resize_node_bounds(
818 &self,
819 node: &CanvasNode,
820 proposed: Bounds<Pixels>,
821 ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
822 let Some(transform) = self
823 .node_kind(&node.kind)
824 .and_then(CanvasNodeKind::transform_policy)
825 else {
826 return Ok(proposed);
827 };
828
829 let bounds = transform.resize_node_bounds(CanvasNodeResizeProposal {
830 node,
831 bounds: proposed,
832 })?;
833 validate_resize_bounds(CanvasRecordKind::Node, node.id.clone(), &node.kind, bounds)?;
834 Ok(bounds)
835 }
836
837 pub fn resize_shape_bounds(
838 &self,
839 shape: &CanvasShape,
840 proposed: Bounds<Pixels>,
841 ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
842 let Some(transform) = self
843 .shape_kind(&shape.kind)
844 .and_then(CanvasShapeKind::transform_policy)
845 else {
846 return Ok(proposed);
847 };
848
849 let bounds = transform.resize_shape_bounds(CanvasShapeResizeProposal {
850 shape,
851 bounds: proposed,
852 })?;
853 validate_resize_bounds(
854 CanvasRecordKind::Shape,
855 shape.id.clone(),
856 &shape.kind,
857 bounds,
858 )?;
859 Ok(bounds)
860 }
861}
862
863fn built_in_group_shape_kind() -> CanvasShapeKind {
864 CanvasShapeKind::new().with_interaction_policy(GroupShapeKind)
865}
866
867struct GroupShapeKind;
868
869impl CanvasShapeInteractionPolicy for GroupShapeKind {
870 fn shape_contains_point(&self, hit: CanvasShapeHitTest<'_>) -> Option<bool> {
871 let border = group_border_width(hit.shape, hit.margin);
872 let outer = group_outer_bounds(hit.bounds, hit.margin);
873 if !outer.contains(&hit.point) {
874 return Some(false);
875 }
876
877 let Some(inner) = group_inner_bounds(hit.bounds, border) else {
878 return Some(true);
879 };
880 Some(!inner.contains(&hit.point))
881 }
882
883 fn shape_intersects_bounds(&self, hit: CanvasShapeBoundsHitTest<'_>) -> Option<bool> {
884 let outer = group_outer_bounds(hit.bounds, hit.margin);
885 if !outer.intersects(&hit.query_bounds) {
886 return Some(false);
887 }
888
889 let border = group_border_width(hit.shape, hit.margin);
890 match group_inner_bounds(hit.bounds, border) {
891 Some(inner) => Some(!bounds_contains_bounds(inner, hit.query_bounds)),
892 None => Some(true),
893 }
894 }
895}
896
897fn group_border_width(shape: &CanvasShape, margin: Pixels) -> Pixels {
898 shape.style.stroke_width.max(px(4.0)) + margin
899}
900
901fn group_outer_bounds(bounds: Bounds<Pixels>, margin: Pixels) -> Bounds<Pixels> {
902 bounds.dilate(margin)
903}
904
905fn group_inner_bounds(bounds: Bounds<Pixels>, border: Pixels) -> Option<Bounds<Pixels>> {
906 let inner_size = Size {
907 width: (bounds.size.width - border * 2.0).max(Pixels::ZERO),
908 height: (bounds.size.height - border * 2.0).max(Pixels::ZERO),
909 };
910 if inner_size.width == Pixels::ZERO || inner_size.height == Pixels::ZERO {
911 return None;
912 }
913
914 Some(Bounds::new(
915 Point::new(bounds.origin.x + border, bounds.origin.y + border),
916 inner_size,
917 ))
918}
919
920fn bounds_contains_bounds(outer: Bounds<Pixels>, inner: Bounds<Pixels>) -> bool {
921 inner.origin.x >= outer.origin.x
922 && inner.origin.y >= outer.origin.y
923 && inner.origin.x + inner.size.width <= outer.origin.x + outer.size.width
924 && inner.origin.y + inner.size.height <= outer.origin.y + outer.size.height
925}
926
927fn merge_default_data(data: &mut CanvasValue, defaults: CanvasValue) {
928 for (key, value) in defaults {
929 data.entry(key).or_insert(value);
930 }
931}
932
933fn validate_resize_bounds(
934 record_kind: CanvasRecordKind,
935 record_id: impl Into<CanvasRecordId>,
936 kind: &str,
937 bounds: Bounds<Pixels>,
938) -> Result<(), CanvasSchemaError> {
939 if !bounds.origin.x.as_f32().is_finite()
940 || !bounds.origin.y.as_f32().is_finite()
941 || !bounds.size.width.as_f32().is_finite()
942 || !bounds.size.height.as_f32().is_finite()
943 || bounds.size.width <= Pixels::ZERO
944 || bounds.size.height <= Pixels::ZERO
945 {
946 return Err(CanvasSchemaError::invalid_data(
947 record_kind,
948 record_id,
949 kind,
950 "resize policy returned invalid bounds",
951 ));
952 }
953
954 Ok(())
955}
956
957#[cfg(test)]
958mod tests {
959 use super::*;
960 use crate::{
961 CanvasDocument, CanvasEndpoint, CanvasRecordKind, DocumentError, EdgeId, NodeId, ShapeId,
962 test_support::document_fixture,
963 };
964 use open_gpui::{Bounds, point, px, size};
965 use serde_json::{Value, json};
966
967 struct RequiredTitleNodeKind;
968
969 impl CanvasNodeSchemaPolicy for RequiredTitleNodeKind {
970 fn default_data(&self) -> CanvasValue {
971 CanvasValue::from_iter([("title".to_string(), json!("Untitled"))])
972 }
973
974 fn migrate_node(&self, node: &mut CanvasNode) -> Result<(), CanvasSchemaError> {
975 if let Some(value) = node.data.remove("label") {
976 node.data.insert("title".to_string(), value);
977 }
978 Ok(())
979 }
980
981 fn validate_node(&self, node: &CanvasNode) -> Result<(), CanvasSchemaError> {
982 match node.data.get("title") {
983 Some(Value::String(title)) if !title.trim().is_empty() => Ok(()),
984 None => Err(CanvasSchemaError::missing_required_data(
985 CanvasRecordKind::Node,
986 node.id.clone(),
987 &node.kind,
988 "title",
989 )),
990 Some(_) => Err(CanvasSchemaError::invalid_data(
991 CanvasRecordKind::Node,
992 node.id.clone(),
993 &node.kind,
994 "title must be a non-empty string",
995 )),
996 }
997 }
998 }
999
1000 impl CanvasNodeGeometryPolicy for RequiredTitleNodeKind {
1001 fn node_bounds(&self, node: &CanvasNode) -> Option<Bounds<Pixels>> {
1002 Some(node.bounds().dilate(px(10.0)))
1003 }
1004
1005 fn handle_position(
1006 &self,
1007 node: &CanvasNode,
1008 handle_id: &crate::HandleId,
1009 ) -> Option<Point<Pixels>> {
1010 (handle_id.as_str() == "out").then(|| {
1011 point(
1012 node.position.x + node.size.width + px(20.0),
1013 node.position.y,
1014 )
1015 })
1016 }
1017 }
1018
1019 struct RequiredRelationEdgeKind;
1020
1021 impl CanvasEdgeSchemaPolicy for RequiredRelationEdgeKind {
1022 fn validate_edge(&self, edge: &CanvasEdge) -> Result<(), CanvasSchemaError> {
1023 if edge.data.contains_key("relation") {
1024 Ok(())
1025 } else {
1026 Err(CanvasSchemaError::missing_required_data(
1027 CanvasRecordKind::Edge,
1028 edge.id.clone(),
1029 &edge.kind,
1030 "relation",
1031 ))
1032 }
1033 }
1034 }
1035
1036 struct SizedShapeKind;
1037
1038 impl CanvasShapeSchemaPolicy for SizedShapeKind {
1039 fn default_data(&self) -> CanvasValue {
1040 CanvasValue::from_iter([("shapeType".to_string(), json!("box"))])
1041 }
1042
1043 fn validate_shape(&self, shape: &CanvasShape) -> Result<(), CanvasSchemaError> {
1044 if shape.data.contains_key("shapeType") {
1045 Ok(())
1046 } else {
1047 Err(CanvasSchemaError::missing_required_data(
1048 CanvasRecordKind::Shape,
1049 shape.id.clone(),
1050 &shape.kind,
1051 "shapeType",
1052 ))
1053 }
1054 }
1055 }
1056
1057 impl CanvasShapeGeometryPolicy for SizedShapeKind {
1058 fn shape_bounds(&self, shape: &CanvasShape) -> Option<Bounds<Pixels>> {
1059 Some(shape.bounds.dilate(px(5.0)))
1060 }
1061 }
1062
1063 struct MinimumSizeNodeKind;
1064
1065 impl CanvasNodeTransformPolicy for MinimumSizeNodeKind {
1066 fn resize_node_bounds(
1067 &self,
1068 proposal: CanvasNodeResizeProposal<'_>,
1069 ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
1070 Ok(Bounds::new(
1071 proposal.bounds.origin,
1072 size(
1073 proposal.bounds.size.width.max(px(48.0)),
1074 proposal.bounds.size.height.max(px(32.0)),
1075 ),
1076 ))
1077 }
1078 }
1079
1080 struct RejectingShapeResizeKind;
1081
1082 impl CanvasShapeTransformPolicy for RejectingShapeResizeKind {
1083 fn resize_shape_bounds(
1084 &self,
1085 proposal: CanvasShapeResizeProposal<'_>,
1086 ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
1087 Err(CanvasSchemaError::invalid_data(
1088 CanvasRecordKind::Shape,
1089 proposal.shape.id.clone(),
1090 &proposal.shape.kind,
1091 "resize is disabled",
1092 ))
1093 }
1094 }
1095
1096 struct InvalidShapeResizeKind;
1097
1098 struct RightHalfNodeKind;
1099
1100 impl CanvasNodeInteractionPolicy for RightHalfNodeKind {
1101 fn node_contains_point(&self, hit: CanvasNodeHitTest<'_>) -> Option<bool> {
1102 Some(hit.point.x >= hit.bounds.center().x)
1103 }
1104
1105 fn node_intersects_bounds(&self, hit: CanvasNodeBoundsHitTest<'_>) -> Option<bool> {
1106 let active = Bounds::from_corners(
1107 Point::new(hit.bounds.center().x, hit.bounds.origin.y),
1108 Point::new(
1109 hit.bounds.origin.x + hit.bounds.size.width,
1110 hit.bounds.origin.y + hit.bounds.size.height,
1111 ),
1112 )
1113 .dilate(hit.margin);
1114 Some(active.intersects(&hit.query_bounds))
1115 }
1116 }
1117
1118 struct TopHalfShapeKind;
1119
1120 impl CanvasShapeInteractionPolicy for TopHalfShapeKind {
1121 fn shape_contains_point(&self, hit: CanvasShapeHitTest<'_>) -> Option<bool> {
1122 Some(hit.point.y <= hit.bounds.center().y)
1123 }
1124
1125 fn shape_intersects_bounds(&self, hit: CanvasShapeBoundsHitTest<'_>) -> Option<bool> {
1126 let active = Bounds::from_corners(
1127 hit.bounds.origin,
1128 Point::new(
1129 hit.bounds.origin.x + hit.bounds.size.width,
1130 hit.bounds.center().y,
1131 ),
1132 )
1133 .dilate(hit.margin);
1134 Some(active.intersects(&hit.query_bounds))
1135 }
1136 }
1137
1138 struct PaintedNodeKind;
1139
1140 impl CanvasNodeRenderPolicy for PaintedNodeKind {
1141 fn node_paint(&self, _node: &CanvasNode) -> Option<CanvasKindPaint> {
1142 Some(CanvasKindPaint {
1143 fill: Some("#fff8c5".to_string()),
1144 stroke: Some("#bf8700".to_string()),
1145 stroke_width: Some(px(2.0)),
1146 corner_radius: Some(px(10.0)),
1147 })
1148 }
1149
1150 fn node_label(&self, _node: &CanvasNode) -> Option<CanvasKindLabel> {
1151 Some(
1152 CanvasKindLabel::new("Node label")
1153 .with_inset(px(8.0))
1154 .with_color("#24292f"),
1155 )
1156 }
1157 }
1158
1159 struct PaintedEdgeKind;
1160
1161 impl CanvasEdgeRenderPolicy for PaintedEdgeKind {
1162 fn edge_paint(&self, _edge: &CanvasEdge) -> Option<CanvasKindPaint> {
1163 Some(CanvasKindPaint {
1164 fill: None,
1165 stroke: Some("#d1242f".to_string()),
1166 stroke_width: Some(px(5.0)),
1167 corner_radius: None,
1168 })
1169 }
1170 }
1171
1172 struct PaintedShapeKind;
1173
1174 impl CanvasShapeRenderPolicy for PaintedShapeKind {
1175 fn shape_paint(&self, _shape: &CanvasShape) -> Option<CanvasKindPaint> {
1176 Some(CanvasKindPaint {
1177 fill: Some("#ddf4ff".to_string()),
1178 stroke: Some("#0969da".to_string()),
1179 stroke_width: Some(px(3.0)),
1180 corner_radius: Some(px(4.0)),
1181 })
1182 }
1183
1184 fn shape_label(&self, _shape: &CanvasShape) -> Option<CanvasKindLabel> {
1185 Some(
1186 CanvasKindLabel::new("Shape label")
1187 .with_inset(px(4.0))
1188 .with_color("#0969da"),
1189 )
1190 }
1191 }
1192
1193 struct HiddenLabelNodeKind;
1194
1195 impl CanvasNodeRenderPolicy for HiddenLabelNodeKind {
1196 fn node_label(&self, _node: &CanvasNode) -> Option<CanvasKindLabel> {
1197 Some(CanvasKindLabel::new("Hidden").hidden())
1198 }
1199 }
1200
1201 struct EmptyLabelShapeKind;
1202
1203 impl CanvasShapeRenderPolicy for EmptyLabelShapeKind {
1204 fn shape_label(&self, _shape: &CanvasShape) -> Option<CanvasKindLabel> {
1205 Some(CanvasKindLabel::new(" "))
1206 }
1207 }
1208
1209 impl CanvasShapeTransformPolicy for InvalidShapeResizeKind {
1210 fn resize_shape_bounds(
1211 &self,
1212 proposal: CanvasShapeResizeProposal<'_>,
1213 ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
1214 Ok(Bounds::new(
1215 proposal.bounds.origin,
1216 size(px(0.0), proposal.bounds.size.height),
1217 ))
1218 }
1219 }
1220
1221 fn required_title_node_kind() -> CanvasNodeKind {
1222 CanvasNodeKind::new()
1223 .with_schema_policy(RequiredTitleNodeKind)
1224 .with_geometry_policy(RequiredTitleNodeKind)
1225 }
1226
1227 fn relation_edge_kind() -> CanvasEdgeKind {
1228 CanvasEdgeKind::new().with_schema_policy(RequiredRelationEdgeKind)
1229 }
1230
1231 fn sized_shape_kind() -> CanvasShapeKind {
1232 CanvasShapeKind::new()
1233 .with_schema_policy(SizedShapeKind)
1234 .with_geometry_policy(SizedShapeKind)
1235 }
1236
1237 fn minimum_size_node_kind() -> CanvasNodeKind {
1238 CanvasNodeKind::new().with_transform_policy(MinimumSizeNodeKind)
1239 }
1240
1241 fn rejecting_shape_resize_kind() -> CanvasShapeKind {
1242 CanvasShapeKind::new().with_transform_policy(RejectingShapeResizeKind)
1243 }
1244
1245 fn invalid_shape_resize_kind() -> CanvasShapeKind {
1246 CanvasShapeKind::new().with_transform_policy(InvalidShapeResizeKind)
1247 }
1248
1249 fn right_half_node_kind() -> CanvasNodeKind {
1250 CanvasNodeKind::new().with_interaction_policy(RightHalfNodeKind)
1251 }
1252
1253 fn top_half_shape_kind() -> CanvasShapeKind {
1254 CanvasShapeKind::new().with_interaction_policy(TopHalfShapeKind)
1255 }
1256
1257 fn painted_node_kind() -> CanvasNodeKind {
1258 CanvasNodeKind::new().with_render_policy(PaintedNodeKind)
1259 }
1260
1261 fn painted_edge_kind() -> CanvasEdgeKind {
1262 CanvasEdgeKind::new().with_render_policy(PaintedEdgeKind)
1263 }
1264
1265 fn painted_shape_kind() -> CanvasShapeKind {
1266 CanvasShapeKind::new().with_render_policy(PaintedShapeKind)
1267 }
1268
1269 fn hidden_label_node_kind() -> CanvasNodeKind {
1270 CanvasNodeKind::new().with_render_policy(HiddenLabelNodeKind)
1271 }
1272
1273 fn empty_label_shape_kind() -> CanvasShapeKind {
1274 CanvasShapeKind::new().with_render_policy(EmptyLabelShapeKind)
1275 }
1276
1277 #[test]
1278 fn open_registry_leaves_unknown_kinds_unchanged() {
1279 let registry = CanvasKindRegistry::open();
1280 let mut node = CanvasNode::new("n", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
1281 node.kind = "unknown".to_string();
1282 node.data
1283 .insert("custom".to_string(), json!({"kept": true}));
1284
1285 let normalized = registry.normalize_node(node.clone()).unwrap();
1286
1287 assert_eq!(normalized, node);
1288 }
1289
1290 #[test]
1291 fn registered_node_kind_applies_migration_defaults_and_validation() {
1292 let mut registry = CanvasKindRegistry::open();
1293 registry.register_node_kind("note", required_title_node_kind());
1294
1295 let mut node = CanvasNode::new("n", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
1296 node.kind = "note".to_string();
1297 node.data.insert("label".to_string(), json!("Migrated"));
1298 let normalized = registry.normalize_node(node).unwrap();
1299
1300 assert_eq!(normalized.data.get("title"), Some(&json!("Migrated")));
1301
1302 let mut defaulted = CanvasNode::new(
1303 "defaulted",
1304 point(px(0.0), px(0.0)),
1305 size(px(10.0), px(10.0)),
1306 );
1307 defaulted.kind = "note".to_string();
1308 let defaulted = registry.normalize_node(defaulted).unwrap();
1309
1310 assert_eq!(defaulted.data.get("title"), Some(&json!("Untitled")));
1311
1312 let mut invalid =
1313 CanvasNode::new("invalid", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
1314 invalid.kind = "note".to_string();
1315 invalid.data.insert("title".to_string(), json!(false));
1316
1317 assert!(matches!(
1318 registry.normalize_node(invalid),
1319 Err(CanvasSchemaError::InvalidData {
1320 record_kind: CanvasRecordKind::Node,
1321 record_id: CanvasRecordId::Node(id),
1322 kind,
1323 ..
1324 }) if id == NodeId::from("invalid") && kind == "note"
1325 ));
1326 }
1327
1328 #[test]
1329 fn node_policy_categories_are_independent() {
1330 let mut registry = CanvasKindRegistry::open();
1331 registry.register_node_kind(
1332 "schema-only",
1333 CanvasNodeKind::new().with_schema_policy(RequiredTitleNodeKind),
1334 );
1335 registry.register_node_kind(
1336 "geometry-only",
1337 CanvasNodeKind::new().with_geometry_policy(RequiredTitleNodeKind),
1338 );
1339 registry.register_node_kind("render-only", painted_node_kind());
1340 registry.register_node_kind("transform-only", minimum_size_node_kind());
1341 registry.register_node_kind("interaction-only", right_half_node_kind());
1342
1343 let mut schema_node =
1344 CanvasNode::new("schema", point(px(0.0), px(0.0)), size(px(100.0), px(80.0)));
1345 schema_node.kind = "schema-only".to_string();
1346 let normalized = registry.normalize_node(schema_node.clone()).unwrap();
1347 assert_eq!(normalized.data.get("title"), Some(&json!("Untitled")));
1348 assert_eq!(registry.node_bounds(&normalized), None);
1349 assert_eq!(registry.node_paint(&normalized), None);
1350
1351 let mut geometry_node = CanvasNode::new(
1352 "geometry",
1353 point(px(0.0), px(0.0)),
1354 size(px(100.0), px(80.0)),
1355 );
1356 geometry_node.kind = "geometry-only".to_string();
1357 assert_eq!(
1358 registry.normalize_node(geometry_node.clone()).unwrap(),
1359 geometry_node
1360 );
1361 assert_eq!(
1362 registry.node_bounds(&geometry_node),
1363 Some(geometry_node.bounds().dilate(px(10.0)))
1364 );
1365 assert_eq!(registry.node_paint(&geometry_node), None);
1366
1367 let mut render_node =
1368 CanvasNode::new("render", point(px(0.0), px(0.0)), size(px(100.0), px(80.0)));
1369 render_node.kind = "render-only".to_string();
1370 assert!(registry.node_paint(&render_node).is_some());
1371 assert_eq!(registry.node_bounds(&render_node), None);
1372
1373 let mut transform_node = CanvasNode::new(
1374 "transform",
1375 point(px(0.0), px(0.0)),
1376 size(px(100.0), px(80.0)),
1377 );
1378 transform_node.kind = "transform-only".to_string();
1379 assert_eq!(
1380 registry
1381 .resize_node_bounds(
1382 &transform_node,
1383 Bounds::new(point(px(0.0), px(0.0)), size(px(1.0), px(1.0))),
1384 )
1385 .unwrap()
1386 .size,
1387 size(px(48.0), px(32.0))
1388 );
1389 assert_eq!(registry.node_paint(&transform_node), None);
1390
1391 let mut interaction_node = CanvasNode::new(
1392 "interaction",
1393 point(px(0.0), px(0.0)),
1394 size(px(100.0), px(80.0)),
1395 );
1396 interaction_node.kind = "interaction-only".to_string();
1397 assert_eq!(
1398 registry.node_contains_point(
1399 &interaction_node,
1400 point(px(25.0), px(20.0)),
1401 interaction_node.bounds(),
1402 Pixels::ZERO,
1403 ),
1404 Some(false)
1405 );
1406 assert_eq!(registry.node_bounds(&interaction_node), None);
1407 }
1408
1409 #[test]
1410 fn registered_edge_and_shape_kinds_validate_data() {
1411 let mut registry = CanvasKindRegistry::open();
1412 registry.register_edge_kind("relation", relation_edge_kind());
1413 registry.register_shape_kind("box", sized_shape_kind());
1414
1415 let edge = edge_with_kind("relation");
1416 assert!(matches!(
1417 registry.normalize_edge(edge),
1418 Err(CanvasSchemaError::MissingRequiredData {
1419 record_kind: CanvasRecordKind::Edge,
1420 record_id: CanvasRecordId::Edge(id),
1421 field,
1422 ..
1423 }) if id == EdgeId::from("a-b") && field == "relation"
1424 ));
1425
1426 let mut shape = CanvasShape::new(
1427 "shape",
1428 Bounds::new(point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
1429 );
1430 shape.kind = "box".to_string();
1431 let shape = registry.normalize_shape(shape).unwrap();
1432
1433 assert_eq!(shape.data.get("shapeType"), Some(&json!("box")));
1434 assert_eq!(
1435 registry.shape_bounds(&shape).unwrap(),
1436 Bounds::new(point(px(-5.0), px(-5.0)), size(px(20.0), px(20.0)))
1437 );
1438 }
1439
1440 #[test]
1441 fn edge_and_shape_policy_categories_are_independent() {
1442 let mut registry = CanvasKindRegistry::open();
1443 registry.register_edge_kind(
1444 "edge-schema",
1445 CanvasEdgeKind::new().with_schema_policy(RequiredRelationEdgeKind),
1446 );
1447 registry.register_edge_kind("edge-render", painted_edge_kind());
1448 registry.register_shape_kind(
1449 "shape-schema",
1450 CanvasShapeKind::new().with_schema_policy(SizedShapeKind),
1451 );
1452 registry.register_shape_kind(
1453 "shape-geometry",
1454 CanvasShapeKind::new().with_geometry_policy(SizedShapeKind),
1455 );
1456 registry.register_shape_kind("shape-render", painted_shape_kind());
1457 registry.register_shape_kind("shape-transform", rejecting_shape_resize_kind());
1458 registry.register_shape_kind("shape-interaction", top_half_shape_kind());
1459
1460 let mut edge = edge_with_kind("edge-schema");
1461 assert!(matches!(
1462 registry.normalize_edge(edge.clone()),
1463 Err(CanvasSchemaError::MissingRequiredData { .. })
1464 ));
1465 edge.data.insert("relation".to_string(), json!("uses"));
1466 assert_eq!(registry.normalize_edge(edge.clone()).unwrap(), edge);
1467 assert_eq!(registry.edge_paint(&edge), None);
1468
1469 let render_edge = edge_with_kind("edge-render");
1470 assert!(registry.edge_paint(&render_edge).is_some());
1471 assert_eq!(
1472 registry.normalize_edge(render_edge.clone()).unwrap(),
1473 render_edge
1474 );
1475
1476 let mut schema_shape = CanvasShape::new(
1477 "schema-shape",
1478 Bounds::new(point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
1479 );
1480 schema_shape.kind = "shape-schema".to_string();
1481 let normalized = registry.normalize_shape(schema_shape).unwrap();
1482 assert_eq!(normalized.data.get("shapeType"), Some(&json!("box")));
1483 assert_eq!(registry.shape_bounds(&normalized), None);
1484
1485 let mut geometry_shape = CanvasShape::new(
1486 "geometry-shape",
1487 Bounds::new(point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
1488 );
1489 geometry_shape.kind = "shape-geometry".to_string();
1490 assert_eq!(
1491 registry.normalize_shape(geometry_shape.clone()).unwrap(),
1492 geometry_shape
1493 );
1494 assert_eq!(
1495 registry.shape_bounds(&geometry_shape),
1496 Some(Bounds::new(
1497 point(px(-5.0), px(-5.0)),
1498 size(px(20.0), px(20.0))
1499 ))
1500 );
1501
1502 let mut render_shape = CanvasShape::new(
1503 "render-shape",
1504 Bounds::new(point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
1505 );
1506 render_shape.kind = "shape-render".to_string();
1507 assert!(registry.shape_paint(&render_shape).is_some());
1508 assert_eq!(registry.shape_bounds(&render_shape), None);
1509
1510 let mut transform_shape = CanvasShape::new(
1511 "transform-shape",
1512 Bounds::new(point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
1513 );
1514 transform_shape.kind = "shape-transform".to_string();
1515 assert!(matches!(
1516 registry.resize_shape_bounds(&transform_shape, transform_shape.bounds),
1517 Err(CanvasSchemaError::InvalidData { message, .. }) if message == "resize is disabled"
1518 ));
1519 assert_eq!(registry.shape_paint(&transform_shape), None);
1520
1521 let mut interaction_shape = CanvasShape::new(
1522 "interaction-shape",
1523 Bounds::new(point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
1524 );
1525 interaction_shape.kind = "shape-interaction".to_string();
1526 assert_eq!(
1527 registry.shape_contains_point(
1528 &interaction_shape,
1529 point(px(5.0), px(2.0)),
1530 interaction_shape.bounds,
1531 Pixels::ZERO,
1532 ),
1533 Some(true)
1534 );
1535 assert_eq!(
1536 registry.shape_contains_point(
1537 &interaction_shape,
1538 point(px(5.0), px(8.0)),
1539 interaction_shape.bounds,
1540 Pixels::ZERO,
1541 ),
1542 Some(false)
1543 );
1544 assert_eq!(
1545 registry.shape_intersects_bounds(
1546 &interaction_shape,
1547 interaction_shape.bounds,
1548 Bounds::new(point(px(2.0), px(1.0)), size(px(4.0), px(2.0))),
1549 Pixels::ZERO,
1550 ),
1551 Some(true)
1552 );
1553 assert_eq!(
1554 registry.shape_intersects_bounds(
1555 &interaction_shape,
1556 interaction_shape.bounds,
1557 Bounds::new(point(px(2.0), px(8.0)), size(px(4.0), px(2.0))),
1558 Pixels::ZERO,
1559 ),
1560 Some(false)
1561 );
1562 assert_eq!(registry.shape_paint(&interaction_shape), None);
1563 }
1564
1565 #[test]
1566 fn built_in_group_shape_kind_hits_border_but_not_interior() {
1567 let registry = CanvasKindRegistry::open();
1568 let mut group = CanvasShape::new(
1569 "group",
1570 Bounds::new(point(px(10.0), px(20.0)), size(px(100.0), px(80.0))),
1571 );
1572 group.kind = CanvasKindRegistry::GROUP_SHAPE_KIND.to_string();
1573 group.style.stroke_width = px(1.0);
1574
1575 assert_eq!(
1576 registry.shape_contains_point(
1577 &group,
1578 point(px(12.0), px(22.0)),
1579 group.bounds,
1580 Pixels::ZERO,
1581 ),
1582 Some(true)
1583 );
1584 assert_eq!(
1585 registry.shape_contains_point(
1586 &group,
1587 point(px(60.0), px(60.0)),
1588 group.bounds,
1589 Pixels::ZERO,
1590 ),
1591 Some(false)
1592 );
1593 assert_eq!(
1594 registry.shape_contains_point(&group, point(px(60.0), px(60.0)), group.bounds, px(8.0)),
1595 Some(false)
1596 );
1597 assert_eq!(
1598 registry.shape_intersects_bounds(
1599 &group,
1600 group.bounds,
1601 Bounds::new(point(px(40.0), px(40.0)), size(px(20.0), px(20.0))),
1602 Pixels::ZERO,
1603 ),
1604 Some(false)
1605 );
1606 assert_eq!(
1607 registry.shape_intersects_bounds(
1608 &group,
1609 group.bounds,
1610 Bounds::new(point(px(8.0), px(40.0)), size(px(10.0), px(20.0))),
1611 Pixels::ZERO,
1612 ),
1613 Some(true)
1614 );
1615 }
1616
1617 #[test]
1618 fn registered_resize_policy_can_clamp_or_reject_bounds() {
1619 let mut registry = CanvasKindRegistry::open();
1620 registry.register_node_kind("min", minimum_size_node_kind());
1621 registry.register_shape_kind("locked-size", rejecting_shape_resize_kind());
1622
1623 let mut node =
1624 CanvasNode::new("node", point(px(10.0), px(20.0)), size(px(100.0), px(80.0)));
1625 node.kind = "min".to_string();
1626 let bounds = registry
1627 .resize_node_bounds(
1628 &node,
1629 Bounds::new(point(px(10.0), px(20.0)), size(px(12.0), px(8.0))),
1630 )
1631 .unwrap();
1632 assert_eq!(
1633 bounds,
1634 Bounds::new(point(px(10.0), px(20.0)), size(px(48.0), px(32.0)))
1635 );
1636
1637 let mut shape = CanvasShape::new(
1638 "shape",
1639 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(80.0))),
1640 );
1641 shape.kind = "locked-size".to_string();
1642 assert!(matches!(
1643 registry.resize_shape_bounds(&shape, shape.bounds),
1644 Err(CanvasSchemaError::InvalidData {
1645 record_kind: CanvasRecordKind::Shape,
1646 record_id: CanvasRecordId::Shape(id),
1647 kind,
1648 message,
1649 }) if id == ShapeId::from("shape")
1650 && kind == "locked-size"
1651 && message == "resize is disabled"
1652 ));
1653 }
1654
1655 #[test]
1656 fn registered_resize_policy_output_is_validated() {
1657 let mut registry = CanvasKindRegistry::open();
1658 registry.register_shape_kind("invalid-size", invalid_shape_resize_kind());
1659
1660 let mut shape = CanvasShape::new(
1661 "shape",
1662 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(80.0))),
1663 );
1664 shape.kind = "invalid-size".to_string();
1665
1666 assert!(matches!(
1667 registry.resize_shape_bounds(&shape, shape.bounds),
1668 Err(CanvasSchemaError::InvalidData {
1669 record_kind: CanvasRecordKind::Shape,
1670 record_id: CanvasRecordId::Shape(id),
1671 kind,
1672 message,
1673 }) if id == ShapeId::from("shape")
1674 && kind == "invalid-size"
1675 && message == "resize policy returned invalid bounds"
1676 ));
1677 }
1678
1679 #[test]
1680 fn registered_hit_policy_can_reject_points_inside_bounds() {
1681 let mut registry = CanvasKindRegistry::open();
1682 registry.register_node_kind("right-half", right_half_node_kind());
1683
1684 let mut node = CanvasNode::new("node", point(px(0.0), px(0.0)), size(px(100.0), px(80.0)));
1685 node.kind = "right-half".to_string();
1686 let bounds = node.bounds();
1687
1688 assert_eq!(
1689 registry.node_contains_point(&node, point(px(25.0), px(20.0)), bounds, Pixels::ZERO),
1690 Some(false)
1691 );
1692 assert_eq!(
1693 registry.node_contains_point(&node, point(px(75.0), px(20.0)), bounds, Pixels::ZERO),
1694 Some(true)
1695 );
1696 }
1697
1698 #[test]
1699 fn registered_paint_policy_supplies_renderer_neutral_defaults() {
1700 let mut registry = CanvasKindRegistry::open();
1701 registry.register_node_kind("painted-node", painted_node_kind());
1702 registry.register_edge_kind("painted-edge", painted_edge_kind());
1703 registry.register_shape_kind("painted-shape", painted_shape_kind());
1704 registry.register_node_kind("hidden-label", hidden_label_node_kind());
1705 registry.register_shape_kind("empty-label", empty_label_shape_kind());
1706
1707 let mut node = CanvasNode::new("node", point(px(0.0), px(0.0)), size(px(100.0), px(80.0)));
1708 node.kind = "painted-node".to_string();
1709 assert_eq!(
1710 registry.node_paint(&node),
1711 Some(CanvasKindPaint {
1712 fill: Some("#fff8c5".to_string()),
1713 stroke: Some("#bf8700".to_string()),
1714 stroke_width: Some(px(2.0)),
1715 corner_radius: Some(px(10.0)),
1716 })
1717 );
1718 assert_eq!(
1719 registry.node_label(&node),
1720 Some(
1721 CanvasKindLabel::new("Node label")
1722 .with_inset(px(8.0))
1723 .with_color("#24292f")
1724 )
1725 );
1726
1727 let mut edge = CanvasEdge::new(
1728 "edge",
1729 CanvasEndpoint::new("source", None::<&str>),
1730 CanvasEndpoint::new("target", None::<&str>),
1731 );
1732 edge.kind = "painted-edge".to_string();
1733 assert_eq!(
1734 registry.edge_paint(&edge),
1735 Some(CanvasKindPaint {
1736 fill: None,
1737 stroke: Some("#d1242f".to_string()),
1738 stroke_width: Some(px(5.0)),
1739 corner_radius: None,
1740 })
1741 );
1742
1743 let mut shape = CanvasShape::new(
1744 "shape",
1745 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(80.0))),
1746 );
1747 shape.kind = "painted-shape".to_string();
1748 assert_eq!(
1749 registry.shape_paint(&shape),
1750 Some(CanvasKindPaint {
1751 fill: Some("#ddf4ff".to_string()),
1752 stroke: Some("#0969da".to_string()),
1753 stroke_width: Some(px(3.0)),
1754 corner_radius: Some(px(4.0)),
1755 })
1756 );
1757 assert_eq!(
1758 registry.shape_label(&shape),
1759 Some(
1760 CanvasKindLabel::new("Shape label")
1761 .with_inset(px(4.0))
1762 .with_color("#0969da")
1763 )
1764 );
1765
1766 let mut hidden_label_node =
1767 CanvasNode::new("hidden", point(px(0.0), px(0.0)), size(px(100.0), px(80.0)));
1768 hidden_label_node.kind = "hidden-label".to_string();
1769 assert_eq!(registry.node_label(&hidden_label_node), None);
1770
1771 let mut empty_label_shape = CanvasShape::new(
1772 "empty",
1773 Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(80.0))),
1774 );
1775 empty_label_shape.kind = "empty-label".to_string();
1776 assert_eq!(registry.shape_label(&empty_label_shape), None);
1777
1778 node.kind = "unknown".to_string();
1779 assert_eq!(registry.node_paint(&node), None);
1780 assert_eq!(registry.node_label(&node), None);
1781 edge.kind = "unknown".to_string();
1782 assert_eq!(registry.edge_paint(&edge), None);
1783 shape.kind = "unknown".to_string();
1784 assert_eq!(registry.shape_label(&shape), None);
1785 }
1786
1787 #[test]
1788 fn document_from_snapshot_runs_registered_kind_normalization() {
1789 let mut registry = CanvasKindRegistry::open();
1790 registry.register_node_kind("note", required_title_node_kind());
1791
1792 let mut node = CanvasNode::new("n", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
1793 node.kind = "note".to_string();
1794 node.data.insert("label".to_string(), json!("Snapshot"));
1795 let document = document_fixture().node(node).build();
1796
1797 let loaded =
1798 CanvasDocument::from_snapshot_with_kind_registry(document.to_snapshot(), ®istry)
1799 .unwrap();
1800
1801 assert_eq!(
1802 loaded.node(&NodeId::from("n")).unwrap().data.get("title"),
1803 Some(&json!("Snapshot"))
1804 );
1805 }
1806
1807 #[test]
1808 fn document_mutation_path_rejects_registered_kind_errors_atomically() {
1809 let mut registry = CanvasKindRegistry::open();
1810 registry.register_node_kind("note", required_title_node_kind());
1811 let mut node = CanvasNode::new("n", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
1812 node.kind = "note".to_string();
1813 node.data.insert("title".to_string(), json!(false));
1814 let mut document = document_fixture().build();
1815
1816 let err = document
1817 .commit_transaction_with_kind_registry(
1818 CanvasTransaction::single(DocumentCommand::InsertNode(node)),
1819 ®istry,
1820 )
1821 .unwrap_err();
1822
1823 assert!(matches!(
1824 err,
1825 DocumentError::Schema(CanvasSchemaError::InvalidData { .. })
1826 ));
1827 assert_eq!(document.node_count(), 0);
1828 }
1829
1830 fn edge_with_kind(kind: &str) -> CanvasEdge {
1831 let mut edge = CanvasEdge::new(
1832 "a-b",
1833 CanvasEndpoint::new("a", None::<&str>),
1834 CanvasEndpoint::new("b", None::<&str>),
1835 );
1836 edge.kind = kind.to_string();
1837 edge
1838 }
1839
1840 #[test]
1841 fn record_ids_format_with_record_kind_prefix() {
1842 assert_eq!(
1843 CanvasRecordId::Node(NodeId::from("n")).to_string(),
1844 "node:n"
1845 );
1846 assert_eq!(
1847 CanvasRecordId::Edge(EdgeId::from("e")).to_string(),
1848 "edge:e"
1849 );
1850 assert_eq!(
1851 CanvasRecordId::Shape(ShapeId::from("s")).to_string(),
1852 "shape:s"
1853 );
1854 }
1855}