Skip to main content

open_gpui_canvas/
schema.rs

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(), &registry)
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                &registry,
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}