Skip to main content

flow_rs_leptos/
hooks.rs

1//! Leptos hooks and utilities for flow editors
2
3use leptos::prelude::*;
4use std::rc::Rc;
5use wasm_bindgen::{closure::Closure, JsCast};
6use web_sys::{KeyboardEvent, MouseEvent};
7// use web_sys::{HtmlCanvasElement, WheelEvent}; // Unused imports
8
9// use crate::drag::DragHandler; // Unused import
10use crate::signals::FlowSignalManager;
11// use crate::edge_connection::{ConnectionHandle, ConnectionPreview, EdgeCreator, HandleDetector}; // Unused imports
12use crate::events::{FlowEvent, KeyboardModifiers, MouseButton};
13// use crate::events::NodeEvent; // Unused import
14// use crate::signals::{FlowState, ViewportState}; // Unused imports
15use flow_rs_core::{Graph, Position};
16
17/// NEW: Simplified canvas mouse interactions using FlowSignalManager
18pub fn use_canvas_mouse_simplified(
19    canvas_ref: NodeRef<leptos::html::Canvas>,
20    signal_manager: FlowSignalManager,
21    on_flow_event: Option<Rc<dyn Fn(FlowEvent)>>,
22) {
23    Effect::new(move |_| {
24        if let Some(canvas) = canvas_ref.get_untracked() {
25            let canvas_element = (*canvas).clone();
26            let _canvas_clone = canvas.clone();
27            let signal_manager_clone = signal_manager.clone();
28            let on_flow_event_clone = on_flow_event.clone();
29
30            // Mouse down handler
31            let mouse_down_handler = Closure::wrap(Box::new(move |event: MouseEvent| {
32                let canvas_pos = Position::new(event.offset_x() as f64, event.offset_y() as f64);
33                let viewport_state = signal_manager_clone.viewport_state.get();
34                
35                // Convert canvas position to world position
36                let world_pos = Position::new(
37                    canvas_pos.x / viewport_state.viewport.zoom + viewport_state.viewport.x,
38                    canvas_pos.y / viewport_state.viewport.zoom + viewport_state.viewport.y,
39                );
40
41                // Get current graph from signal manager
42                let graph = signal_manager_clone.get_graph();
43                
44                // Check if we clicked on a node
45                let clicked_node = graph.nodes().find(|node| {
46                    let bounds = node.bounds();
47                    crate::events::utils::position_in_rect(world_pos, &bounds)
48                });
49
50                match clicked_node {
51                    Some(node) => {
52                        // Handle node click
53                        if let Some(handler) = &on_flow_event_clone {
54                            handler(FlowEvent::NodeClick {
55                                node_id: node.id.clone(),
56                                position: world_pos,
57                                button: MouseButton::Left,
58                                modifiers: KeyboardModifiers::default(),
59                            });
60                        }
61                    }
62                    None => {
63                        // Handle canvas click
64                        if let Some(handler) = &on_flow_event_clone {
65                            handler(FlowEvent::CanvasClick {
66                                position: world_pos,
67                                button: MouseButton::Left,
68                                modifiers: KeyboardModifiers::default(),
69                            });
70                        }
71                    }
72                }
73            }) as Box<dyn FnMut(_)>);
74
75            canvas_element
76                .add_event_listener_with_callback(
77                    "mousedown",
78                    mouse_down_handler.as_ref().unchecked_ref(),
79                )
80                .unwrap();
81
82            mouse_down_handler.forget();
83        }
84    });
85}
86
87/// OLD: Original canvas mouse interactions (commented out due to trait bound issues)
88/*
89pub fn use_canvas_mouse<N, E>(
90    canvas_ref: NodeRef<leptos::html::Canvas>,
91    graph: RwSignal<Graph<N, E>>,
92    viewport: RwSignal<ViewportState>,
93    flow_state: RwSignal<FlowState>,
94    on_flow_event: Option<Rc<dyn Fn(FlowEvent)>>,
95    on_node_event: Option<Rc<dyn Fn(NodeEvent<N>)>>,
96) where
97    N: Clone + 'static,
98    E: Clone + Default + 'static,
99{
100    Effect::new(move |_| {
101        if let Some(canvas) = canvas_ref.get_untracked() {
102            let canvas_element = (*canvas).clone();
103            let _canvas_clone = canvas.clone();
104            let graph_clone = graph.clone();
105            let viewport_clone = viewport.clone();
106            let flow_state_clone = flow_state.clone();
107            let on_flow_event_clone = on_flow_event.clone();
108            let on_node_event_clone = on_node_event.clone();
109
110            // Create interaction system components
111            let _drag_handler = DragHandler::new();
112            let _handle_detector = HandleDetector::new();
113            let connection_preview =
114                std::rc::Rc::new(std::cell::RefCell::new(ConnectionPreview::new()));
115            let edge_creator = EdgeCreator::new();
116
117            // Mouse down handler
118            let mouse_down_handler = {
119                let canvas = canvas_clone.clone();
120                let graph = graph_clone.clone();
121                let viewport = viewport_clone.clone();
122                let flow_state = flow_state_clone.clone();
123                let on_flow_event = on_flow_event_clone.clone();
124                let on_node_event = on_node_event_clone.clone();
125                let connection_preview = connection_preview.clone();
126
127                Closure::wrap(Box::new(move |event: MouseEvent| {
128                    event.prevent_default();
129
130                    let button = MouseButton::from_mouse_event(&event);
131                    let modifiers = KeyboardModifiers::from_mouse_event(&event);
132
133                    if let Some(MouseButton::Left) = button {
134                        let canvas_element =
135                            (*canvas).clone().unchecked_into::<HtmlCanvasElement>();
136                        // Use proper DOM rect access with mouse integration
137                        let mut rect_utils = crate::dom_rect::DomRectUtils::new();
138                        let viewport_state = viewport.get_untracked();
139                        let canvas_pos =
140                            crate::mouse_integration::utils::mouse_event_to_canvas_coords(
141                                &event,
142                                &canvas_element,
143                                &mut rect_utils,
144                            )
145                            .unwrap_or_else(|_| {
146                                // Fallback to simple calculation if DOM rect access fails
147                                Position::new(event.client_x() as f64, event.client_y() as f64)
148                            });
149                        let world_pos = crate::events::utils::canvas_to_world(
150                            canvas_pos,
151                            viewport_state.viewport.offset,
152                            viewport_state.viewport.zoom,
153                        );
154
155                        // Check if we clicked on a node
156                        // We need to get the graph value in a reactive context
157                        let graph_value = graph.get_untracked();
158                        let clicked_node = graph_value.nodes().find(|node| {
159                            let bounds = node.bounds();
160                            crate::events::utils::position_in_rect(world_pos, &bounds)
161                        });
162
163                        match clicked_node {
164                            Some(node) => {
165                                // Check if we clicked on a connection handle
166                                let handle_detector = HandleDetector::new();
167                                let detected_handle =
168                                    handle_detector.detect_handle(&node, world_pos);
169
170                                match detected_handle {
171                                    Some(ConnectionHandle::Output) => {
172                                        // Start connection from output handle
173                                        // This initiates a connection preview and sets up connection state
174                                        let node_id = node.id.clone();
175                                        flow_state.update(|state| {
176                                            state.set_connection_mode(true);
177                                            state.set_connection_source(Some(node_id.clone()));
178                                            state.set_connection_start_position(Some(canvas_pos));
179                                        });
180
181                                        connection_preview.borrow_mut().start_connection(
182                                            &graph_value,
183                                            &node_id,
184                                            canvas_pos,
185                                        );
186
187                                        if let Some(handler) = &on_flow_event {
188                                            handler(FlowEvent::ConnectionStart {
189                                                source_node: node_id,
190                                                position: world_pos,
191                                            });
192                                        }
193                                    }
194                                    Some(ConnectionHandle::Input) => {
195                                        // Complete connection to input handle
196                                        // This attempts to create an edge between the source and target nodes
197                                        let flow_state_val = flow_state.get_untracked();
198                                        if let Some(source_node_id) =
199                                            flow_state_val.connection_source()
200                                        {
201                                            // Complete the connection - need to get a fresh copy of the graph
202                        let mut graph_value = graph.get_untracked();
203                        let result = edge_creator.create_edge(
204                            &mut graph_value,
205                            source_node_id,
206                            &node.id,
207                        );
208
209                        if result
210                            == crate::edge_connection::ConnectionResult::Valid
211                        {
212                            // Connection successful
213                            graph.set_untracked(graph_value);
214
215                                                if let Some(handler) = &on_flow_event {
216                                                    handler(FlowEvent::ConnectionComplete {
217                                                        source_node: source_node_id.clone(),
218                                                        target_node: node.id.clone(),
219                                                    });
220                                                }
221                                            }
222                                        }
223
224                                        // End connection mode
225                                        flow_state.update(|state| {
226                                            state.set_connection_mode(false);
227                                            state.set_connection_source(None);
228                                            state.set_connection_start_position(None);
229                                        });
230                                        connection_preview.borrow_mut().end_preview();
231                                    }
232                                    None => {
233                                        // Regular node click
234                                        flow_state.update(|state| {
235                                            if modifiers.shift {
236                                                state.add_node_to_selection(node.id.clone());
237                                            } else {
238                                                state.select_node(node.id.clone());
239                                            }
240                                            state.start_drag(canvas_pos);
241                                        });
242
243                                        if let Some(handler) = &on_node_event {
244                                            handler(NodeEvent::Click {
245                                                node: node.clone(),
246                                                position: world_pos,
247                                                modifiers,
248                                            });
249                                        }
250
251                                        if let Some(handler) = &on_flow_event {
252                                            handler(FlowEvent::DragStart {
253                                                position: world_pos,
254                                                target: crate::events::DragTarget::Node(
255                                                    node.id.clone(),
256                                                ),
257                                            });
258                                        }
259                                    }
260                                }
261                            }
262                            None => {
263                                // Canvas click
264                                flow_state.update(|state| {
265                                    if !modifiers.shift {
266                                        state.clear_selection();
267                                    }
268                                    state.start_drag(canvas_pos);
269                                });
270
271                                if let Some(handler) = &on_flow_event {
272                                    handler(FlowEvent::CanvasClick {
273                                        position: world_pos,
274                                        button: MouseButton::Left,
275                                        modifiers,
276                                    });
277
278                                    handler(FlowEvent::DragStart {
279                                        position: world_pos,
280                                        target: crate::events::DragTarget::Canvas,
281                                    });
282                                }
283                            }
284                        }
285                    }
286                }) as Box<dyn FnMut(_)>)
287            };
288
289            // Mouse move handler
290            let mouse_move_handler = {
291                let canvas = canvas_clone.clone();
292                let graph = graph_clone.clone();
293                let viewport = viewport_clone.clone();
294                let flow_state = flow_state_clone.clone();
295                let on_flow_event = on_flow_event_clone.clone();
296                let connection_preview = connection_preview.clone();
297
298                Closure::wrap(Box::new(move |event: MouseEvent| {
299                    let canvas_element = (*canvas).clone().unchecked_into::<HtmlCanvasElement>();
300                    // Use proper DOM rect access with mouse integration
301                    let mut rect_utils = crate::dom_rect::DomRectUtils::new();
302                    let canvas_pos = crate::mouse_integration::utils::mouse_event_to_canvas_coords(
303                        &event,
304                        &canvas_element,
305                        &mut rect_utils,
306                    )
307                    .unwrap_or_else(|_| {
308                        // Fallback to simple calculation if DOM rect access fails
309                        Position::new(event.client_x() as f64, event.client_y() as f64)
310                    });
311
312                    // Use get_untracked to avoid reactivity issues
313                    let flow_state_val = flow_state.get_untracked();
314
315                    // Check if we're in connection mode and update preview
316                    if flow_state_val.is_connection_mode() {
317                        connection_preview.borrow_mut().update_preview(canvas_pos);
318
319                        if let Some(handler) = &on_flow_event {
320                            let viewport_state = viewport.get_untracked();
321                            let world_pos = crate::events::utils::canvas_to_world(
322                                canvas_pos,
323                                viewport_state.viewport.offset,
324                                viewport_state.viewport.zoom,
325                            );
326                            handler(FlowEvent::ConnectionUpdate {
327                                position: world_pos,
328                            });
329                        }
330                    } else if flow_state_val.is_dragging {
331                        // Update drag state first
332                        flow_state.update(|state| {
333                            state.update_drag(canvas_pos);
334                        });
335
336                        // Get viewport state without tracking
337                        let viewport_state = viewport.get_untracked();
338                        let world_pos = crate::events::utils::canvas_to_world(
339                            canvas_pos,
340                            viewport_state.viewport.offset,
341                            viewport_state.viewport.zoom,
342                        );
343
344                        // Get updated flow state to check for drag delta
345                        let updated_flow_state = flow_state.get_untracked();
346                        if let Some(canvas_delta) = updated_flow_state.drag_delta() {
347                            // Handle different drag targets
348                            let selected_nodes = &updated_flow_state.selected_nodes;
349                            if !selected_nodes.is_empty() {
350                                // Dragging nodes - convert canvas delta to world delta
351                                let world_delta = Position::new(
352                                    canvas_delta.x / viewport_state.viewport.zoom,
353                                    canvas_delta.y / viewport_state.viewport.zoom,
354                                );
355
356                                // Clone the drag handler to avoid borrowing issues
357                                let drag_handler = DragHandler::new();
358                                // Get flow state before entering the graph update closure
359                                let current_flow_state = flow_state.get_untracked();
360                        graph.update(|graph_mut| {
361                            drag_handler.apply_drag_to_nodes(
362                                graph_mut,
363                                &current_flow_state,
364                                world_delta,
365                            );
366                        });
367                            } else {
368                                // Dragging canvas (panning)
369                                viewport.update(|vp| {
370                                    let world_delta = Position::new(
371                                        -canvas_delta.x / vp.viewport.zoom,
372                                        -canvas_delta.y / vp.viewport.zoom,
373                                    );
374                                    vp.pan(world_delta);
375                                });
376
377                                // Reset drag start to current position for smooth panning
378                                flow_state.update(|state| {
379                                    state.drag_start = Some(canvas_pos);
380                                });
381                            }
382
383                            if let Some(handler) = &on_flow_event {
384                                handler(FlowEvent::DragUpdate {
385                                    position: world_pos,
386                                    delta: canvas_delta,
387                                });
388                            }
389                        }
390                    }
391                }) as Box<dyn FnMut(_)>)
392            };
393
394            // Mouse up handler
395            let mouse_up_handler = {
396                let viewport = viewport_clone.clone();
397                let flow_state = flow_state_clone.clone();
398                let on_flow_event = on_flow_event_clone.clone();
399                let connection_preview = connection_preview.clone();
400
401                Closure::wrap(Box::new(move |event: MouseEvent| {
402                    let flow_state_val = flow_state.get_untracked();
403
404                    // Check if we're in connection mode and cancel it
405                    if flow_state_val.is_connection_mode() {
406                        flow_state.update(|state| {
407                            state.set_connection_mode(false);
408                            state.set_connection_source(None);
409                            state.set_connection_start_position(None);
410                        });
411                        connection_preview.borrow_mut().end_preview();
412
413                        if let Some(handler) = &on_flow_event {
414                            handler(FlowEvent::ConnectionCancel);
415                        }
416                    } else if flow_state_val.is_dragging {
417                        let _canvas_element =
418                            (*canvas).clone().unchecked_into::<HtmlCanvasElement>();
419                        // TODO: Implement proper DOM rect access
420                        // For now, use placeholder values
421                        let canvas_rect = (0.0, 0.0, 800.0, 600.0); // (left, top, width, height)
422                        let canvas_pos = Position::new(
423                            event.client_x() as f64 - canvas_rect.0,
424                            event.client_y() as f64 - canvas_rect.1,
425                        );
426
427                        let viewport_state = viewport.get_untracked();
428                        let world_pos = crate::events::utils::canvas_to_world(
429                            canvas_pos,
430                            viewport_state.viewport.offset,
431                            viewport_state.viewport.zoom,
432                        );
433
434                        // Determine drag target
435                        let drag_target = if !flow_state_val.selected_nodes.is_empty() {
436                            if flow_state_val.selected_nodes.len() == 1 {
437                                crate::events::DragTarget::Node(
438                                    flow_state_val.selected_nodes[0].clone(),
439                                )
440                            } else {
441                                crate::events::DragTarget::Nodes(
442                                    flow_state_val.selected_nodes.clone(),
443                                )
444                            }
445                        } else {
446                            crate::events::DragTarget::Canvas
447                        };
448
449                        flow_state.update(|state| {
450                            state.end_drag();
451                        });
452
453                        if let Some(handler) = &on_flow_event {
454                            handler(FlowEvent::DragEnd {
455                                position: world_pos,
456                                target: drag_target,
457                            });
458                        }
459                    }
460                }) as Box<dyn FnMut(_)>)
461            };
462
463            // Add event listeners using a fresh reference
464            let _ = canvas_element.add_event_listener_with_callback(
465                "mousedown",
466                mouse_down_handler.as_ref().unchecked_ref(),
467            );
468            let _ = canvas_element.add_event_listener_with_callback(
469                "mousemove",
470                mouse_move_handler.as_ref().unchecked_ref(),
471            );
472            let _ = canvas_element.add_event_listener_with_callback(
473                "mouseup",
474                mouse_up_handler.as_ref().unchecked_ref(),
475            );
476
477            // Store closures to prevent garbage collection
478            mouse_down_handler.forget();
479            mouse_move_handler.forget();
480            mouse_up_handler.forget();
481        }
482    });
483}
484
485/// Hook for managing canvas wheel events (zoom)
486pub fn use_canvas_wheel(
487    canvas_ref: NodeRef<leptos::html::Canvas>,
488    viewport: RwSignal<ViewportState>,
489    on_flow_event: Option<Rc<dyn Fn(FlowEvent)>>,
490) {
491    Effect::new(move |_| {
492        if let Some(canvas) = canvas_ref.get_untracked() {
493            let canvas_element = (*canvas).clone();
494            let viewport_clone = viewport.clone();
495            let on_flow_event_clone = on_flow_event.clone();
496
497            let wheel_handler = Closure::wrap(Box::new(move |event: WheelEvent| {
498                event.prevent_default();
499
500                let _canvas_element = (*canvas).clone().unchecked_into::<HtmlCanvasElement>();
501                // TODO: Implement proper DOM rect access
502                // For now, use placeholder values
503                let canvas_rect = (0.0, 0.0, 800.0, 600.0); // (left, top, width, height)
504                let canvas_pos = Position::new(
505                    event.client_x() as f64 - canvas_rect.0,
506                    event.client_y() as f64 - canvas_rect.1,
507                );
508
509                let delta_y = event.delta_y();
510                let zoom_factor = if delta_y < 0.0 { 1.1 } else { 1.0 / 1.1 };
511
512                let _old_viewport = viewport_clone.get_untracked();
513
514                viewport_clone.update(|vp| {
515                    let world_pos = crate::events::utils::canvas_to_world(
516                        canvas_pos,
517                        vp.viewport.offset,
518                        vp.viewport.zoom,
519                    );
520                    vp.zoom_at(zoom_factor, world_pos);
521                });
522
523                let new_viewport = viewport_clone.get_untracked();
524                if let Some(handler) = &on_flow_event_clone {
525                    handler(FlowEvent::ViewportChanged {
526                        offset: new_viewport.viewport.offset,
527                        zoom: new_viewport.viewport.zoom,
528                    });
529                }
530            }) as Box<dyn FnMut(_)>);
531
532            let _ = canvas_element
533                .add_event_listener_with_callback("wheel", wheel_handler.as_ref().unchecked_ref());
534
535            wheel_handler.forget();
536        }
537    });
538}
539*/
540
541/// Hook for managing keyboard shortcuts
542pub fn use_keyboard_shortcuts(on_flow_event: Option<Rc<dyn Fn(FlowEvent)>>) {
543    Effect::new(move |_| {
544        let on_flow_event_clone = on_flow_event.clone();
545
546        let keydown_handler = Closure::wrap(Box::new(move |event: KeyboardEvent| {
547            let modifiers = KeyboardModifiers::from_keyboard_event(&event);
548            let key = event.key();
549
550            match key.as_str() {
551                "Delete" | "Backspace" => {
552                    if let Some(_handler) = &on_flow_event_clone {
553                        // Would trigger delete selected items
554                    }
555                }
556                "a" | "A" if modifiers.ctrl_only() => {
557                    event.prevent_default();
558                    if let Some(_handler) = &on_flow_event_clone {
559                        // Would trigger select all
560                    }
561                }
562                "z" | "Z" if modifiers.ctrl_only() => {
563                    event.prevent_default();
564                    if let Some(_handler) = &on_flow_event_clone {
565                        // Would trigger undo
566                    }
567                }
568                "y" | "Y" if modifiers.ctrl_only() => {
569                    event.prevent_default();
570                    if let Some(_handler) = &on_flow_event_clone {
571                        // Would trigger redo
572                    }
573                }
574                _ => {}
575            }
576        }) as Box<dyn FnMut(_)>);
577
578        if let Some(window) = web_sys::window() {
579            let _ = window.add_event_listener_with_callback(
580                "keydown",
581                keydown_handler.as_ref().unchecked_ref(),
582            );
583            keydown_handler.forget();
584        }
585    });
586}
587
588/// Hook for managing graph operations with undo/redo
589// Temporary test with concrete types
590pub fn use_graph_operations_concrete(graph: RwSignal<Graph<(), ()>>) -> GraphOperationsHandle<(), ()> {
591    let undo_stack = RwSignal::new(Vec::<Graph<(), ()>>::new());
592    let redo_stack = RwSignal::new(Vec::<Graph<(), ()>>::new());
593
594    GraphOperationsHandle {
595        graph,
596        undo_stack,
597        redo_stack,
598    }
599}
600
601/// Handle for graph operations with undo/redo support
602pub struct GraphOperationsHandle<N, E>
603where
604    N: Clone + Send + Sync + 'static + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash + std::fmt::Debug + Default,
605    E: Clone + Send + Sync + 'static + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash + std::fmt::Debug + Default,
606{
607    graph: RwSignal<Graph<N, E>>,
608    undo_stack: RwSignal<Vec<Graph<N, E>>>,
609    redo_stack: RwSignal<Vec<Graph<N, E>>>,
610}
611
612impl<N, E> GraphOperationsHandle<N, E>
613where
614    N: Clone + Send + Sync + 'static + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash + std::fmt::Debug + Default,
615    E: Clone + Send + Sync + 'static + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash + std::fmt::Debug + Default,
616{
617    /// Execute an operation with undo support
618    pub fn execute<F>(&self, operation: F)
619    where
620        F: FnOnce(&mut Graph<N, E>),
621    {
622        // Save current state to undo stack
623        self.undo_stack.update(|stack| {
624            stack.push(self.graph.get());
625            // Limit undo stack size
626            if stack.len() > 100 {
627                stack.remove(0);
628            }
629        });
630
631        // Clear redo stack
632        self.redo_stack.update(|stack| stack.clear());
633
634        // Execute operation
635        self.graph.update(operation);
636    }
637
638    /// Undo last operation
639    pub fn undo(&self) -> bool {
640        let mut success = false;
641
642        self.undo_stack.update(|undo_stack| {
643            if let Some(previous_state) = undo_stack.pop() {
644                // Save current state to redo stack
645                self.redo_stack.update(|redo_stack| {
646                    redo_stack.push(self.graph.get());
647                });
648
649                // Restore previous state
650                self.graph.set(previous_state);
651                success = true;
652            }
653        });
654
655        success
656    }
657
658    /// Redo last undone operation
659    pub fn redo(&self) -> bool {
660        let mut success = false;
661
662        self.redo_stack.update(|redo_stack| {
663            if let Some(next_state) = redo_stack.pop() {
664                // Save current state to undo stack
665                self.undo_stack.update(|undo_stack| {
666                    undo_stack.push(self.graph.get());
667                });
668
669                // Restore next state
670                self.graph.set(next_state);
671                success = true;
672            }
673        });
674
675        success
676    }
677
678    /// Check if undo is available
679    pub fn can_undo(&self) -> bool {
680        !self.undo_stack.get().is_empty()
681    }
682
683    /// Check if redo is available
684    pub fn can_redo(&self) -> bool {
685        !self.redo_stack.get().is_empty()
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use flow_rs_core::prelude::{NodeBuilder, Size};
693
694    #[test]
695    fn test_graph_operations_handle() {
696        // Create a simple graph without signals for testing
697        let mut graph = Graph::<(), ()>::new();
698
699        // Add a node directly
700        let node = NodeBuilder::new("test")
701            .position(0.0, 0.0)
702            .size(100.0, 50.0)
703            .build();
704
705        let result = graph.add_node(node);
706        assert!(result.is_ok());
707
708        // Verify the node was added
709        assert_eq!(graph.node_count(), 1);
710
711        // Test basic graph operations
712        let nodes: Vec<_> = graph.nodes().collect();
713        assert_eq!(nodes.len(), 1);
714        assert_eq!(nodes[0].id.to_string(), "test");
715    }
716}