Skip to main content

jellyflow_core/core/model/
edge.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::core::ids::PortId;
5
6use super::port::PortKind;
7
8fn is_false(v: &bool) -> bool {
9    !*v
10}
11
12fn edge_view_descriptor_is_default(value: &EdgeViewDescriptor) -> bool {
13    value.is_default()
14}
15
16/// Edge kind.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum EdgeKind {
20    /// Typed data flow.
21    Data,
22    /// Exec/control flow.
23    Exec,
24}
25
26impl EdgeKind {
27    pub fn port_kind(self) -> PortKind {
28        match self {
29            Self::Data => PortKind::Data,
30            Self::Exec => PortKind::Exec,
31        }
32    }
33}
34
35/// Route-style hint for adapter-owned edge drawing.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum EdgeRouteKind {
39    /// Direct line between resolved endpoints.
40    Straight,
41    /// Orthogonal/polyline route with right-angle legs.
42    Orthogonal,
43    /// Cubic bezier route using endpoint sides as curvature hints.
44    Bezier,
45    /// XyFlow-style smooth-step route.
46    SmoothStep,
47}
48
49/// Edge between two ports.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Edge {
52    /// Edge kind.
53    pub kind: EdgeKind,
54    /// Source port.
55    pub from: PortId,
56    /// Target port.
57    pub to: PortId,
58    /// Whether the edge is hidden (XyFlow `edge.hidden`).
59    ///
60    /// Hidden edges are excluded from derived selection and rendering surfaces.
61    #[serde(default, skip_serializing_if = "is_false")]
62    pub hidden: bool,
63
64    /// Whether the edge can be selected (XyFlow `edge.selectable`).
65    ///
66    /// When omitted, the global `NodeGraphInteractionState.edges_selectable` decides.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub selectable: Option<bool>,
69
70    /// Whether the edge can receive keyboard focus (XyFlow `edge.focusable`).
71    ///
72    /// When omitted, the global `NodeGraphInteractionState.edges_focusable` decides.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub focusable: Option<bool>,
75
76    /// Optional edge hit-test interaction width in logical pixels (XyFlow `edge.interactionWidth`).
77    ///
78    /// When omitted, the global `NodeGraphInteractionState.edge_interaction_width` decides.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub interaction_width: Option<f32>,
81
82    /// Whether the edge can be deleted via editor interactions (XyFlow `edge.deletable`).
83    ///
84    /// When omitted, the global `NodeGraphInteractionState.edges_deletable` decides.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub deletable: Option<bool>,
87
88    /// Whether the edge can be reconnected via editor interactions (XyFlow `edge.reconnectable`).
89    ///
90    /// In XyFlow this field is a `boolean | 'source' | 'target'`. `true` enables reconnecting both
91    /// endpoints, `'source'` only enables reconnecting the source endpoint and `'target'` only
92    /// enables reconnecting the target endpoint.
93    ///
94    /// When omitted, the global `NodeGraphInteractionState.edges_reconnectable` decides.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub reconnectable: Option<EdgeReconnectable>,
97
98    /// Opaque edge payload (domain-owned).
99    ///
100    /// This is for product facts that belong to the relationship itself: branch conditions,
101    /// cardinality, error-path facts, and adapter-readable labels that are part of graph meaning.
102    #[serde(default)]
103    pub data: Value,
104
105    /// Renderer-neutral presentation metadata for this edge.
106    ///
107    /// Adapter-ephemeral state such as hover, selection, focused editor widgets, or open toolbars
108    /// must remain outside the graph.
109    #[serde(default, skip_serializing_if = "edge_view_descriptor_is_default")]
110    pub view: EdgeViewDescriptor,
111}
112
113impl Edge {
114    /// Creates an edge with renderer-neutral defaults.
115    pub fn new(kind: EdgeKind, from: PortId, to: PortId) -> Self {
116        Self {
117            kind,
118            from,
119            to,
120            hidden: false,
121            selectable: None,
122            focusable: None,
123            interaction_width: None,
124            deletable: None,
125            reconnectable: None,
126            data: Value::Null,
127            view: EdgeViewDescriptor::default(),
128        }
129    }
130}
131
132/// Renderer-neutral presentation metadata for an edge.
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
134pub struct EdgeViewDescriptor {
135    /// Adapter-facing renderer key.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub renderer_key: Option<String>,
138    /// Adapter-facing label text when the label is a view concern.
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub label: Option<String>,
141    /// Label placement hint.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub label_anchor: Option<EdgeLabelAnchor>,
144    /// Source marker key, interpreted by adapters.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub source_marker_key: Option<String>,
147    /// Target marker key, interpreted by adapters.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub target_marker_key: Option<String>,
150    /// Style token, interpreted by adapters.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub style_token: Option<String>,
153    /// Route-style hint, interpreted by adapters and runtime geometry projections.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub route_kind: Option<EdgeRouteKind>,
156    /// Optional hit-test width hint in logical pixels.
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub hit_target_width: Option<f32>,
159}
160
161impl EdgeViewDescriptor {
162    pub fn new() -> Self {
163        Self::default()
164    }
165
166    pub fn with_renderer_key(mut self, renderer_key: impl Into<String>) -> Self {
167        self.renderer_key = Some(renderer_key.into());
168        self
169    }
170
171    pub fn with_label(mut self, label: impl Into<String>) -> Self {
172        self.label = Some(label.into());
173        self
174    }
175
176    pub fn with_label_anchor(mut self, label_anchor: EdgeLabelAnchor) -> Self {
177        self.label_anchor = Some(label_anchor);
178        self
179    }
180
181    pub fn with_source_marker_key(mut self, source_marker_key: impl Into<String>) -> Self {
182        self.source_marker_key = Some(source_marker_key.into());
183        self
184    }
185
186    pub fn with_target_marker_key(mut self, target_marker_key: impl Into<String>) -> Self {
187        self.target_marker_key = Some(target_marker_key.into());
188        self
189    }
190
191    pub fn with_style_token(mut self, style_token: impl Into<String>) -> Self {
192        self.style_token = Some(style_token.into());
193        self
194    }
195
196    pub fn with_route_kind(mut self, route_kind: EdgeRouteKind) -> Self {
197        self.route_kind = Some(route_kind);
198        self
199    }
200
201    pub fn with_hit_target_width(mut self, hit_target_width: f32) -> Self {
202        self.hit_target_width = Some(hit_target_width);
203        self
204    }
205
206    pub fn is_default(&self) -> bool {
207        self == &Self::default()
208    }
209}
210
211/// Label anchor hint for adapter-owned edge labels.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "snake_case")]
214pub enum EdgeLabelAnchor {
215    Source,
216    Center,
217    Target,
218}
219
220/// Per-edge reconnect enablement (XyFlow `edge.reconnectable`).
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(untagged)]
223pub enum EdgeReconnectable {
224    Bool(bool),
225    Endpoint(EdgeReconnectableEndpoint),
226}
227
228impl EdgeReconnectable {
229    pub fn allows_source(self) -> bool {
230        matches!(
231            self,
232            Self::Bool(true) | Self::Endpoint(EdgeReconnectableEndpoint::Source)
233        )
234    }
235
236    pub fn allows_target(self) -> bool {
237        matches!(
238            self,
239            Self::Bool(true) | Self::Endpoint(EdgeReconnectableEndpoint::Target)
240        )
241    }
242
243    pub fn endpoint_flags(self) -> (bool, bool) {
244        (self.allows_source(), self.allows_target())
245    }
246}
247
248/// Which endpoint is reconnectable (`'source' | 'target'`).
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum EdgeReconnectableEndpoint {
252    Source,
253    Target,
254}