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 ¤t_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}