flow_rs_core/
handle.rs

1//! Handle system for precise node connections
2//!
3//! Handles are connection points on nodes that allow for precise edge attachment.
4//! They support input/output semantics, type validation, and positioning.
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use crate::error::{FlowError, Result};
10use crate::types::{NodeId, Position, Size};
11
12/// Handle identifier
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15pub struct HandleId(pub String);
16
17impl HandleId {
18    /// Create a new handle ID
19    pub fn new(id: impl Into<String>) -> Self {
20        Self(id.into())
21    }
22
23    /// Get handle ID as string
24    pub fn as_str(&self) -> &str {
25        &self.0
26    }
27}
28
29impl From<&str> for HandleId {
30    fn from(id: &str) -> Self {
31        Self(id.to_string())
32    }
33}
34
35impl From<String> for HandleId {
36    fn from(id: String) -> Self {
37        Self(id)
38    }
39}
40
41/// Handle type indicating connection direction
42#[derive(Debug, Clone, PartialEq)]
43#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
44pub enum HandleType {
45    Source,
46    Target,
47}
48
49impl HandleType {
50    /// Check if this is a source handle
51    pub fn is_source(&self) -> bool {
52        matches!(self, HandleType::Source)
53    }
54
55    /// Check if this is a target handle
56    pub fn is_target(&self) -> bool {
57        matches!(self, HandleType::Target)
58    }
59}
60
61/// Handle position relative to node
62#[derive(Debug, Clone, PartialEq)]
63#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
64pub enum HandlePosition {
65    Top,
66    Right,
67    Bottom,
68    Left,
69    Custom(Position),
70}
71
72impl HandlePosition {
73    /// Convert handle position to absolute position relative to node
74    pub fn to_position(&self) -> Position {
75        match self {
76            HandlePosition::Top => Position::new(0.0, -10.0),
77            HandlePosition::Right => Position::new(80.0, 30.0),
78            HandlePosition::Bottom => Position::new(40.0, 60.0),
79            HandlePosition::Left => Position::new(0.0, 30.0),
80            HandlePosition::Custom(pos) => *pos,
81        }
82    }
83}
84
85/// Connection handle on a node
86#[derive(Debug, Clone, PartialEq)]
87#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
88pub struct Handle {
89    pub id: HandleId,
90    pub handle_type: HandleType,
91    pub position: HandlePosition,
92    pub connection_limit: Option<usize>,
93    pub valid_connection_types: Option<Vec<String>>,
94    pub style: Option<String>,
95}
96
97impl Handle {
98    /// Create a new handle
99    pub fn new(id: impl Into<HandleId>, handle_type: HandleType, position: HandlePosition) -> Self {
100        Self {
101            id: id.into(),
102            handle_type,
103            position,
104            connection_limit: None,
105            valid_connection_types: None,
106            style: None,
107        }
108    }
109
110    /// Create a source handle
111    pub fn source(id: impl Into<HandleId>, position: HandlePosition) -> Self {
112        Self::new(id, HandleType::Source, position)
113    }
114
115    /// Create a target handle
116    pub fn target(id: impl Into<HandleId>, position: HandlePosition) -> Self {
117        Self::new(id, HandleType::Target, position)
118    }
119
120    /// Set connection limit
121    pub fn with_connection_limit(mut self, limit: usize) -> Self {
122        self.connection_limit = Some(limit);
123        self
124    }
125
126    /// Set valid connection types
127    pub fn with_connection_types(mut self, types: Vec<String>) -> Self {
128        self.valid_connection_types = Some(types);
129        self
130    }
131
132    /// Set style
133    pub fn with_style(mut self, style: impl Into<String>) -> Self {
134        self.style = Some(style.into());
135        self
136    }
137
138    /// Calculate absolute position of handle given node position and size
139    pub fn absolute_position(&self, node_pos: Position, node_size: Size) -> Position {
140        match &self.position {
141            HandlePosition::Top => Position::new(node_pos.x + node_size.width / 2.0, node_pos.y),
142            HandlePosition::Right => Position::new(
143                node_pos.x + node_size.width,
144                node_pos.y + node_size.height / 2.0,
145            ),
146            HandlePosition::Bottom => Position::new(
147                node_pos.x + node_size.width / 2.0,
148                node_pos.y + node_size.height,
149            ),
150            HandlePosition::Left => Position::new(node_pos.x, node_pos.y + node_size.height / 2.0),
151            HandlePosition::Custom(pos) => Position::new(node_pos.x + pos.x, node_pos.y + pos.y),
152        }
153    }
154
155    /// Check if a point is within the handle's bounds
156    pub fn contains_point(
157        &self,
158        point: Position,
159        node_pos: Position,
160        node_size: Size,
161        handle_size: f64,
162    ) -> bool {
163        let handle_pos = self.absolute_position(node_pos, node_size);
164        let half_size = handle_size / 2.0;
165
166        point.x >= handle_pos.x - half_size
167            && point.x <= handle_pos.x + half_size
168            && point.y >= handle_pos.y - half_size
169            && point.y <= handle_pos.y + half_size
170    }
171
172    /// Check if this handle can connect to another handle
173    pub fn can_connect_to(&self, other: &Handle) -> bool {
174        // Can't connect to same type (source to source, target to target)
175        if self.handle_type == other.handle_type {
176            return false;
177        }
178
179        // Check connection type compatibility
180        if let (Some(self_types), Some(other_types)) =
181            (&self.valid_connection_types, &other.valid_connection_types)
182        {
183            // At least one matching type required
184            return self_types.iter().any(|t| other_types.contains(t));
185        }
186
187        true // No type restrictions
188    }
189}
190
191/// Handle manager for a node
192#[derive(Debug, Clone, PartialEq)]
193#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
194pub struct HandleManager {
195    handles: Vec<Handle>,
196    node_id: NodeId,
197}
198
199impl Default for HandleManager {
200    fn default() -> Self {
201        Self {
202            handles: Vec::new(),
203            node_id: "default".into(),
204        }
205    }
206}
207
208impl HandleManager {
209    /// Create a new handle manager
210    pub fn new(node_id: NodeId) -> Self {
211        Self {
212            handles: Vec::new(),
213            node_id,
214        }
215    }
216
217    /// Add a handle
218    pub fn add_handle(&mut self, handle: Handle) -> Result<()> {
219        // Check for duplicate handle IDs
220        if self.handles.iter().any(|h| h.id == handle.id) {
221            return Err(FlowError::invalid_operation(format!(
222                "Handle '{}' already exists",
223                handle.id.as_str()
224            )));
225        }
226
227        self.handles.push(handle);
228        Ok(())
229    }
230
231    /// Remove a handle
232    pub fn remove_handle(&mut self, handle_id: &HandleId) -> Result<Handle> {
233        let index = self
234            .handles
235            .iter()
236            .position(|h| &h.id == handle_id)
237            .ok_or_else(|| {
238                FlowError::invalid_operation(format!("Handle '{}' not found", handle_id.as_str()))
239            })?;
240
241        Ok(self.handles.remove(index))
242    }
243
244    /// Get a handle by ID
245    pub fn get_handle(&self, handle_id: &HandleId) -> Option<&Handle> {
246        self.handles.iter().find(|h| &h.id == handle_id)
247    }
248
249    /// Get all handles
250    pub fn handles(&self) -> &[Handle] {
251        &self.handles
252    }
253
254    /// Find handle at position
255    pub fn handle_at_position(
256        &self,
257        point: Position,
258        node_pos: Position,
259        node_size: Size,
260        handle_size: f64,
261    ) -> Option<&Handle> {
262        self.handles
263            .iter()
264            .find(|handle| handle.contains_point(point, node_pos, node_size, handle_size))
265    }
266
267    /// Get source handles
268    pub fn source_handles(&self) -> impl Iterator<Item = &Handle> {
269        self.handles
270            .iter()
271            .filter(|h| h.handle_type == HandleType::Source)
272    }
273
274    /// Get target handles
275    pub fn target_handles(&self) -> impl Iterator<Item = &Handle> {
276        self.handles
277            .iter()
278            .filter(|h| h.handle_type == HandleType::Target)
279    }
280
281    /// Count connections for a handle
282    ///
283    /// Note: This method requires graph integration to work properly.
284    /// Use `Graph::get_handle_connections()` directly for accurate counting.
285    pub fn connection_count(&self, _handle_id: &HandleId) -> usize {
286        // This method is deprecated in favor of Graph-level connection counting
287        // because it needs access to the graph's edge collection to count connections accurately.
288        // Use graph.get_handle_connections(node_id, handle_id).len() instead.
289        0
290    }
291
292    /// Check if handle can accept new connections
293    ///
294    /// Note: This method requires graph integration to work properly.
295    /// Use `Graph::can_handle_accept_connection()` for accurate validation.
296    pub fn can_accept_connection(&self, handle_id: &HandleId) -> bool {
297        if let Some(handle) = self.get_handle(handle_id) {
298            if let Some(_limit) = handle.connection_limit {
299                // This method is deprecated in favor of Graph-level validation
300                // because it needs access to the graph's edge collection.
301                // Use graph.can_handle_accept_connection(node_id, handle_id) instead.
302                return true; // Conservative default
303            }
304        }
305        true
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use crate::types::{Position, Size};
313
314    #[test]
315    fn test_handle_creation() {
316        let handle = Handle::new("output", HandleType::Source, HandlePosition::Right);
317
318        assert_eq!(handle.id.as_str(), "output");
319        assert_eq!(handle.handle_type, HandleType::Source);
320        assert_eq!(handle.position, HandlePosition::Right);
321        assert!(handle.connection_limit.is_none());
322    }
323
324    #[test]
325    fn test_handle_builder_methods() {
326        let handle = Handle::source("out", HandlePosition::Right)
327            .with_connection_limit(1)
328            .with_connection_types(vec!["data".to_string()])
329            .with_style("custom");
330
331        assert_eq!(handle.connection_limit, Some(1));
332        assert_eq!(
333            handle.valid_connection_types,
334            Some(vec!["data".to_string()])
335        );
336        assert_eq!(handle.style, Some("custom".to_string()));
337    }
338
339    #[test]
340    fn test_absolute_position_calculation() {
341        let node_pos = Position::new(100.0, 200.0);
342        let node_size = Size::new(80.0, 60.0);
343
344        // Test standard positions
345        let top_handle = Handle::new("top", HandleType::Source, HandlePosition::Top);
346        assert_eq!(
347            top_handle.absolute_position(node_pos, node_size),
348            Position::new(140.0, 200.0) // x: 100 + 40, y: 200
349        );
350
351        let right_handle = Handle::new("right", HandleType::Source, HandlePosition::Right);
352        assert_eq!(
353            right_handle.absolute_position(node_pos, node_size),
354            Position::new(180.0, 230.0) // x: 100 + 80, y: 200 + 30
355        );
356
357        let custom_handle = Handle::new(
358            "custom",
359            HandleType::Source,
360            HandlePosition::Custom(Position::new(10.0, 20.0)),
361        );
362        assert_eq!(
363            custom_handle.absolute_position(node_pos, node_size),
364            Position::new(110.0, 220.0) // x: 100 + 10, y: 200 + 20
365        );
366    }
367
368    #[test]
369    fn test_point_inside_handle() {
370        let handle = Handle::new("test", HandleType::Source, HandlePosition::Right);
371        let node_pos = Position::new(0.0, 0.0);
372        let node_size = Size::new(100.0, 50.0);
373        let handle_size = 10.0;
374
375        // Handle is at (100, 25) with size 10x10
376        assert!(handle.contains_point(
377            Position::new(100.0, 25.0),
378            node_pos,
379            node_size,
380            handle_size
381        ));
382        assert!(handle.contains_point(Position::new(95.0, 25.0), node_pos, node_size, handle_size));
383        assert!(handle.contains_point(
384            Position::new(105.0, 25.0),
385            node_pos,
386            node_size,
387            handle_size
388        ));
389        assert!(!handle.contains_point(
390            Position::new(90.0, 25.0),
391            node_pos,
392            node_size,
393            handle_size
394        ));
395        assert!(!handle.contains_point(
396            Position::new(100.0, 35.0),
397            node_pos,
398            node_size,
399            handle_size
400        ));
401    }
402
403    #[test]
404    fn test_handle_connection_compatibility() {
405        let source_handle = Handle::source("out", HandlePosition::Right);
406        let target_handle = Handle::target("in", HandlePosition::Left);
407        let another_source = Handle::source("out2", HandlePosition::Bottom);
408
409        // Source can connect to target
410        assert!(source_handle.can_connect_to(&target_handle));
411        assert!(target_handle.can_connect_to(&source_handle));
412
413        // Source cannot connect to source
414        assert!(!source_handle.can_connect_to(&another_source));
415    }
416
417    #[test]
418    fn test_handle_connection_type_validation() {
419        let data_source = Handle::source("data_out", HandlePosition::Right)
420            .with_connection_types(vec!["data".to_string()]);
421        let data_target = Handle::target("data_in", HandlePosition::Left)
422            .with_connection_types(vec!["data".to_string()]);
423        let control_target = Handle::target("control_in", HandlePosition::Left)
424            .with_connection_types(vec!["control".to_string()]);
425
426        // Compatible types
427        assert!(data_source.can_connect_to(&data_target));
428
429        // Incompatible types
430        assert!(!data_source.can_connect_to(&control_target));
431    }
432
433    #[test]
434    fn test_handle_manager_operations() {
435        let mut manager = HandleManager::new("node1".into());
436
437        // Add handles
438        let handle1 = Handle::source("out", HandlePosition::Right);
439        let handle2 = Handle::target("in", HandlePosition::Left);
440
441        manager.add_handle(handle1.clone()).unwrap();
442        manager.add_handle(handle2.clone()).unwrap();
443
444        assert_eq!(manager.handles().len(), 2);
445
446        // Test duplicate prevention
447        assert!(manager.add_handle(handle1.clone()).is_err());
448
449        // Test retrieval
450        assert!(manager.get_handle(&"out".into()).is_some());
451        assert!(manager.get_handle(&"nonexistent".into()).is_none());
452
453        // Test removal
454        let removed = manager.remove_handle(&"out".into()).unwrap();
455        assert_eq!(removed.id.as_str(), "out");
456        assert_eq!(manager.handles().len(), 1);
457    }
458
459    #[test]
460    fn test_handle_manager_find_at_position() {
461        let mut manager = HandleManager::new("node1".into());
462        let handle = Handle::new("right", HandleType::Source, HandlePosition::Right);
463        manager.add_handle(handle).unwrap();
464
465        let node_pos = Position::new(0.0, 0.0);
466        let node_size = Size::new(100.0, 50.0);
467        let handle_size = 10.0;
468
469        // Should find handle at right position (100, 25)
470        let found = manager.handle_at_position(
471            Position::new(100.0, 25.0),
472            node_pos,
473            node_size,
474            handle_size,
475        );
476        assert!(found.is_some());
477        assert_eq!(found.unwrap().id.as_str(), "right");
478
479        // Should not find handle at wrong position
480        let not_found =
481            manager.handle_at_position(Position::new(50.0, 25.0), node_pos, node_size, handle_size);
482        assert!(not_found.is_none());
483    }
484
485    #[test]
486    fn test_handle_type_filtering() {
487        let mut manager = HandleManager::new("node1".into());
488
489        manager
490            .add_handle(Handle::source("out1", HandlePosition::Right))
491            .unwrap();
492        manager
493            .add_handle(Handle::source("out2", HandlePosition::Top))
494            .unwrap();
495        manager
496            .add_handle(Handle::target("in1", HandlePosition::Left))
497            .unwrap();
498
499        let sources: Vec<_> = manager.source_handles().collect();
500        let targets: Vec<_> = manager.target_handles().collect();
501
502        assert_eq!(sources.len(), 2);
503        assert_eq!(targets.len(), 1);
504    }
505}