1use std::collections::HashMap;
12use std::rc::Rc;
13use std::sync::atomic::{AtomicUsize, Ordering};
14
15use dioxus::html::geometry::WheelDelta;
16use dioxus::html::input_data::MouseButton;
17use dioxus::prelude::*;
18
19use crate::edge::{EdgeItem, EdgeMarkers, EdgeViewCtx, HANDLE_RIM};
20use crate::node::{NodeItem, NodeViewCtx};
21use crate::path::connection_path;
22use crate::state::{
23 orient_connection, ConnectionState, DragState, FlowApi, FlowConfig, FlowCore, FlowHandle,
24 Interaction,
25};
26use crate::types::{
27 AnchorMode, ConnectEnd, Connection, DeleteRequest, Edge, HandleKey, HandleKind, Id, Node,
28 NodeGeom, Point, Rect, Viewport,
29};
30
31pub static STYLE: &str = include_str!("style.css");
34
35static NEXT_IID: AtomicUsize = AtomicUsize::new(0);
36
37fn client_point(coords: dioxus::html::geometry::ClientPoint) -> Point {
38 Point::new(coords.x, coords.y)
39}
40
41fn wheel_pixels(delta: WheelDelta, page: f64) -> Point {
45 match delta {
46 WheelDelta::Pixels(v) => Point::new(v.x, v.y),
47 WheelDelta::Lines(v) => Point::new(v.x * 16.0, v.y * 16.0),
48 WheelDelta::Pages(v) => {
49 let scale = page.max(240.0);
50 Point::new(v.x * scale, v.y * scale)
51 }
52 }
53}
54
55const PINCH_ZOOM_SENSITIVITY: f64 = 0.0025;
58
59#[allow(clippy::too_many_arguments)]
73#[component]
74pub fn Canvas(
75 #[props(default = 0.25)] min_zoom: f64,
76 #[props(default = 4.0)] max_zoom: f64,
77 #[props(default = true)]
79 pan_on_drag: bool,
80 #[props(default = true)]
82 zoom_on_scroll: bool,
83 #[props(default = false)]
87 pan_on_scroll: bool,
88 #[props(default = true)]
90 nodes_draggable: bool,
91 #[props(default = 0.0)]
94 drag_threshold: f64,
95 #[props(default = 28.0)]
97 connection_radius: f64,
98 #[props(default = 0.12)] fit_view_padding: f64,
99 id: Option<String>,
102 #[props(default = "Node graph".to_string())]
104 aria_label: String,
105 class: Option<String>,
107 on_pane_press: Option<Callback<Event<PointerData>>>,
113 edges: Option<Signal<Vec<Edge>>>,
116 geoms: Option<Memo<Vec<NodeGeom>>>,
119 deselect_nodes: Option<Callback<()>>,
122 on_connect: Option<EventHandler<Connection>>,
125 on_connect_start: Option<EventHandler<HandleKey>>,
127 on_connect_end: Option<EventHandler<ConnectEnd>>,
131 is_valid_connection: Option<Callback<Connection, bool>>,
134 on_node_drag_start: Option<EventHandler<Vec<Id>>>,
138 on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
141 on_pane_click: Option<EventHandler<Point>>,
144 on_pane_double_click: Option<EventHandler<Point>>,
148 on_canvas_key_down: Option<Callback<Event<KeyboardData>>>,
151 on_drag_move: Option<Callback<Point>>,
154 world: Option<Element>,
156 children: Element,
164) -> Element {
165 let viewport = use_signal(Viewport::default);
166 let container = use_signal(|| Rect::ZERO);
167 let interaction = use_signal(Interaction::default);
168 let connection = use_signal(|| None::<ConnectionState>);
169 let handles = use_signal(HashMap::new);
170 let mut config = use_signal(FlowConfig::default);
171 let drag = use_signal(DragState::default);
172 let epoch = use_signal(|| 0u64);
173 let pending_sizes = use_signal(Vec::new);
174 let size_flush_queued = use_signal(|| false);
175 let pending_handles = use_signal(Vec::new);
176 let handle_flush_queued = use_signal(|| false);
177 let snap_key = use_memo(move || {
178 connection
179 .read()
180 .as_ref()
181 .and_then(|c| c.snap.as_ref())
182 .map(|s| s.key.clone())
183 });
184 let connect_from = use_memo(move || connection.read().as_ref().map(|c| c.from.clone()));
185 let overlay_insets = use_signal(HashMap::new);
186 let own_edges = use_signal(Vec::new);
187 let edges = edges.unwrap_or(own_edges);
188 let empty_geoms = use_memo(Vec::new);
189 let geoms = geoms.unwrap_or(empty_geoms);
190 let deselect_nodes = deselect_nodes.unwrap_or_else(|| use_callback(move |_: ()| {}));
191
192 let core = use_hook(|| FlowCore {
193 iid: NEXT_IID.fetch_add(1, Ordering::Relaxed),
194 viewport,
195 container,
196 interaction,
197 connection,
198 handles,
199 edges,
200 geoms,
201 config,
202 drag,
203 epoch,
204 snap_key,
205 connect_from,
206 deselect_nodes,
207 overlay_insets,
208 pending_sizes,
209 size_flush_queued,
210 pending_handles,
211 handle_flush_queued,
212 on_connect_start,
213 valid_connection: is_valid_connection,
214 });
215 use_context_provider(|| core);
216
217 let cfg = FlowConfig {
219 min_zoom,
220 max_zoom,
221 pan_on_drag,
222 zoom_on_scroll,
223 pan_on_scroll,
224 nodes_draggable,
225 drag_threshold,
226 connection_radius,
227 fit_view_padding,
228 };
229 if *config.peek() != cfg {
230 config.set(cfg);
231 }
232
233 let mounted: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
235 let refresh_rect = use_callback(move |_: ()| {
236 let element = mounted.peek().clone();
237 let mut container = container;
238 if let Some(element) = element {
239 spawn(async move {
240 if let Ok(rect) = element.get_client_rect().await {
241 let rect = Rect::new(rect.origin.x, rect.origin.y, rect.width(), rect.height());
242 if *container.peek() != rect {
243 container.set(rect);
244 }
245 }
246 });
247 }
248 });
249
250 let end_gesture = use_callback(move |_: ()| {
253 let mut interaction = interaction;
254 let mut connection = connection;
255 if *interaction.peek() != Interaction::None {
256 interaction.set(Interaction::None);
257 }
258 if connection.peek().is_some() {
259 connection.set(None);
260 }
261 let mut drag = drag;
262 let mut state = drag.write();
263 state.pointer_id = None;
264 state.suppress_click = false;
265 });
266
267 let on_pointer_down = move |evt: Event<PointerData>| {
268 refresh_rect.call(());
269 core.cancel_animations();
270 if *interaction.peek() != Interaction::None {
273 return;
274 }
275 if evt.trigger_button() != Some(MouseButton::Primary) {
276 return;
277 }
278 if let Some(handler) = &on_pane_press {
281 handler.call(evt.clone());
282 if *interaction.peek() != Interaction::None {
283 return;
284 }
285 }
286 let client = client_point(evt.client_coordinates());
287 {
288 let mut drag = drag;
289 let mut state = drag.write();
290 state.pointer_id = Some(evt.pointer_id());
291 state.origin_client = client;
292 state.last_client = client;
293 state.moved = false;
294 state.suppress_click = false;
295 state.grabs.clear();
296 }
297 let mut interaction = interaction;
298 if pan_on_drag {
299 interaction.set(Interaction::Pan);
300 } else {
301 interaction.set(Interaction::PanePressed);
302 }
303 };
304
305 let on_pointer_move = move |evt: Event<PointerData>| {
306 let current = *interaction.peek();
307 if current == Interaction::None {
308 return;
309 }
310 if drag
313 .peek()
314 .pointer_id
315 .is_some_and(|id| id != evt.pointer_id())
316 {
317 return;
318 }
319 if evt.held_buttons().is_empty() {
322 end_gesture.call(());
323 return;
324 }
325 let client = client_point(evt.client_coordinates());
326 match current {
327 Interaction::Pan => {
328 let delta = {
329 let mut drag = drag;
330 let mut state = drag.write();
331 let delta = client - state.last_client;
332 state.last_client = client;
333 state.moved = true;
334 delta
335 };
336 let mut viewport = viewport;
337 let vp = *viewport.peek();
338 viewport.set(vp.panned(delta));
339 }
340 Interaction::DragNode => {
341 let began = {
346 let mut drag = drag;
347 let mut state = drag.write();
348 state.last_client = client;
349 let travelled = state.origin_client.distance(client);
350 let passed = state.moved || travelled >= config.peek().drag_threshold;
351 let began = passed && !state.moved;
352 if passed {
353 state.moved = true;
354 }
355 if !passed {
356 return;
357 }
358 began
359 };
360 if began {
361 if let Some(handler) = &on_node_drag_start {
362 let ids: Vec<Id> =
363 drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
364 handler.call(ids);
365 }
366 }
367 let flow = core.client_to_flow(client);
368 if let Some(handler) = &on_drag_move {
369 handler.call(flow);
370 }
371 }
372 Interaction::Connect => {
373 let flow = core.client_to_flow(client);
374 let mut connection = connection;
375 let from = connection.peek().as_ref().map(|c| c.from.clone());
376 if let Some(from) = from {
377 let snap = core.find_snap(&from, flow);
378 connection.set(Some(ConnectionState {
379 from,
380 cursor: flow,
381 snap,
382 }));
383 }
384 }
385 _ => {}
386 }
387 };
388
389 let on_pointer_up = move |evt: Event<PointerData>| {
390 if drag
391 .peek()
392 .pointer_id
393 .is_some_and(|id| id != evt.pointer_id())
394 {
395 return;
396 }
397 let current = *interaction.peek();
398 match current {
399 Interaction::Pan | Interaction::PanePressed => {
401 let state = drag.peek().clone();
402 let is_click =
403 (current == Interaction::PanePressed || !state.moved) && !state.suppress_click;
404 if is_click {
405 let client = client_point(evt.client_coordinates());
406 let flow = core.client_to_flow(client);
407 if !evt.modifiers().shift() {
408 deselect_nodes.call(());
409 deselect_edges(edges);
410 }
411 if let Some(handler) = &on_pane_click {
412 handler.call(flow);
413 }
414 }
415 }
416 Interaction::Connect => {
417 let done = connection.peek().clone();
418 if let Some(done) = done {
419 let completed = done
420 .snap
421 .as_ref()
422 .map(|snap| orient_connection(&done.from, &snap.key));
423 if let Some(conn) = completed.clone() {
424 match &on_connect {
425 Some(handler) => handler.call(conn),
426 None => add_edge_for_connection(edges, conn),
427 }
428 }
429 if let Some(handler) = &on_connect_end {
433 let client = client_point(evt.client_coordinates());
434 handler.call(ConnectEnd {
435 point: core.client_to_flow(client),
436 connection: completed,
437 });
438 }
439 }
440 }
441 Interaction::DragNode if drag.peek().moved => {
442 if let Some(handler) = &on_node_drag_stop {
443 let ids: Vec<Id> = drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
444 handler.call(ids);
445 }
446 }
447 _ => {}
448 }
449 end_gesture.call(());
450 };
451
452 let on_wheel = move |evt: Event<WheelData>| {
453 let config = *config.peek();
454 if !config.pan_on_scroll && !config.zoom_on_scroll {
455 return;
456 }
457 evt.prevent_default();
458 core.cancel_animations();
459 let client = client_point(evt.client_coordinates());
460 let page = container.peek().height;
461 let delta = wheel_pixels(evt.delta(), page);
462 let modifiers = evt.modifiers();
463 if config.pan_on_scroll {
466 if modifiers.ctrl() || modifiers.meta() {
467 if delta.y != 0.0 {
468 let factor = (-delta.y * PINCH_ZOOM_SENSITIVITY).exp();
469 core.zoom_by(factor, Some(client), 0);
470 }
471 return;
472 }
473 let mut viewport = viewport;
474 let vp = *viewport.peek();
475 let by = if modifiers.shift() && delta.x == 0.0 {
478 Point::new(-delta.y, 0.0)
479 } else {
480 Point::new(-delta.x, -delta.y)
481 };
482 viewport.set(vp.panned(by));
483 return;
484 }
485 if delta.y == 0.0 {
486 return;
487 }
488 let factor = (-delta.y * 0.0022).exp().clamp(0.5, 2.0);
489 core.zoom_by(factor, Some(client), 0);
490 };
491
492 let on_key_down = move |evt: Event<KeyboardData>| {
493 if evt.key() == Key::Escape {
494 end_gesture.call(());
495 }
496 if let Some(handler) = &on_canvas_key_down {
497 handler.call(evt);
498 }
499 };
500
501 let gesture = *interaction.read();
504 let root_class = format!(
505 "dioxus-flow{}{}",
506 match gesture {
507 Interaction::Pan => " df-panning",
508 Interaction::Connect => " df-connecting",
509 _ => "",
510 },
511 class
512 .as_deref()
513 .map(|c| format!(" {c}"))
514 .unwrap_or_default()
515 );
516
517 rsx! {
518 document::Style { {STYLE} }
519 div {
520 id,
521 class: root_class,
522 tabindex: "0",
523 role: "application",
524 aria_label,
525 onmounted: move |evt| {
526 let mut mounted = mounted;
527 mounted.set(Some(evt.data()));
528 refresh_rect.call(());
529 },
530 onresize: move |_| refresh_rect.call(()),
531 onpointerdown: on_pointer_down,
532 onpointermove: on_pointer_move,
533 onpointerup: on_pointer_up,
534 onpointercancel: move |evt: Event<PointerData>| {
535 let owner = drag.peek().pointer_id;
536 if owner.is_none() || owner == Some(evt.pointer_id()) {
537 end_gesture.call(());
538 }
539 },
540 onwheel: on_wheel,
541 ondoubleclick: move |evt: Event<MouseData>| {
542 if let Some(handler) = &on_pane_double_click {
543 let client = client_point(evt.client_coordinates());
544 handler.call(core.client_to_flow(client));
545 }
546 },
547 onkeydown: on_key_down,
548 ViewportPane { {world} }
549 {children}
550 }
551 }
552}
553
554#[component]
575pub fn Flow<T: Clone + PartialEq + 'static>(
576 nodes: Signal<Vec<Node<T>>>,
578 edges: Signal<Vec<Edge>>,
580 #[props(default)]
584 anchor: AnchorMode,
585 #[props(default = 0.25)] min_zoom: f64,
586 #[props(default = 4.0)] max_zoom: f64,
587 #[props(default = true)]
589 pan_on_drag: bool,
590 #[props(default = true)]
592 zoom_on_scroll: bool,
593 #[props(default = false)]
595 pan_on_scroll: bool,
596 #[props(default = true)]
598 nodes_draggable: bool,
599 #[props(default = 0.0)]
602 drag_threshold: f64,
603 #[props(default = 28.0)]
605 connection_radius: f64,
606 #[props(default = false)]
608 fit_view: bool,
609 #[props(default = 0.12)] fit_view_padding: f64,
610 #[props(default = true)]
612 delete_key: bool,
613 id: Option<String>,
615 class: Option<String>,
617 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
620 edge_view: Option<Callback<EdgeViewCtx, Element>>,
622 on_connect: Option<EventHandler<Connection>>,
625 on_connect_start: Option<EventHandler<HandleKey>>,
627 on_connect_end: Option<EventHandler<ConnectEnd>>,
631 is_valid_connection: Option<Callback<Connection, bool>>,
634 on_node_drag_start: Option<EventHandler<Vec<Id>>>,
638 on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
641 on_delete: Option<EventHandler<DeleteRequest>>,
648 on_node_click: Option<EventHandler<Id>>,
649 on_edge_click: Option<EventHandler<Id>>,
650 on_pane_click: Option<EventHandler<Point>>,
652 on_pane_double_click: Option<EventHandler<Point>>,
654 handle: Option<FlowHandle<T>>,
657 children: Element,
660) -> Element {
661 let geoms = use_memo(move || {
662 nodes
663 .read()
664 .iter()
665 .map(|node| NodeGeom {
666 id: node.id.clone(),
667 rect: node.rect(),
668 selected: node.selected,
669 source_side: node.source_side,
670 target_side: node.target_side,
671 measured: node.size.is_some() || node.measured.is_some(),
672 })
673 .collect::<Vec<_>>()
674 });
675 let deselect_nodes = use_callback(move |_: ()| {
676 if nodes.peek().iter().any(|n| n.selected) {
677 nodes.clone().with_mut(|nodes| {
678 for node in nodes.iter_mut() {
679 node.selected = false;
680 }
681 });
682 }
683 });
684
685 let attach_core: Signal<Option<FlowCore>> = use_signal(|| None);
687
688 use_effect(move || {
690 let Some(core) = *attach_core.read() else {
691 return;
692 };
693 if let Some(handle) = handle {
694 let mut inner = handle.inner;
695 if inner.peek().is_none() {
696 inner.set(Some(FlowApi { core, nodes }));
697 }
698 }
699 });
700
701 let mut did_initial_fit = use_signal(|| false);
703 use_effect(move || {
704 let Some(core) = *attach_core.read() else {
705 return;
706 };
707 let container_ready = core.container.read().width > 0.0;
708 let geoms = geoms.read();
709 let nodes_ready = !geoms.is_empty() && geoms.iter().all(|g| g.measured);
710 if fit_view && !*did_initial_fit.peek() && container_ready && nodes_ready {
711 did_initial_fit.set(true);
712 core.fit_view(0);
713 }
714 });
715
716 let on_drag_move = use_callback(move |flow: Point| {
717 let Some(core) = *attach_core.peek() else {
718 return;
719 };
720 let grabs = core.drag.peek().grabs.clone();
721 let mut nodes = nodes;
722 nodes.with_mut(|nodes| {
723 for (id, grab) in &grabs {
724 if let Some(node) = nodes.iter_mut().find(|n| &n.id == id) {
725 node.position = flow - *grab;
726 }
727 }
728 });
729 });
730
731 let on_canvas_key_down = use_callback(move |evt: Event<KeyboardData>| match evt.key() {
732 Key::Delete | Key::Backspace if delete_key => {
733 let Some(core) = *attach_core.peek() else {
734 return;
735 };
736 let request = delete_request(nodes, core.edges);
737 if request.nodes.is_empty() && request.edges.is_empty() {
738 return;
739 }
740 match &on_delete {
741 Some(handler) => handler.call(request),
742 None => delete_selected(nodes, core.edges),
743 }
744 }
745 _ => {}
746 });
747
748 rsx! {
749 Canvas {
750 min_zoom,
751 max_zoom,
752 pan_on_drag,
753 zoom_on_scroll,
754 pan_on_scroll,
755 nodes_draggable,
756 drag_threshold,
757 connection_radius,
758 fit_view_padding,
759 id,
760 class,
761 edges,
762 geoms,
763 deselect_nodes,
764 on_connect,
765 on_connect_start,
766 on_connect_end,
767 is_valid_connection,
768 on_node_drag_start,
769 on_node_drag_stop,
770 on_pane_click,
771 on_pane_double_click,
772 on_canvas_key_down,
773 on_drag_move,
774 world: rsx! {
775 CoreProbe { attach_core }
776 match anchor {
777 AnchorMode::Handles => rsx! {
778 EdgesLayer { edge_view, on_edge_click }
779 NodesLayer { nodes, node_view, on_node_click }
780 },
781 AnchorMode::Seats => rsx! {
782 SeatGraphLayers {
783 nodes,
784 node_view,
785 on_node_click,
786 edge_view,
787 on_edge_click,
788 }
789 },
790 }
791 ConnectionLine {}
792 },
793 {children}
794 }
795 }
796}
797
798#[component]
804fn SeatGraphLayers<T: Clone + PartialEq + 'static>(
805 nodes: Signal<Vec<Node<T>>>,
806 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
807 on_node_click: Option<EventHandler<Id>>,
808 edge_view: Option<Callback<EdgeViewCtx, Element>>,
809 on_edge_click: Option<EventHandler<Id>>,
810) -> Element {
811 let core = use_context::<FlowCore>();
812 let anchors = use_memo(move || {
817 let geoms = core.geoms.read();
818 let frames: std::collections::BTreeMap<Id, Rect> = geoms
819 .iter()
820 .map(|geom| (geom.id.clone(), geom.rect))
821 .collect();
822 let links: Vec<crate::ports::Link> = core
823 .edges
824 .read()
825 .iter()
826 .map(|edge| crate::ports::Link {
827 id: edge.id.clone(),
828 start: crate::ports::Terminal::Node(edge.source.clone()),
829 end: crate::ports::Terminal::Node(edge.target.clone()),
830 start_seat: edge.source_seat,
831 end_seat: edge.target_seat,
832 })
833 .collect();
834 crate::ports::solve_ports(&frames, &links)
835 });
836
837 let adapted_edge_view = edge_view.map(|view| {
841 Callback::new(move |ctx: crate::edge::SeatEdgeViewCtx| {
842 view.call(EdgeViewCtx {
843 edge: ctx.edge.clone(),
844 source: ctx.anchors.start.point(),
845 source_side: ctx.anchors.start.side(),
846 target: ctx.anchors.end.point(),
847 target_side: ctx.anchors.end.side(),
848 path: crate::path::EdgePath {
849 d: ctx.geometry.path.clone(),
850 label: ctx.geometry.label,
851 },
852 marker_end: None,
854 })
855 })
856 });
857
858 let edges = core.edges;
859 let solved = anchors.read();
860 rsx! {
861 crate::edge::SeatEdges {
862 edges,
863 anchors,
864 edge_view: adapted_edge_view,
865 on_edge_click,
866 }
867 crate::edge::SeatEdgeLabels { edges, anchors }
868 NodesLayer { nodes, node_view, on_node_click }
869 svg { class: "df-edges df-ports", "aria-hidden": "true",
871 for edge in edges.read().iter() {
872 if let Some(pair) = solved.get(&edge.id) {
873 g {
874 key: "{edge.id}",
875 class: if edge.selected { "df-selected" },
876 circle {
877 class: "df-port",
878 cx: pair.start.x,
879 cy: pair.start.y,
880 r: crate::ports::PORT_RADIUS,
881 }
882 circle {
883 class: "df-port",
884 cx: pair.end.x,
885 cy: pair.end.y,
886 r: crate::ports::PORT_RADIUS,
887 }
888 }
889 }
890 }
891 }
892 }
893}
894
895#[component]
898fn CoreProbe(attach_core: Signal<Option<FlowCore>>) -> Element {
899 let core = use_context::<FlowCore>();
900 let mut attach_core = attach_core;
901 if attach_core.peek().is_none() {
902 attach_core.set(Some(core));
903 }
904 rsx! {}
905}
906
907#[component]
910fn ViewportPane(children: Element) -> Element {
911 let core = use_context::<FlowCore>();
912 let vp = *core.viewport.read();
913 rsx! {
914 div {
915 class: "df-viewport",
916 style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
917 {children}
918 }
919 }
920}
921
922#[component]
926pub fn WorldLayer(class: Option<String>, children: Element) -> Element {
927 let core = use_context::<FlowCore>();
928 let vp = *core.viewport.read();
929 let class = format!(
930 "df-world-layer{}",
931 class
932 .as_deref()
933 .map(|c| format!(" {c}"))
934 .unwrap_or_default()
935 );
936 rsx! {
937 div {
938 class,
939 style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
940 {children}
941 }
942 }
943}
944
945#[component]
946fn NodesLayer<T: Clone + PartialEq + 'static>(
947 nodes: Signal<Vec<Node<T>>>,
948 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
949 on_node_click: Option<EventHandler<Id>>,
950) -> Element {
951 rsx! {
952 div { class: "df-nodes",
953 for node in nodes.read().iter() {
954 NodeItem::<T> {
955 key: "{node.id}",
956 nodes,
957 node: node.clone(),
958 node_view,
959 on_node_click,
960 }
961 }
962 }
963 }
964}
965
966#[component]
967fn EdgesLayer(
968 edge_view: Option<Callback<EdgeViewCtx, Element>>,
969 on_edge_click: Option<EventHandler<Id>>,
970) -> Element {
971 let core = use_context::<FlowCore>();
972 let edges = core.edges.read();
973 let geoms = core.geoms.read();
974 let handles = core.handles.read();
975 let geom_by_id: HashMap<&str, &NodeGeom> =
976 geoms.iter().map(|geom| (geom.id.as_str(), geom)).collect();
977 let handle_idx: HashMap<(&str, HandleKind, &str), &crate::types::HandleGeom> = handles
981 .iter()
982 .map(|(key, geom)| ((key.node.as_str(), key.kind, key.id.as_str()), geom))
983 .collect();
984 let anchor = |geom: &NodeGeom, kind: HandleKind, handle_id: &Option<Id>| {
985 let key = (geom.id.as_str(), kind, handle_id.as_deref().unwrap_or(""));
986 crate::state::anchor_from_geom(handle_idx.get(&key).copied(), geom, kind)
987 };
988
989 let items: Vec<_> = edges
990 .iter()
991 .filter_map(|edge| {
992 let source_geom = geom_by_id.get(edge.source.as_str())?;
993 let target_geom = geom_by_id.get(edge.target.as_str())?;
994 let (source, source_side, source_on_handle) =
995 anchor(source_geom, HandleKind::Source, &edge.source_handle);
996 let (target, target_side, target_on_handle) =
997 anchor(target_geom, HandleKind::Target, &edge.target_handle);
998 let source = if source_on_handle {
1001 source + source_side.normal() * HANDLE_RIM
1002 } else {
1003 source
1004 };
1005 let target = if target_on_handle {
1006 target + target_side.normal() * HANDLE_RIM
1007 } else {
1008 target
1009 };
1010 Some((
1011 edge.clone(),
1012 source,
1013 source_side,
1014 target,
1015 target_side,
1016 source_geom.rect,
1017 target_geom.rect,
1018 ))
1019 })
1020 .collect();
1021
1022 rsx! {
1023 svg { class: "df-edges", "aria-hidden": "true",
1026 defs { EdgeMarkers { iid: core.iid } }
1027 for (edge, source, source_side, target, target_side, source_rect, target_rect) in items {
1028 EdgeItem {
1029 key: "{edge.id}",
1030 edge,
1031 source,
1032 source_side,
1033 target,
1034 target_side,
1035 source_rect,
1036 target_rect,
1037 edge_view,
1038 on_edge_click,
1039 }
1040 }
1041 }
1042 }
1043}
1044
1045#[component]
1047fn ConnectionLine() -> Element {
1048 let core = use_context::<FlowCore>();
1049 let connection = core.connection.read();
1050 let Some(conn) = connection.as_ref() else {
1051 return rsx! {};
1052 };
1053 let Some((from, from_side)) = core.anchor_of(&conn.from) else {
1054 return rsx! {};
1055 };
1056 let (to, to_side) = match &conn.snap {
1057 Some(snap) => (snap.point, Some(snap.side)),
1058 None => (conn.cursor, None),
1059 };
1060 let d = connection_path(from, from_side, to, to_side);
1061 rsx! {
1062 svg { class: "df-connection",
1063 path { class: "df-connection-path", d }
1064 }
1065 }
1066}
1067
1068pub(crate) fn deselect_edges(mut edges: Signal<Vec<Edge>>) {
1069 if edges.peek().iter().any(|e| e.selected) {
1070 edges.with_mut(|edges| {
1071 for edge in edges.iter_mut() {
1072 edge.selected = false;
1073 }
1074 });
1075 }
1076}
1077
1078fn add_edge_for_connection(mut edges: Signal<Vec<Edge>>, conn: Connection) {
1081 let duplicate = edges.peek().iter().any(|e| {
1082 e.source == conn.source
1083 && e.target == conn.target
1084 && e.source_handle == conn.source_handle
1085 && e.target_handle == conn.target_handle
1086 });
1087 if duplicate {
1088 return;
1089 }
1090 let mut edge = conn.into_edge();
1091 let base = edge.id.clone();
1092 let mut n = 2;
1093 while edges.peek().iter().any(|e| e.id == edge.id) {
1094 edge.id = format!("{base}-{n}");
1095 n += 1;
1096 }
1097 edges.with_mut(|edges| edges.push(edge));
1098}
1099
1100fn delete_request<T: Clone + PartialEq + 'static>(
1102 nodes: Signal<Vec<Node<T>>>,
1103 edges: Signal<Vec<Edge>>,
1104) -> DeleteRequest {
1105 let removed: std::collections::HashSet<Id> = nodes
1106 .peek()
1107 .iter()
1108 .filter(|n| n.selected)
1109 .map(|n| n.id.clone())
1110 .collect();
1111 let edge_ids = edges
1112 .peek()
1113 .iter()
1114 .filter(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target))
1115 .map(|e| e.id.clone())
1116 .collect();
1117 DeleteRequest {
1118 nodes: removed.into_iter().collect(),
1119 edges: edge_ids,
1120 }
1121}
1122
1123pub(crate) fn delete_selected<T: Clone + PartialEq + 'static>(
1124 mut nodes: Signal<Vec<Node<T>>>,
1125 mut edges: Signal<Vec<Edge>>,
1126) {
1127 let removed: std::collections::HashSet<Id> = nodes
1128 .peek()
1129 .iter()
1130 .filter(|n| n.selected)
1131 .map(|n| n.id.clone())
1132 .collect();
1133 let any_edges = edges
1134 .peek()
1135 .iter()
1136 .any(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target));
1137 if !removed.is_empty() {
1138 nodes.with_mut(|nodes| nodes.retain(|n| !n.selected));
1139 }
1140 if any_edges {
1141 edges.with_mut(|edges| {
1142 edges.retain(|e| {
1143 !e.selected && !removed.contains(&e.source) && !removed.contains(&e.target)
1144 })
1145 });
1146 }
1147}