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)]
83 zoom_on_scroll: bool,
84 #[props(default = true)]
89 pan_on_scroll: bool,
90 #[props(default = true)]
92 nodes_draggable: bool,
93 #[props(default = 0.0)]
96 drag_threshold: f64,
97 #[props(default = 28.0)]
99 connection_radius: f64,
100 #[props(default = 0.12)] fit_view_padding: f64,
101 id: Option<String>,
104 #[props(default = "Node graph".to_string())]
106 aria_label: String,
107 class: Option<String>,
109 on_pane_press: Option<Callback<Event<PointerData>>>,
115 edges: Option<Signal<Vec<Edge>>>,
118 geoms: Option<Memo<Vec<NodeGeom>>>,
121 deselect_nodes: Option<Callback<()>>,
124 on_connect: Option<EventHandler<Connection>>,
127 on_connect_start: Option<EventHandler<HandleKey>>,
129 on_connect_end: Option<EventHandler<ConnectEnd>>,
133 is_valid_connection: Option<Callback<Connection, bool>>,
136 on_node_drag_start: Option<EventHandler<Vec<Id>>>,
140 on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
143 on_pane_click: Option<EventHandler<Point>>,
146 on_pane_double_click: Option<EventHandler<Point>>,
150 on_canvas_key_down: Option<Callback<Event<KeyboardData>>>,
153 on_drag_move: Option<Callback<Point>>,
156 world: Option<Element>,
158 children: Element,
166) -> Element {
167 let viewport = use_signal(Viewport::default);
168 let container = use_signal(|| Rect::ZERO);
169 let interaction = use_signal(Interaction::default);
170 let connection = use_signal(|| None::<ConnectionState>);
171 let handles = use_signal(HashMap::new);
172 let mut config = use_signal(FlowConfig::default);
173 let drag = use_signal(DragState::default);
174 let epoch = use_signal(|| 0u64);
175 let pending_sizes = use_signal(Vec::new);
176 let size_flush_queued = use_signal(|| false);
177 let pending_handles = use_signal(Vec::new);
178 let handle_flush_queued = use_signal(|| false);
179 let snap_key = use_memo(move || {
180 connection
181 .read()
182 .as_ref()
183 .and_then(|c| c.snap.as_ref())
184 .map(|s| s.key.clone())
185 });
186 let connect_from = use_memo(move || connection.read().as_ref().map(|c| c.from.clone()));
187 let overlay_insets = use_signal(HashMap::new);
188 let own_edges = use_signal(Vec::new);
189 let edges = edges.unwrap_or(own_edges);
190 let empty_geoms = use_memo(Vec::new);
191 let geoms = geoms.unwrap_or(empty_geoms);
192 let deselect_nodes = deselect_nodes.unwrap_or_else(|| use_callback(move |_: ()| {}));
193
194 let core = use_hook(|| FlowCore {
195 iid: NEXT_IID.fetch_add(1, Ordering::Relaxed),
196 viewport,
197 container,
198 interaction,
199 connection,
200 handles,
201 edges,
202 geoms,
203 config,
204 drag,
205 epoch,
206 snap_key,
207 connect_from,
208 deselect_nodes,
209 overlay_insets,
210 pending_sizes,
211 size_flush_queued,
212 pending_handles,
213 handle_flush_queued,
214 on_connect_start,
215 valid_connection: is_valid_connection,
216 });
217 use_context_provider(|| core);
218
219 let cfg = FlowConfig {
221 min_zoom,
222 max_zoom,
223 pan_on_drag,
224 zoom_on_scroll,
225 pan_on_scroll,
226 nodes_draggable,
227 drag_threshold,
228 connection_radius,
229 fit_view_padding,
230 };
231 if *config.peek() != cfg {
232 config.set(cfg);
233 }
234
235 let mounted: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
237 let refresh_rect = use_callback(move |_: ()| {
238 let element = mounted.peek().clone();
239 let mut container = container;
240 if let Some(element) = element {
241 spawn(async move {
242 if let Ok(rect) = element.get_client_rect().await {
243 let rect = Rect::new(rect.origin.x, rect.origin.y, rect.width(), rect.height());
244 if *container.peek() != rect {
245 container.set(rect);
246 }
247 }
248 });
249 }
250 });
251
252 let end_gesture = use_callback(move |_: ()| {
255 let mut interaction = interaction;
256 let mut connection = connection;
257 if *interaction.peek() != Interaction::None {
258 interaction.set(Interaction::None);
259 }
260 if connection.peek().is_some() {
261 connection.set(None);
262 }
263 let mut drag = drag;
264 let mut state = drag.write();
265 state.pointer_id = None;
266 state.suppress_click = false;
267 });
268
269 let on_pointer_down = move |evt: Event<PointerData>| {
270 refresh_rect.call(());
271 core.cancel_animations();
272 if *interaction.peek() != Interaction::None {
275 return;
276 }
277 if evt.trigger_button() != Some(MouseButton::Primary) {
278 return;
279 }
280 if let Some(handler) = &on_pane_press {
283 handler.call(evt.clone());
284 if *interaction.peek() != Interaction::None {
285 return;
286 }
287 }
288 let client = client_point(evt.client_coordinates());
289 {
290 let mut drag = drag;
291 let mut state = drag.write();
292 state.pointer_id = Some(evt.pointer_id());
293 state.origin_client = client;
294 state.last_client = client;
295 state.moved = false;
296 state.suppress_click = false;
297 state.grabs.clear();
298 }
299 let mut interaction = interaction;
300 if pan_on_drag {
301 interaction.set(Interaction::Pan);
302 } else {
303 interaction.set(Interaction::PanePressed);
304 }
305 };
306
307 let on_pointer_move = move |evt: Event<PointerData>| {
308 let current = *interaction.peek();
309 if current == Interaction::None {
310 return;
311 }
312 if drag
315 .peek()
316 .pointer_id
317 .is_some_and(|id| id != evt.pointer_id())
318 {
319 return;
320 }
321 if evt.held_buttons().is_empty() {
324 end_gesture.call(());
325 return;
326 }
327 let client = client_point(evt.client_coordinates());
328 match current {
329 Interaction::Pan => {
330 let delta = {
331 let mut drag = drag;
332 let mut state = drag.write();
333 let delta = client - state.last_client;
334 state.last_client = client;
335 state.moved = true;
336 delta
337 };
338 let mut viewport = viewport;
339 let vp = *viewport.peek();
340 viewport.set(vp.panned(delta));
341 }
342 Interaction::DragNode => {
343 let began = {
348 let mut drag = drag;
349 let mut state = drag.write();
350 state.last_client = client;
351 let travelled = state.origin_client.distance(client);
352 let passed = state.moved || travelled >= config.peek().drag_threshold;
353 let began = passed && !state.moved;
354 if passed {
355 state.moved = true;
356 }
357 if !passed {
358 return;
359 }
360 began
361 };
362 if began {
363 if let Some(handler) = &on_node_drag_start {
364 let ids: Vec<Id> =
365 drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
366 handler.call(ids);
367 }
368 }
369 let flow = core.client_to_flow(client);
370 if let Some(handler) = &on_drag_move {
371 handler.call(flow);
372 }
373 }
374 Interaction::Connect => {
375 let flow = core.client_to_flow(client);
376 let mut connection = connection;
377 let from = connection.peek().as_ref().map(|c| c.from.clone());
378 if let Some(from) = from {
379 let snap = core.find_snap(&from, flow);
380 connection.set(Some(ConnectionState {
381 from,
382 cursor: flow,
383 snap,
384 }));
385 }
386 }
387 _ => {}
388 }
389 };
390
391 let on_pointer_up = move |evt: Event<PointerData>| {
392 if drag
393 .peek()
394 .pointer_id
395 .is_some_and(|id| id != evt.pointer_id())
396 {
397 return;
398 }
399 let current = *interaction.peek();
400 match current {
401 Interaction::Pan | Interaction::PanePressed => {
403 let state = drag.peek().clone();
404 let is_click =
405 (current == Interaction::PanePressed || !state.moved) && !state.suppress_click;
406 if is_click {
407 let client = client_point(evt.client_coordinates());
408 let flow = core.client_to_flow(client);
409 if !evt.modifiers().shift() {
410 deselect_nodes.call(());
411 deselect_edges(edges);
412 }
413 if let Some(handler) = &on_pane_click {
414 handler.call(flow);
415 }
416 }
417 }
418 Interaction::Connect => {
419 let done = connection.peek().clone();
420 if let Some(done) = done {
421 let completed = done
422 .snap
423 .as_ref()
424 .map(|snap| orient_connection(&done.from, &snap.key));
425 if let Some(conn) = completed.clone() {
426 match &on_connect {
427 Some(handler) => handler.call(conn),
428 None => add_edge_for_connection(edges, conn),
429 }
430 }
431 if let Some(handler) = &on_connect_end {
435 let client = client_point(evt.client_coordinates());
436 handler.call(ConnectEnd {
437 point: core.client_to_flow(client),
438 connection: completed,
439 });
440 }
441 }
442 }
443 Interaction::DragNode if drag.peek().moved => {
444 if let Some(handler) = &on_node_drag_stop {
445 let ids: Vec<Id> = drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
446 handler.call(ids);
447 }
448 }
449 _ => {}
450 }
451 end_gesture.call(());
452 };
453
454 let on_wheel = move |evt: Event<WheelData>| {
455 let config = *config.peek();
456 if !config.pan_on_scroll && !config.zoom_on_scroll {
457 return;
458 }
459 evt.prevent_default();
460 core.cancel_animations();
461 let client = client_point(evt.client_coordinates());
462 let page = container.peek().height;
463 let delta = wheel_pixels(evt.delta(), page);
464 let modifiers = evt.modifiers();
465 if config.pan_on_scroll {
468 if modifiers.ctrl() || modifiers.meta() {
469 if delta.y != 0.0 {
470 let factor = (-delta.y * PINCH_ZOOM_SENSITIVITY).exp();
471 core.zoom_by(factor, Some(client), 0);
472 }
473 return;
474 }
475 let mut viewport = viewport;
476 let vp = *viewport.peek();
477 let by = if modifiers.shift() && delta.x == 0.0 {
480 Point::new(-delta.y, 0.0)
481 } else {
482 Point::new(-delta.x, -delta.y)
483 };
484 viewport.set(vp.panned(by));
485 return;
486 }
487 if delta.y == 0.0 {
488 return;
489 }
490 let factor = (-delta.y * 0.0022).exp().clamp(0.5, 2.0);
491 core.zoom_by(factor, Some(client), 0);
492 };
493
494 let on_key_down = move |evt: Event<KeyboardData>| {
495 if evt.key() == Key::Escape {
496 end_gesture.call(());
497 }
498 if let Some(handler) = &on_canvas_key_down {
499 handler.call(evt);
500 }
501 };
502
503 let gesture = *interaction.read();
506 let root_class = format!(
507 "dioxus-flow{}{}",
508 match gesture {
509 Interaction::Pan => " df-panning",
510 Interaction::Connect => " df-connecting",
511 _ => "",
512 },
513 class
514 .as_deref()
515 .map(|c| format!(" {c}"))
516 .unwrap_or_default()
517 );
518
519 rsx! {
520 document::Style { {STYLE} }
521 div {
522 id,
523 class: root_class,
524 tabindex: "0",
525 role: "application",
526 aria_label,
527 onmounted: move |evt| {
528 let mut mounted = mounted;
529 mounted.set(Some(evt.data()));
530 refresh_rect.call(());
531 },
532 onresize: move |_| refresh_rect.call(()),
533 onpointerdown: on_pointer_down,
534 onpointermove: on_pointer_move,
535 onpointerup: on_pointer_up,
536 onpointercancel: move |evt: Event<PointerData>| {
537 let owner = drag.peek().pointer_id;
538 if owner.is_none() || owner == Some(evt.pointer_id()) {
539 end_gesture.call(());
540 }
541 },
542 onwheel: on_wheel,
543 ondoubleclick: move |evt: Event<MouseData>| {
544 if let Some(handler) = &on_pane_double_click {
545 let client = client_point(evt.client_coordinates());
546 handler.call(core.client_to_flow(client));
547 }
548 },
549 onkeydown: on_key_down,
550 ViewportPane { {world} }
551 {children}
552 }
553 }
554}
555
556#[component]
577pub fn Flow<T: Clone + PartialEq + 'static>(
578 nodes: Signal<Vec<Node<T>>>,
580 edges: Signal<Vec<Edge>>,
582 #[props(default)]
586 anchor: AnchorMode,
587 #[props(default = 0.25)] min_zoom: f64,
588 #[props(default = 4.0)] max_zoom: f64,
589 #[props(default = true)]
591 pan_on_drag: bool,
592 #[props(default = true)]
595 zoom_on_scroll: bool,
596 #[props(default = true)]
600 pan_on_scroll: bool,
601 #[props(default = true)]
603 nodes_draggable: bool,
604 #[props(default = 0.0)]
607 drag_threshold: f64,
608 #[props(default = 28.0)]
610 connection_radius: f64,
611 #[props(default = false)]
613 fit_view: bool,
614 #[props(default = 0.12)] fit_view_padding: f64,
615 #[props(default = true)]
617 delete_key: bool,
618 id: Option<String>,
620 class: Option<String>,
622 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
625 edge_view: Option<Callback<EdgeViewCtx, Element>>,
627 on_connect: Option<EventHandler<Connection>>,
630 on_connect_start: Option<EventHandler<HandleKey>>,
632 on_connect_end: Option<EventHandler<ConnectEnd>>,
636 is_valid_connection: Option<Callback<Connection, bool>>,
639 on_node_drag_start: Option<EventHandler<Vec<Id>>>,
643 on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
646 on_delete: Option<EventHandler<DeleteRequest>>,
653 on_node_click: Option<EventHandler<Id>>,
654 on_edge_click: Option<EventHandler<Id>>,
655 on_pane_click: Option<EventHandler<Point>>,
657 on_pane_double_click: Option<EventHandler<Point>>,
659 handle: Option<FlowHandle<T>>,
662 children: Element,
665) -> Element {
666 let geoms = use_memo(move || {
667 nodes
668 .read()
669 .iter()
670 .map(|node| NodeGeom {
671 id: node.id.clone(),
672 rect: node.rect(),
673 selected: node.selected,
674 source_side: node.source_side,
675 target_side: node.target_side,
676 measured: node.size.is_some() || node.measured.is_some(),
677 })
678 .collect::<Vec<_>>()
679 });
680 let deselect_nodes = use_callback(move |_: ()| {
681 if nodes.peek().iter().any(|n| n.selected) {
682 nodes.clone().with_mut(|nodes| {
683 for node in nodes.iter_mut() {
684 node.selected = false;
685 }
686 });
687 }
688 });
689
690 let attach_core: Signal<Option<FlowCore>> = use_signal(|| None);
692
693 use_effect(move || {
695 let Some(core) = *attach_core.read() else {
696 return;
697 };
698 if let Some(handle) = handle {
699 let mut inner = handle.inner;
700 if inner.peek().is_none() {
701 inner.set(Some(FlowApi { core, nodes }));
702 }
703 }
704 });
705
706 let mut fits = use_signal(|| 0u8);
720 let mut fitted: Signal<Option<(Rect, Rect)>> = use_signal(|| None);
725 let mut touched = use_signal(|| false);
726 use_effect(move || {
727 if let Some(core) = *attach_core.read() {
728 if *core.interaction.read() != Interaction::None {
729 touched.set(true);
730 }
731 }
732 });
733 use_effect(move || {
734 let Some(core) = *attach_core.read() else {
735 return;
736 };
737 if !fit_view || *touched.peek() {
738 return;
739 }
740 let container = *core.container.read();
741 let bounds = geoms
742 .read()
743 .iter()
744 .map(|geom| geom.rect)
745 .reduce(|acc, rect| acc.union(&rect));
746 let Some(bounds) = bounds else { return };
747 if container.width <= 0.0 || container.height <= 0.0 {
748 return;
749 }
750 let same = |a: Rect, b: Rect| {
754 (a.x - b.x).abs() < 1.0
755 && (a.y - b.y).abs() < 1.0
756 && (a.width - b.width).abs() < 1.0
757 && (a.height - b.height).abs() < 1.0
758 };
759 let settled = fitted
760 .peek()
761 .is_some_and(|(b, c)| same(b, bounds) && same(c, container));
762 if settled || *fits.peek() >= 12 {
763 return;
764 }
765 let done = *fits.peek();
766 fitted.set(Some((bounds, container)));
767 fits.set(done + 1);
768 core.fit_view(0);
769 });
770
771 let on_drag_move = use_callback(move |flow: Point| {
772 let Some(core) = *attach_core.peek() else {
773 return;
774 };
775 let grabs = core.drag.peek().grabs.clone();
776 let mut nodes = nodes;
777 nodes.with_mut(|nodes| {
778 for (id, grab) in &grabs {
779 if let Some(node) = nodes.iter_mut().find(|n| &n.id == id) {
780 node.position = flow - *grab;
781 }
782 }
783 });
784 });
785
786 let on_canvas_key_down = use_callback(move |evt: Event<KeyboardData>| match evt.key() {
787 Key::Delete | Key::Backspace if delete_key => {
788 let Some(core) = *attach_core.peek() else {
789 return;
790 };
791 let request = delete_request(nodes, core.edges);
792 if request.nodes.is_empty() && request.edges.is_empty() {
793 return;
794 }
795 match &on_delete {
796 Some(handler) => handler.call(request),
797 None => delete_selected(nodes, core.edges),
798 }
799 }
800 _ => {}
801 });
802
803 rsx! {
804 Canvas {
805 min_zoom,
806 max_zoom,
807 pan_on_drag,
808 zoom_on_scroll,
809 pan_on_scroll,
810 nodes_draggable,
811 drag_threshold,
812 connection_radius,
813 fit_view_padding,
814 id,
815 class,
816 edges,
817 geoms,
818 deselect_nodes,
819 on_connect,
820 on_connect_start,
821 on_connect_end,
822 is_valid_connection,
823 on_node_drag_start,
824 on_node_drag_stop,
825 on_pane_click,
826 on_pane_double_click,
827 on_canvas_key_down,
828 on_drag_move,
829 world: rsx! {
830 CoreProbe { attach_core }
831 match anchor {
832 AnchorMode::Handles => rsx! {
833 EdgesLayer { edge_view, on_edge_click }
834 NodesLayer { nodes, node_view, on_node_click }
835 },
836 AnchorMode::Seats => rsx! {
837 SeatGraphLayers {
838 nodes,
839 node_view,
840 on_node_click,
841 edge_view,
842 on_edge_click,
843 }
844 },
845 }
846 ConnectionLine {}
847 },
848 {children}
849 }
850 }
851}
852
853#[component]
859fn SeatGraphLayers<T: Clone + PartialEq + 'static>(
860 nodes: Signal<Vec<Node<T>>>,
861 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
862 on_node_click: Option<EventHandler<Id>>,
863 edge_view: Option<Callback<EdgeViewCtx, Element>>,
864 on_edge_click: Option<EventHandler<Id>>,
865) -> Element {
866 let core = use_context::<FlowCore>();
867 let anchors = use_memo(move || {
872 let geoms = core.geoms.read();
873 let frames: std::collections::BTreeMap<Id, Rect> = geoms
874 .iter()
875 .map(|geom| (geom.id.clone(), geom.rect))
876 .collect();
877 let links: Vec<crate::ports::Link> = core
878 .edges
879 .read()
880 .iter()
881 .map(|edge| crate::ports::Link {
882 id: edge.id.clone(),
883 start: crate::ports::Terminal::Node(edge.source.clone()),
884 end: crate::ports::Terminal::Node(edge.target.clone()),
885 start_seat: edge.source_seat,
886 end_seat: edge.target_seat,
887 })
888 .collect();
889 crate::ports::solve_ports(&frames, &links)
890 });
891
892 let adapted_edge_view = edge_view.map(|view| {
896 Callback::new(move |ctx: crate::edge::SeatEdgeViewCtx| {
897 view.call(EdgeViewCtx {
898 edge: ctx.edge.clone(),
899 source: ctx.anchors.start.point(),
900 source_side: ctx.anchors.start.side(),
901 target: ctx.anchors.end.point(),
902 target_side: ctx.anchors.end.side(),
903 path: crate::path::EdgePath {
904 d: ctx.geometry.path.clone(),
905 label: ctx.geometry.label,
906 },
907 marker_end: None,
909 })
910 })
911 });
912
913 let edges = core.edges;
914 let solved = anchors.read();
915 rsx! {
916 crate::edge::SeatEdges {
917 edges,
918 anchors,
919 edge_view: adapted_edge_view,
920 on_edge_click,
921 }
922 crate::edge::SeatEdgeLabels { edges, anchors }
923 NodesLayer { nodes, node_view, on_node_click }
924 svg { class: "df-edges df-ports", "aria-hidden": "true",
926 for edge in edges.read().iter() {
927 if let Some(pair) = solved.get(&edge.id) {
928 g {
929 key: "{edge.id}",
930 class: if edge.selected { "df-selected" },
931 circle {
932 class: "df-port",
933 cx: pair.start.x,
934 cy: pair.start.y,
935 r: crate::ports::PORT_RADIUS,
936 }
937 circle {
938 class: "df-port",
939 cx: pair.end.x,
940 cy: pair.end.y,
941 r: crate::ports::PORT_RADIUS,
942 }
943 }
944 }
945 }
946 }
947 }
948}
949
950#[component]
953fn CoreProbe(attach_core: Signal<Option<FlowCore>>) -> Element {
954 let core = use_context::<FlowCore>();
955 let mut attach_core = attach_core;
956 if attach_core.peek().is_none() {
957 attach_core.set(Some(core));
958 }
959 rsx! {}
960}
961
962#[component]
965fn ViewportPane(children: Element) -> Element {
966 let core = use_context::<FlowCore>();
967 let vp = *core.viewport.read();
968 rsx! {
969 div {
970 class: "df-viewport",
971 style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
972 {children}
973 }
974 }
975}
976
977#[component]
981pub fn WorldLayer(class: Option<String>, children: Element) -> Element {
982 let core = use_context::<FlowCore>();
983 let vp = *core.viewport.read();
984 let class = format!(
985 "df-world-layer{}",
986 class
987 .as_deref()
988 .map(|c| format!(" {c}"))
989 .unwrap_or_default()
990 );
991 rsx! {
992 div {
993 class,
994 style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
995 {children}
996 }
997 }
998}
999
1000#[component]
1008fn NodesLayer<T: Clone + PartialEq + 'static>(
1009 nodes: Signal<Vec<Node<T>>>,
1010 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
1011 on_node_click: Option<EventHandler<Id>>,
1012) -> Element {
1013 let core = use_context::<FlowCore>();
1014 let tiles = use_memo(move || {
1015 let nodes = nodes.read();
1016 let grabbed = (*core.interaction.read() == Interaction::DragNode)
1023 .then(|| core.drag.peek().grabs.clone());
1024 crate::tile::tiles(nodes.iter().map(|node| {
1025 let raised = node.selected
1026 || grabbed
1027 .as_ref()
1028 .is_some_and(|grabs| grabs.iter().any(|(id, _)| id == &node.id));
1029 (node.rect(), raised)
1030 }))
1031 });
1032 let all = nodes.read();
1034 rsx! {
1035 div { class: "df-nodes",
1036 for tile in tiles.read().iter() {
1037 div {
1038 key: "{tile.cell.0},{tile.cell.1}",
1039 class: if tile.raised { "df-tile df-raised" } else { "df-tile" },
1040 style: tile_style(tile),
1041 for node in tile.members.iter().filter_map(|&i| all.get(i)) {
1042 NodeItem::<T> {
1043 key: "{node.id}",
1044 nodes,
1045 node: node.clone(),
1046 origin: tile.origin(),
1047 node_view,
1048 on_node_click,
1049 }
1050 }
1051 }
1052 }
1053 }
1054 }
1055}
1056
1057fn tile_style(tile: &crate::tile::Tile) -> String {
1063 format!(
1064 "transform:translate({}px,{}px);width:{}px;height:{}px;\
1065 contain-intrinsic-size:{}px {}px;",
1066 tile.rect.x,
1067 tile.rect.y,
1068 tile.rect.width,
1069 tile.rect.height,
1070 tile.rect.width,
1071 tile.rect.height,
1072 )
1073}
1074
1075#[component]
1076fn EdgesLayer(
1077 edge_view: Option<Callback<EdgeViewCtx, Element>>,
1078 on_edge_click: Option<EventHandler<Id>>,
1079) -> Element {
1080 let core = use_context::<FlowCore>();
1081 let edges = core.edges.read();
1082 let geoms = core.geoms.read();
1083 let handles = core.handles.read();
1084 let geom_by_id: HashMap<&str, &NodeGeom> =
1085 geoms.iter().map(|geom| (geom.id.as_str(), geom)).collect();
1086 let handle_idx: HashMap<(&str, HandleKind, &str), &crate::types::HandleGeom> = handles
1090 .iter()
1091 .map(|(key, geom)| ((key.node.as_str(), key.kind, key.id.as_str()), geom))
1092 .collect();
1093 let anchor = |geom: &NodeGeom, kind: HandleKind, handle_id: &Option<Id>| {
1094 let key = (geom.id.as_str(), kind, handle_id.as_deref().unwrap_or(""));
1095 crate::state::anchor_from_geom(handle_idx.get(&key).copied(), geom, kind)
1096 };
1097
1098 let items: Vec<_> = edges
1099 .iter()
1100 .filter_map(|edge| {
1101 let source_geom = geom_by_id.get(edge.source.as_str())?;
1102 let target_geom = geom_by_id.get(edge.target.as_str())?;
1103 let (source, source_side, source_on_handle) =
1104 anchor(source_geom, HandleKind::Source, &edge.source_handle);
1105 let (target, target_side, target_on_handle) =
1106 anchor(target_geom, HandleKind::Target, &edge.target_handle);
1107 let source = if source_on_handle {
1110 source + source_side.normal() * HANDLE_RIM
1111 } else {
1112 source
1113 };
1114 let target = if target_on_handle {
1115 target + target_side.normal() * HANDLE_RIM
1116 } else {
1117 target
1118 };
1119 Some((
1120 edge.clone(),
1121 source,
1122 source_side,
1123 target,
1124 target_side,
1125 source_geom.rect,
1126 target_geom.rect,
1127 ))
1128 })
1129 .collect();
1130
1131 let tiles = crate::tile::tiles(items.iter().map(|item| {
1137 let (edge, source, source_side, target, target_side, source_rect, target_rect) = item;
1138 let geo = crate::path::EdgeGeometry::new(*source, *source_side, *target, *target_side)
1139 .with_rects(*source_rect, *target_rect);
1140 (crate::path::edge_bounds(edge.kind, &geo), edge.selected)
1141 }));
1142
1143 rsx! {
1144 div { class: "df-edges", "aria-hidden": "true",
1147 svg { class: "df-edge-defs",
1150 defs { EdgeMarkers { iid: core.iid } }
1151 }
1152 for tile in tiles.iter() {
1153 svg {
1154 key: "{tile.cell.0},{tile.cell.1}",
1155 class: if tile.raised { "df-edge-tile df-raised" } else { "df-edge-tile" },
1156 view_box: "{tile.rect.x} {tile.rect.y} {tile.rect.width} {tile.rect.height}",
1160 style: tile_style(tile),
1161 for &i in tile.members.iter() {
1162 if let Some((edge, source, source_side, target, target_side, source_rect, target_rect)) = items.get(i) {
1163 EdgeItem {
1164 key: "{edge.id}",
1165 edge: edge.clone(),
1166 source: *source,
1167 source_side: *source_side,
1168 target: *target,
1169 target_side: *target_side,
1170 source_rect: *source_rect,
1171 target_rect: *target_rect,
1172 edge_view,
1173 on_edge_click,
1174 }
1175 }
1176 }
1177 }
1178 }
1179 }
1180 }
1181}
1182
1183#[component]
1185fn ConnectionLine() -> Element {
1186 let core = use_context::<FlowCore>();
1187 let connection = core.connection.read();
1188 let Some(conn) = connection.as_ref() else {
1189 return rsx! {};
1190 };
1191 let Some((from, from_side)) = core.anchor_of(&conn.from) else {
1192 return rsx! {};
1193 };
1194 let (to, to_side) = match &conn.snap {
1195 Some(snap) => (snap.point, Some(snap.side)),
1196 None => (conn.cursor, None),
1197 };
1198 let d = connection_path(from, from_side, to, to_side);
1199 rsx! {
1200 svg { class: "df-connection",
1201 path { class: "df-connection-path", d }
1202 }
1203 }
1204}
1205
1206pub(crate) fn deselect_edges(mut edges: Signal<Vec<Edge>>) {
1207 if edges.peek().iter().any(|e| e.selected) {
1208 edges.with_mut(|edges| {
1209 for edge in edges.iter_mut() {
1210 edge.selected = false;
1211 }
1212 });
1213 }
1214}
1215
1216fn add_edge_for_connection(mut edges: Signal<Vec<Edge>>, conn: Connection) {
1219 let duplicate = edges.peek().iter().any(|e| {
1220 e.source == conn.source
1221 && e.target == conn.target
1222 && e.source_handle == conn.source_handle
1223 && e.target_handle == conn.target_handle
1224 });
1225 if duplicate {
1226 return;
1227 }
1228 let mut edge = conn.into_edge();
1229 let base = edge.id.clone();
1230 let mut n = 2;
1231 while edges.peek().iter().any(|e| e.id == edge.id) {
1232 edge.id = format!("{base}-{n}");
1233 n += 1;
1234 }
1235 edges.with_mut(|edges| edges.push(edge));
1236}
1237
1238fn delete_request<T: Clone + PartialEq + 'static>(
1240 nodes: Signal<Vec<Node<T>>>,
1241 edges: Signal<Vec<Edge>>,
1242) -> DeleteRequest {
1243 let removed: std::collections::HashSet<Id> = nodes
1244 .peek()
1245 .iter()
1246 .filter(|n| n.selected)
1247 .map(|n| n.id.clone())
1248 .collect();
1249 let edge_ids = edges
1250 .peek()
1251 .iter()
1252 .filter(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target))
1253 .map(|e| e.id.clone())
1254 .collect();
1255 DeleteRequest {
1256 nodes: removed.into_iter().collect(),
1257 edges: edge_ids,
1258 }
1259}
1260
1261pub(crate) fn delete_selected<T: Clone + PartialEq + 'static>(
1262 mut nodes: Signal<Vec<Node<T>>>,
1263 mut edges: Signal<Vec<Edge>>,
1264) {
1265 let removed: std::collections::HashSet<Id> = nodes
1266 .peek()
1267 .iter()
1268 .filter(|n| n.selected)
1269 .map(|n| n.id.clone())
1270 .collect();
1271 let any_edges = edges
1272 .peek()
1273 .iter()
1274 .any(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target));
1275 if !removed.is_empty() {
1276 nodes.with_mut(|nodes| nodes.retain(|n| !n.selected));
1277 }
1278 if any_edges {
1279 edges.with_mut(|edges| {
1280 edges.retain(|e| {
1281 !e.selected && !removed.contains(&e.source) && !removed.contains(&e.target)
1282 })
1283 });
1284 }
1285}