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#[derive(Clone, PartialEq, Props)]
61pub struct CanvasProps {
62 #[props(default = 0.25)]
63 pub min_zoom: f64,
64 #[props(default = 4.0)]
65 pub max_zoom: f64,
66 #[props(default = true)]
68 pub pan_on_drag: bool,
69 #[props(default = true)]
72 pub zoom_on_scroll: bool,
73 #[props(default = true)]
78 pub pan_on_scroll: bool,
79 #[props(default = true)]
81 pub nodes_draggable: bool,
82 #[props(default = 0.0)]
85 pub drag_threshold: f64,
86 #[props(default = 28.0)]
88 pub connection_radius: f64,
89 #[props(default = 0.12)]
90 pub fit_view_padding: f64,
91 pub id: Option<String>,
94 #[props(default = "Node graph".to_string())]
96 pub aria_label: String,
97 pub class: Option<String>,
99 pub on_pane_press: Option<Callback<Event<PointerData>>>,
105 pub edges: Option<Signal<Vec<Edge>>>,
108 pub geoms: Option<Memo<Vec<NodeGeom>>>,
111 pub deselect_nodes: Option<Callback<()>>,
114 pub on_connect: Option<EventHandler<Connection>>,
117 pub on_connect_start: Option<EventHandler<HandleKey>>,
119 pub on_connect_end: Option<EventHandler<ConnectEnd>>,
123 pub is_valid_connection: Option<Callback<Connection, bool>>,
126 pub on_node_drag_start: Option<EventHandler<Vec<Id>>>,
130 pub on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
133 pub on_pane_click: Option<EventHandler<Point>>,
136 pub on_pane_double_click: Option<EventHandler<Point>>,
140 pub on_canvas_key_down: Option<Callback<Event<KeyboardData>>>,
143 pub on_drag_move: Option<Callback<Point>>,
146 pub world: Option<Element>,
148 pub children: Element,
156}
157
158#[allow(non_snake_case)]
172pub fn Canvas(props: CanvasProps) -> Element {
173 let core = props.use_core();
174 props.render(core)
175}
176
177impl CanvasProps {
178 fn use_core(&self) -> FlowCore {
182 let viewport = use_signal(Viewport::default);
183 let container = use_signal(|| Rect::ZERO);
184 let interaction = use_signal(Interaction::default);
185 let connection = use_signal(|| None::<ConnectionState>);
186 let handles = use_signal(HashMap::new);
187 let config = use_signal(FlowConfig::default);
188 let drag = use_signal(DragState::default);
189 let epoch = use_signal(|| 0u64);
190 let pending_sizes = use_signal(Vec::new);
191 let size_flush_queued = use_signal(|| false);
192 let pending_handles = use_signal(Vec::new);
193 let handle_flush_queued = use_signal(|| false);
194 let snap_key = use_memo(move || {
195 connection
196 .read()
197 .as_ref()
198 .and_then(|c| c.snap.as_ref())
199 .map(|s| s.key.clone())
200 });
201 let connect_from = use_memo(move || connection.read().as_ref().map(|c| c.from.clone()));
202 let overlay_insets = use_signal(HashMap::new);
203 let own_edges = use_signal(Vec::new);
204 let edges = self.edges.unwrap_or(own_edges);
205 let empty_geoms = use_memo(Vec::new);
206 let geoms = self.geoms.unwrap_or(empty_geoms);
207 let noop_deselect = use_callback(move |_: ()| {});
208 let deselect_nodes = self.deselect_nodes.unwrap_or(noop_deselect);
209
210 let core = use_hook(|| FlowCore {
211 iid: NEXT_IID.fetch_add(1, Ordering::Relaxed),
212 viewport,
213 container,
214 interaction,
215 connection,
216 handles,
217 edges,
218 geoms,
219 config,
220 drag,
221 epoch,
222 snap_key,
223 connect_from,
224 deselect_nodes,
225 overlay_insets,
226 pending_sizes,
227 size_flush_queued,
228 pending_handles,
229 handle_flush_queued,
230 on_connect_start: self.on_connect_start,
231 valid_connection: self.is_valid_connection,
232 });
233 use_context_provider(|| core);
234
235 core
236 }
237
238 fn render(self, core: FlowCore) -> Element {
239 let Self {
240 min_zoom,
241 max_zoom,
242 fit_view_padding,
243 pan_on_drag,
244 zoom_on_scroll,
245 pan_on_scroll,
246 nodes_draggable,
247 drag_threshold,
248 connection_radius,
249 id,
250 aria_label,
251 class,
252 on_pane_press,
253 on_connect,
254 on_connect_end,
255 on_node_drag_start,
256 on_node_drag_stop,
257 on_pane_click,
258 on_pane_double_click,
259 on_canvas_key_down,
260 on_drag_move,
261 world,
262 children,
263 ..
264 } = self;
265 let FlowCore {
266 viewport,
267 container,
268 interaction,
269 connection,
270 mut config,
271 drag,
272 edges,
273 deselect_nodes,
274 ..
275 } = core;
276
277 let cfg = FlowConfig {
279 min_zoom,
280 max_zoom,
281 pan_on_drag,
282 zoom_on_scroll,
283 pan_on_scroll,
284 nodes_draggable,
285 drag_threshold,
286 connection_radius,
287 fit_view_padding,
288 };
289 if *config.peek() != cfg {
290 config.set(cfg);
291 }
292
293 let mounted: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
295 let refresh_rect = use_callback(move |_: ()| {
296 let element = mounted.peek().clone();
297 let mut container = container;
298 if let Some(element) = element {
299 spawn(async move {
300 if let Ok(rect) = element.get_client_rect().await {
301 let rect =
302 Rect::new(rect.origin.x, rect.origin.y, rect.width(), rect.height());
303 if *container.peek() != rect {
304 container.set(rect);
305 }
306 }
307 });
308 }
309 });
310
311 let end_gesture = use_callback(move |_: ()| {
314 let mut interaction = interaction;
315 let mut connection = connection;
316 if *interaction.peek() != Interaction::None {
317 interaction.set(Interaction::None);
318 }
319 if connection.peek().is_some() {
320 connection.set(None);
321 }
322 let mut drag = drag;
323 let mut state = drag.write();
324 state.pointer_id = None;
325 state.suppress_click = false;
326 });
327
328 let on_pointer_down = move |evt: Event<PointerData>| {
329 refresh_rect.call(());
330 core.cancel_animations();
331 if *interaction.peek() != Interaction::None {
334 return;
335 }
336 if evt.trigger_button() != Some(MouseButton::Primary) {
337 return;
338 }
339 if let Some(handler) = &on_pane_press {
342 handler.call(evt.clone());
343 if *interaction.peek() != Interaction::None {
344 return;
345 }
346 }
347 let client = client_point(evt.client_coordinates());
348 {
349 let mut drag = drag;
350 let mut state = drag.write();
351 state.pointer_id = Some(evt.pointer_id());
352 state.origin_client = client;
353 state.last_client = client;
354 state.moved = false;
355 state.suppress_click = false;
356 state.grabs.clear();
357 }
358 let mut interaction = interaction;
359 if pan_on_drag {
360 interaction.set(Interaction::Pan);
361 } else {
362 interaction.set(Interaction::PanePressed);
363 }
364 };
365
366 let on_pointer_move = move |evt: Event<PointerData>| {
367 let current = *interaction.peek();
368 if current == Interaction::None {
369 return;
370 }
371 if drag
374 .peek()
375 .pointer_id
376 .is_some_and(|id| id != evt.pointer_id())
377 {
378 return;
379 }
380 if evt.held_buttons().is_empty() {
383 end_gesture.call(());
384 return;
385 }
386 let client = client_point(evt.client_coordinates());
387 match current {
388 Interaction::Pan => {
389 let delta = {
390 let mut drag = drag;
391 let mut state = drag.write();
392 let delta = client - state.last_client;
393 state.last_client = client;
394 state.moved = true;
395 delta
396 };
397 let mut viewport = viewport;
398 let vp = *viewport.peek();
399 viewport.set(vp.panned(delta));
400 }
401 Interaction::DragNode => {
402 let began = {
407 let mut drag = drag;
408 let mut state = drag.write();
409 state.last_client = client;
410 let travelled = state.origin_client.distance(client);
411 let passed = state.moved || travelled >= config.peek().drag_threshold;
412 let began = passed && !state.moved;
413 if passed {
414 state.moved = true;
415 }
416 if !passed {
417 return;
418 }
419 began
420 };
421 if began {
422 if let Some(handler) = &on_node_drag_start {
423 let ids: Vec<Id> =
424 drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
425 handler.call(ids);
426 }
427 }
428 let flow = core.client_to_flow(client);
429 if let Some(handler) = &on_drag_move {
430 handler.call(flow);
431 }
432 }
433 Interaction::Connect => {
434 let flow = core.client_to_flow(client);
435 let mut connection = connection;
436 let from = connection.peek().as_ref().map(|c| c.from.clone());
437 if let Some(from) = from {
438 let snap = core.find_snap(&from, flow);
439 connection.set(Some(ConnectionState {
440 from,
441 cursor: flow,
442 snap,
443 }));
444 }
445 }
446 _ => {}
447 }
448 };
449
450 let on_pointer_up = move |evt: Event<PointerData>| {
451 if drag
452 .peek()
453 .pointer_id
454 .is_some_and(|id| id != evt.pointer_id())
455 {
456 return;
457 }
458 let current = *interaction.peek();
459 match current {
460 Interaction::Pan | Interaction::PanePressed => {
462 let state = drag.peek().clone();
463 let is_click = (current == Interaction::PanePressed || !state.moved)
464 && !state.suppress_click;
465 if is_click {
466 let client = client_point(evt.client_coordinates());
467 let flow = core.client_to_flow(client);
468 if !evt.modifiers().shift() {
469 deselect_nodes.call(());
470 deselect_edges(edges);
471 }
472 if let Some(handler) = &on_pane_click {
473 handler.call(flow);
474 }
475 }
476 }
477 Interaction::Connect => {
478 let done = connection.peek().clone();
479 if let Some(done) = done {
480 let completed = done
481 .snap
482 .as_ref()
483 .map(|snap| orient_connection(&done.from, &snap.key));
484 if let Some(conn) = completed.clone() {
485 match &on_connect {
486 Some(handler) => handler.call(conn),
487 None => add_edge_for_connection(edges, conn),
488 }
489 }
490 if let Some(handler) = &on_connect_end {
494 let client = client_point(evt.client_coordinates());
495 handler.call(ConnectEnd {
496 point: core.client_to_flow(client),
497 connection: completed,
498 });
499 }
500 }
501 }
502 Interaction::DragNode if drag.peek().moved => {
503 if let Some(handler) = &on_node_drag_stop {
504 let ids: Vec<Id> =
505 drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
506 handler.call(ids);
507 }
508 }
509 _ => {}
510 }
511 end_gesture.call(());
512 };
513
514 let on_wheel = move |evt: Event<WheelData>| {
515 let config = *config.peek();
516 if !config.pan_on_scroll && !config.zoom_on_scroll {
517 return;
518 }
519 evt.prevent_default();
520 core.cancel_animations();
521 let client = client_point(evt.client_coordinates());
522 let page = container.peek().height;
523 let delta = wheel_pixels(evt.delta(), page);
524 let modifiers = evt.modifiers();
525 if config.pan_on_scroll {
528 if modifiers.ctrl() || modifiers.meta() {
529 if delta.y != 0.0 {
530 let factor = (-delta.y * PINCH_ZOOM_SENSITIVITY).exp();
531 core.zoom_by(factor, Some(client), 0);
532 }
533 return;
534 }
535 let mut viewport = viewport;
536 let vp = *viewport.peek();
537 let by = if modifiers.shift() && delta.x == 0.0 {
540 Point::new(-delta.y, 0.0)
541 } else {
542 Point::new(-delta.x, -delta.y)
543 };
544 viewport.set(vp.panned(by));
545 return;
546 }
547 if delta.y == 0.0 {
548 return;
549 }
550 let factor = (-delta.y * 0.0022).exp().clamp(0.5, 2.0);
551 core.zoom_by(factor, Some(client), 0);
552 };
553
554 let on_key_down = move |evt: Event<KeyboardData>| {
555 if evt.key() == Key::Escape {
556 end_gesture.call(());
557 }
558 if let Some(handler) = &on_canvas_key_down {
559 handler.call(evt);
560 }
561 };
562
563 let gesture = *interaction.read();
566 let root_class = format!(
567 "dioxus-flow{}{}",
568 match gesture {
569 Interaction::Pan => " df-panning",
570 Interaction::Connect => " df-connecting",
571 _ => "",
572 },
573 class
574 .as_deref()
575 .map(|c| format!(" {c}"))
576 .unwrap_or_default()
577 );
578
579 rsx! {
580 FlowStyles {}
581 div {
582 id,
583 class: root_class,
584 tabindex: "0",
585 role: "application",
586 aria_label,
587 onmounted: move |evt| {
588 let mut mounted = mounted;
589 mounted.set(Some(evt.data()));
590 refresh_rect.call(());
591 },
592 onresize: move |_| refresh_rect.call(()),
593 onpointerdown: on_pointer_down,
594 onpointermove: on_pointer_move,
595 onpointerup: on_pointer_up,
596 onpointercancel: move |evt: Event<PointerData>| {
597 let owner = drag.peek().pointer_id;
598 if owner.is_none() || owner == Some(evt.pointer_id()) {
599 end_gesture.call(());
600 }
601 },
602 onwheel: on_wheel,
603 ondoubleclick: move |evt: Event<MouseData>| {
604 if let Some(handler) = &on_pane_double_click {
605 let client = client_point(evt.client_coordinates());
606 handler.call(core.client_to_flow(client));
607 }
608 },
609 onkeydown: on_key_down,
610 ViewportPane { {world} }
611 {children}
612 }
613 }
614 }
615}
616
617#[component]
618fn FlowStyles() -> Element {
619 rsx! { document::Style { "{STYLE}" } }
620}
621
622#[component]
643pub fn Flow<T: Clone + PartialEq + 'static>(
644 nodes: Signal<Vec<Node<T>>>,
646 edges: Signal<Vec<Edge>>,
648 #[props(default)]
652 anchor: AnchorMode,
653 #[props(default = 0.25)] min_zoom: f64,
654 #[props(default = 4.0)] max_zoom: f64,
655 #[props(default = true)]
657 pan_on_drag: bool,
658 #[props(default = true)]
661 zoom_on_scroll: bool,
662 #[props(default = true)]
666 pan_on_scroll: bool,
667 #[props(default = true)]
669 nodes_draggable: bool,
670 #[props(default = 0.0)]
673 drag_threshold: f64,
674 #[props(default = 28.0)]
676 connection_radius: f64,
677 #[props(default = false)]
679 fit_view: bool,
680 #[props(default = 0.12)] fit_view_padding: f64,
681 #[props(default = true)]
683 delete_key: bool,
684 id: Option<String>,
686 class: Option<String>,
688 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
691 edge_view: Option<Callback<EdgeViewCtx, Element>>,
693 on_connect: Option<EventHandler<Connection>>,
696 on_connect_start: Option<EventHandler<HandleKey>>,
698 on_connect_end: Option<EventHandler<ConnectEnd>>,
702 is_valid_connection: Option<Callback<Connection, bool>>,
705 on_node_drag_start: Option<EventHandler<Vec<Id>>>,
709 on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
712 on_delete: Option<EventHandler<DeleteRequest>>,
719 on_node_click: Option<EventHandler<Id>>,
720 on_edge_click: Option<EventHandler<Id>>,
721 on_pane_click: Option<EventHandler<Point>>,
723 on_pane_double_click: Option<EventHandler<Point>>,
725 handle: Option<FlowHandle<T>>,
728 children: Element,
731) -> Element {
732 let geoms = use_memo(move || {
733 nodes
734 .read()
735 .iter()
736 .map(|node| NodeGeom {
737 id: node.id.clone(),
738 rect: node.rect(),
739 selected: node.selected,
740 source_side: node.source_side,
741 target_side: node.target_side,
742 measured: node.size.is_some() || node.measured.is_some(),
743 })
744 .collect::<Vec<_>>()
745 });
746 let deselect_nodes = use_callback(move |_: ()| {
747 if nodes.peek().iter().any(|n| n.selected) {
748 nodes.clone().with_mut(|nodes| {
749 for node in nodes.iter_mut() {
750 node.selected = false;
751 }
752 });
753 }
754 });
755
756 let mut canvas = CanvasProps::builder()
757 .min_zoom(min_zoom)
758 .max_zoom(max_zoom)
759 .pan_on_drag(pan_on_drag)
760 .zoom_on_scroll(zoom_on_scroll)
761 .pan_on_scroll(pan_on_scroll)
762 .nodes_draggable(nodes_draggable)
763 .drag_threshold(drag_threshold)
764 .connection_radius(connection_radius)
765 .fit_view_padding(fit_view_padding)
766 .id(id)
767 .class(class)
768 .edges(edges)
769 .geoms(geoms)
770 .deselect_nodes(deselect_nodes)
771 .on_connect(on_connect)
772 .on_connect_start(on_connect_start)
773 .on_connect_end(on_connect_end)
774 .is_valid_connection(is_valid_connection)
775 .on_node_drag_start(on_node_drag_start)
776 .on_node_drag_stop(on_node_drag_stop)
777 .on_pane_click(on_pane_click)
778 .on_pane_double_click(on_pane_double_click)
779 .world(rsx! {
780 match anchor {
781 AnchorMode::Handles => rsx! {
782 EdgesLayer { edge_view, on_edge_click }
783 NodesLayer { nodes, node_view, on_node_click }
784 },
785 AnchorMode::Seats => rsx! {
786 SeatGraphLayers {
787 nodes,
788 node_view,
789 on_node_click,
790 edge_view,
791 on_edge_click,
792 }
793 },
794 }
795 ConnectionLine {}
796 })
797 .children(children)
798 .build();
799 let core = canvas.inner.use_core();
800
801 use_effect(move || {
803 if let Some(handle) = handle {
804 let mut inner = handle.inner;
805 if inner.peek().is_none() {
806 inner.set(Some(FlowApi { core, nodes }));
807 }
808 }
809 });
810
811 use_drop(move || {
812 if let Some(handle) = handle {
813 let mut inner = handle.inner;
814 if inner.peek().is_some_and(|api| api.core == core) {
815 inner.set(None);
816 }
817 }
818 });
819
820 let mut fits = use_signal(|| 0u8);
834 let mut fitted: Signal<Option<(Rect, Rect)>> = use_signal(|| None);
839 let mut touched = use_signal(|| false);
840 use_effect(move || {
841 if *core.interaction.read() != Interaction::None {
842 touched.set(true);
843 }
844 });
845 use_effect(move || {
846 if !fit_view || *touched.peek() {
847 return;
848 }
849 let container = *core.container.read();
850 let bounds = geoms
851 .read()
852 .iter()
853 .map(|geom| geom.rect)
854 .reduce(|acc, rect| acc.union(&rect));
855 let Some(bounds) = bounds else { return };
856 if container.width <= 0.0 || container.height <= 0.0 {
857 return;
858 }
859 let same = |a: Rect, b: Rect| {
863 (a.x - b.x).abs() < 1.0
864 && (a.y - b.y).abs() < 1.0
865 && (a.width - b.width).abs() < 1.0
866 && (a.height - b.height).abs() < 1.0
867 };
868 let settled = fitted
869 .peek()
870 .is_some_and(|(b, c)| same(b, bounds) && same(c, container));
871 if settled || *fits.peek() >= 12 {
872 return;
873 }
874 let done = *fits.peek();
875 fitted.set(Some((bounds, container)));
876 fits.set(done + 1);
877 core.fit_view(0);
878 });
879
880 let on_drag_move = use_callback(move |flow: Point| {
881 let grabs = core.drag.peek().grabs.clone();
882 let mut nodes = nodes;
883 nodes.with_mut(|nodes| {
884 for (id, grab) in &grabs {
885 if let Some(node) = nodes.iter_mut().find(|n| &n.id == id) {
886 node.position = flow - *grab;
887 }
888 }
889 });
890 });
891
892 let on_canvas_key_down = use_callback(move |evt: Event<KeyboardData>| match evt.key() {
893 Key::Delete | Key::Backspace if delete_key => {
894 let request = delete_request(nodes, core.edges);
895 if request.nodes.is_empty() && request.edges.is_empty() {
896 return;
897 }
898 match &on_delete {
899 Some(handler) => handler.call(request),
900 None => delete_selected(nodes, core.edges),
901 }
902 }
903 _ => {}
904 });
905
906 canvas.inner.on_drag_move = Some(on_drag_move);
907 canvas.inner.on_canvas_key_down = Some(on_canvas_key_down);
908 canvas.inner.render(core)
909}
910
911#[component]
917fn SeatGraphLayers<T: Clone + PartialEq + 'static>(
918 nodes: Signal<Vec<Node<T>>>,
919 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
920 on_node_click: Option<EventHandler<Id>>,
921 edge_view: Option<Callback<EdgeViewCtx, Element>>,
922 on_edge_click: Option<EventHandler<Id>>,
923) -> Element {
924 let core = use_context::<FlowCore>();
925 let anchors = use_memo(move || {
930 let geoms = core.geoms.read();
931 let frames: std::collections::BTreeMap<Id, Rect> = geoms
932 .iter()
933 .map(|geom| (geom.id.clone(), geom.rect))
934 .collect();
935 let links: Vec<crate::ports::Link> = core
936 .edges
937 .read()
938 .iter()
939 .map(|edge| crate::ports::Link {
940 id: edge.id.clone(),
941 start: crate::ports::Terminal::Node(edge.source.clone()),
942 end: crate::ports::Terminal::Node(edge.target.clone()),
943 start_seat: edge.source_seat,
944 end_seat: edge.target_seat,
945 })
946 .collect();
947 crate::ports::solve_ports(&frames, &links)
948 });
949
950 let adapted_edge_view = edge_view.map(|view| {
954 Callback::new(move |ctx: crate::edge::SeatEdgeViewCtx| {
955 view.call(EdgeViewCtx {
956 edge: ctx.edge.clone(),
957 source: ctx.anchors.start.point(),
958 source_side: ctx.anchors.start.side(),
959 target: ctx.anchors.end.point(),
960 target_side: ctx.anchors.end.side(),
961 path: crate::path::EdgePath {
962 d: ctx.geometry.path.clone(),
963 label: ctx.geometry.label,
964 },
965 marker_end: None,
967 })
968 })
969 });
970
971 let edges = core.edges;
972 let solved = anchors.read();
973 rsx! {
974 crate::edge::SeatEdges {
975 edges,
976 anchors,
977 edge_view: adapted_edge_view,
978 on_edge_click,
979 }
980 crate::edge::SeatEdgeLabels { edges, anchors }
981 NodesLayer { nodes, node_view, on_node_click }
982 svg { class: "df-edges df-ports", "aria-hidden": "true",
984 for edge in edges.read().iter() {
985 if let Some(pair) = solved.get(&edge.id) {
986 g {
987 key: "{edge.id}",
988 class: if edge.selected { "df-selected" },
989 circle {
990 class: "df-port",
991 cx: pair.start.x,
992 cy: pair.start.y,
993 r: crate::ports::PORT_RADIUS,
994 }
995 circle {
996 class: "df-port",
997 cx: pair.end.x,
998 cy: pair.end.y,
999 r: crate::ports::PORT_RADIUS,
1000 }
1001 }
1002 }
1003 }
1004 }
1005 }
1006}
1007
1008#[component]
1011fn ViewportPane(children: Element) -> Element {
1012 let core = use_context::<FlowCore>();
1013 let vp = *core.viewport.read();
1014 rsx! {
1015 div {
1016 class: "df-viewport",
1017 style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
1018 {children}
1019 }
1020 }
1021}
1022
1023#[component]
1027pub fn WorldLayer(class: Option<String>, children: Element) -> Element {
1028 let core = use_context::<FlowCore>();
1029 let vp = *core.viewport.read();
1030 let class = format!(
1031 "df-world-layer{}",
1032 class
1033 .as_deref()
1034 .map(|c| format!(" {c}"))
1035 .unwrap_or_default()
1036 );
1037 rsx! {
1038 div {
1039 class,
1040 style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
1041 {children}
1042 }
1043 }
1044}
1045
1046#[component]
1054fn NodesLayer<T: Clone + PartialEq + 'static>(
1055 nodes: Signal<Vec<Node<T>>>,
1056 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
1057 on_node_click: Option<EventHandler<Id>>,
1058) -> Element {
1059 let core = use_context::<FlowCore>();
1060 let tiles = use_memo(move || {
1061 let nodes = nodes.read();
1062 let grabbed = (*core.interaction.read() == Interaction::DragNode)
1069 .then(|| core.drag.peek().grabs.clone());
1070 crate::tile::tiles(nodes.iter().map(|node| {
1071 let raised = node.selected
1072 || grabbed
1073 .as_ref()
1074 .is_some_and(|grabs| grabs.iter().any(|(id, _)| id == &node.id));
1075 (node.rect(), raised)
1076 }))
1077 });
1078 let all = nodes.read();
1080 rsx! {
1081 div { class: "df-nodes",
1082 for tile in tiles.read().iter() {
1083 div {
1084 key: "{tile.cell.0},{tile.cell.1}",
1085 class: if tile.raised { "df-tile df-raised" } else { "df-tile" },
1086 style: tile_style(tile),
1087 for node in tile.members.iter().filter_map(|&i| all.get(i)) {
1088 NodeItem::<T> {
1089 key: "{node.id}",
1090 nodes,
1091 node: node.clone(),
1092 origin: tile.origin(),
1093 node_view,
1094 on_node_click,
1095 }
1096 }
1097 }
1098 }
1099 }
1100 }
1101}
1102
1103fn tile_style(tile: &crate::tile::Tile) -> String {
1109 format!(
1110 "transform:translate({}px,{}px);width:{}px;height:{}px;\
1111 contain-intrinsic-size:{}px {}px;",
1112 tile.rect.x,
1113 tile.rect.y,
1114 tile.rect.width,
1115 tile.rect.height,
1116 tile.rect.width,
1117 tile.rect.height,
1118 )
1119}
1120
1121#[component]
1122fn EdgesLayer(
1123 edge_view: Option<Callback<EdgeViewCtx, Element>>,
1124 on_edge_click: Option<EventHandler<Id>>,
1125) -> Element {
1126 let core = use_context::<FlowCore>();
1127 let edges = core.edges.read();
1128 let geoms = core.geoms.read();
1129 let handles = core.handles.read();
1130 let geom_by_id: HashMap<&str, &NodeGeom> =
1131 geoms.iter().map(|geom| (geom.id.as_str(), geom)).collect();
1132 let handle_idx: HashMap<(&str, HandleKind, &str), &crate::types::HandleGeom> = handles
1136 .iter()
1137 .map(|(key, geom)| ((key.node.as_str(), key.kind, key.id.as_str()), geom))
1138 .collect();
1139 let anchor = |geom: &NodeGeom, kind: HandleKind, handle_id: &Option<Id>| {
1140 let key = (geom.id.as_str(), kind, handle_id.as_deref().unwrap_or(""));
1141 crate::state::anchor_from_geom(handle_idx.get(&key).copied(), geom, kind)
1142 };
1143
1144 let items: Vec<_> = edges
1145 .iter()
1146 .filter_map(|edge| {
1147 let source_geom = geom_by_id.get(edge.source.as_str())?;
1148 let target_geom = geom_by_id.get(edge.target.as_str())?;
1149 let (source, source_side, source_on_handle) =
1150 anchor(source_geom, HandleKind::Source, &edge.source_handle);
1151 let (target, target_side, target_on_handle) =
1152 anchor(target_geom, HandleKind::Target, &edge.target_handle);
1153 let source = if source_on_handle {
1156 source + source_side.normal() * HANDLE_RIM
1157 } else {
1158 source
1159 };
1160 let target = if target_on_handle {
1161 target + target_side.normal() * HANDLE_RIM
1162 } else {
1163 target
1164 };
1165 Some((
1166 edge.clone(),
1167 source,
1168 source_side,
1169 target,
1170 target_side,
1171 source_geom.rect,
1172 target_geom.rect,
1173 ))
1174 })
1175 .collect();
1176
1177 let tiles = crate::tile::tiles(items.iter().map(|item| {
1183 let (edge, source, source_side, target, target_side, source_rect, target_rect) = item;
1184 let geo = crate::path::EdgeGeometry::new(*source, *source_side, *target, *target_side)
1185 .with_rects(*source_rect, *target_rect);
1186 (crate::path::edge_bounds(edge.kind, &geo), edge.selected)
1187 }));
1188
1189 rsx! {
1190 div { class: "df-edges", "aria-hidden": "true",
1193 svg { class: "df-edge-defs",
1196 defs { EdgeMarkers { iid: core.iid } }
1197 }
1198 for tile in tiles.iter() {
1199 svg {
1200 key: "{tile.cell.0},{tile.cell.1}",
1201 class: if tile.raised { "df-edge-tile df-raised" } else { "df-edge-tile" },
1202 view_box: "{tile.rect.x} {tile.rect.y} {tile.rect.width} {tile.rect.height}",
1206 style: tile_style(tile),
1207 for &i in tile.members.iter() {
1208 if let Some((edge, source, source_side, target, target_side, source_rect, target_rect)) = items.get(i) {
1209 EdgeItem {
1210 key: "{edge.id}",
1211 edge: edge.clone(),
1212 source: *source,
1213 source_side: *source_side,
1214 target: *target,
1215 target_side: *target_side,
1216 source_rect: *source_rect,
1217 target_rect: *target_rect,
1218 edge_view,
1219 on_edge_click,
1220 }
1221 }
1222 }
1223 }
1224 }
1225 }
1226 }
1227}
1228
1229#[component]
1231fn ConnectionLine() -> Element {
1232 let core = use_context::<FlowCore>();
1233 let connection = core.connection.read();
1234 let Some(conn) = connection.as_ref() else {
1235 return rsx! {};
1236 };
1237 let Some((from, from_side)) = core.anchor_of(&conn.from) else {
1238 return rsx! {};
1239 };
1240 let (to, to_side) = match &conn.snap {
1241 Some(snap) => (snap.point, Some(snap.side)),
1242 None => (conn.cursor, None),
1243 };
1244 let d = connection_path(from, from_side, to, to_side);
1245 rsx! {
1246 svg { class: "df-connection",
1247 path { class: "df-connection-path", d }
1248 }
1249 }
1250}
1251
1252pub(crate) fn deselect_edges(mut edges: Signal<Vec<Edge>>) {
1253 if edges.peek().iter().any(|e| e.selected) {
1254 edges.with_mut(|edges| {
1255 for edge in edges.iter_mut() {
1256 edge.selected = false;
1257 }
1258 });
1259 }
1260}
1261
1262fn add_edge_for_connection(mut edges: Signal<Vec<Edge>>, conn: Connection) {
1265 let duplicate = edges.peek().iter().any(|e| {
1266 e.source == conn.source
1267 && e.target == conn.target
1268 && e.source_handle == conn.source_handle
1269 && e.target_handle == conn.target_handle
1270 });
1271 if duplicate {
1272 return;
1273 }
1274 let mut edge = conn.into_edge();
1275 let base = edge.id.clone();
1276 let mut n = 2;
1277 while edges.peek().iter().any(|e| e.id == edge.id) {
1278 edge.id = format!("{base}-{n}");
1279 n += 1;
1280 }
1281 edges.with_mut(|edges| edges.push(edge));
1282}
1283
1284fn delete_request<T: Clone + PartialEq + 'static>(
1286 nodes: Signal<Vec<Node<T>>>,
1287 edges: Signal<Vec<Edge>>,
1288) -> DeleteRequest {
1289 let removed: std::collections::HashSet<Id> = nodes
1290 .peek()
1291 .iter()
1292 .filter(|n| n.selected)
1293 .map(|n| n.id.clone())
1294 .collect();
1295 let edge_ids = edges
1296 .peek()
1297 .iter()
1298 .filter(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target))
1299 .map(|e| e.id.clone())
1300 .collect();
1301 DeleteRequest {
1302 nodes: removed.into_iter().collect(),
1303 edges: edge_ids,
1304 }
1305}
1306
1307pub(crate) fn delete_selected<T: Clone + PartialEq + 'static>(
1308 mut nodes: Signal<Vec<Node<T>>>,
1309 mut edges: Signal<Vec<Edge>>,
1310) {
1311 let removed: std::collections::HashSet<Id> = nodes
1312 .peek()
1313 .iter()
1314 .filter(|n| n.selected)
1315 .map(|n| n.id.clone())
1316 .collect();
1317 let any_edges = edges
1318 .peek()
1319 .iter()
1320 .any(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target));
1321 if !removed.is_empty() {
1322 nodes.with_mut(|nodes| nodes.retain(|n| !n.selected));
1323 }
1324 if any_edges {
1325 edges.with_mut(|edges| {
1326 edges.retain(|e| {
1327 !e.selected && !removed.contains(&e.source) && !removed.contains(&e.target)
1328 })
1329 });
1330 }
1331}