Skip to main content

dioxus_flow/
flow.rs

1//! The [`Canvas`] and [`Flow`] components: canvas, pan/zoom, pointer state
2//! machine, and the node/edge render layers.
3//!
4//! [`Canvas`] is the lower layer: a pannable/zoomable surface with the shared
5//! [`FlowCore`] context, the pane gesture state machine, and nothing drawn on
6//! it. [`Flow`] builds the node/edge layers, connection gesture defaults and
7//! keyboard handling on top. Applications with their own node/edge rendering
8//! (custom editors, seat-based ports…) can use [`Canvas`] directly and draw
9//! into its `world` slot.
10
11use 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
31/// The component styles [`Canvas`] injects at runtime, public so that tests
32/// and server-side renderers can install the same sheet themselves.
33pub 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
41/// How a wheel notch converts to pixels, per delta unit. A notch reports
42/// lines on Firefox and pixels on Chromium; without the conversion one notch
43/// moves the canvas by three pixels.
44fn 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
55/// Exponent per scrolled pixel while pinch-zooming (ctrl/meta + wheel, which
56/// is also what browsers report for a trackpad pinch).
57const PINCH_ZOOM_SENSITIVITY: f64 = 0.0025;
58
59/// The pannable/zoomable surface every flow is drawn on.
60///
61/// Owns the [`FlowCore`] context, the container geometry, and the pane
62/// gestures: pan (drag or scroll), zoom (scroll or pinch), pane clicks, and
63/// the in-flight connection state machine that [`crate::Handle`]s feed.
64/// Draws nothing itself: [`Flow`] passes its node/edge layers through the
65/// `world` slot, and an application using [`Canvas`] directly renders its own
66/// content there (in flow coordinates) and overlays as `children` (in screen
67/// coordinates).
68///
69/// An application-level gesture that starts on content inside the canvas can
70/// take the pointer away from the pane with [`FlowCore::claim_pointer`]; the
71/// pane then neither pans nor reports a pane click for that press.
72#[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    /// Pan the canvas by dragging empty space.
78    #[props(default = true)]
79    pan_on_drag: bool,
80    /// Zoom with the mouse wheel / trackpad.
81    #[props(default = true)]
82    zoom_on_scroll: bool,
83    /// Scrolling pans instead of zooming (shift swaps the axis, ctrl/meta —
84    /// a trackpad pinch included — zooms about the pointer). Takes precedence
85    /// over `zoom_on_scroll`.
86    #[props(default = false)]
87    pan_on_scroll: bool,
88    /// Master switch for node dragging (read by [`Flow`]'s node layer).
89    #[props(default = true)]
90    nodes_draggable: bool,
91    /// How far (screen px) a press on a node must travel before it moves the
92    /// node, so a sloppy click never nudges one.
93    #[props(default = 0.0)]
94    drag_threshold: f64,
95    /// Snap radius (screen px) for completing a connection near a handle.
96    #[props(default = 28.0)]
97    connection_radius: f64,
98    #[props(default = 0.12)] fit_view_padding: f64,
99    /// `id` attribute for the root element, so applications can find, focus,
100    /// measure, or capture pointers to the canvas by id.
101    id: Option<String>,
102    /// Accessible name for the canvas.
103    #[props(default = "Node graph".to_string())]
104    aria_label: String,
105    /// Extra classes for the root element.
106    class: Option<String>,
107    /// A primary press reaching the pane, before the pane decides to pan:
108    /// the hook for application-level pane gestures (marquee selection,
109    /// pulling a connection from a node border…). A handler that starts one
110    /// claims the pointer with [`FlowCore::claim_pointer`]; the pane then
111    /// leaves this press alone.
112    on_pane_press: Option<Callback<Event<PointerData>>>,
113    /// The caller's edges, when it has any (the [`Flow`] layers and the
114    /// default connect behavior read and write these through the core).
115    edges: Option<Signal<Vec<Edge>>>,
116    /// Node geometry snapshot, when the caller renders nodes ([`Flow`] passes
117    /// its memo; standalone canvases leave it empty).
118    geoms: Option<Memo<Vec<NodeGeom>>>,
119    /// Type-erased "deselect all nodes" for pane clicks and edge selection,
120    /// provided by [`Flow`] which knows the node type.
121    deselect_nodes: Option<Callback<()>>,
122    /// Called when the user completes a connection between two handles. When
123    /// absent, the edge is added to `edges` automatically.
124    on_connect: Option<EventHandler<Connection>>,
125    /// A connection drag has left a handle (started, not completed).
126    on_connect_start: Option<EventHandler<HandleKey>>,
127    /// A connection drag ended — wherever it ended. `connection` is `None`
128    /// when the release was over nothing, and the point says where: the hook
129    /// for "drop on empty canvas to create the node there".
130    on_connect_end: Option<EventHandler<ConnectEnd>>,
131    /// The application's say over which connections may complete. A target
132    /// that fails is never offered as a snap and never completes.
133    is_valid_connection: Option<Callback<Connection, bool>>,
134    /// A node drag actually began (the press travelled past
135    /// `drag_threshold`), with the ids being dragged: the moment to snapshot
136    /// for undo.
137    on_node_drag_start: Option<EventHandler<Vec<Id>>>,
138    /// A node drag ended, with the ids that were dragged. Positions are
139    /// already final in the node list: the moment to snap, settle, persist.
140    on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
141    /// Click on empty canvas; the point is in flow coordinates. Fires only
142    /// when the press neither travelled nor was claimed by content.
143    on_pane_click: Option<EventHandler<Point>>,
144    /// Double-click on the canvas; the point is in flow coordinates. The
145    /// pane cannot tell content from paper here — an application that must
146    /// can hit-test the client point itself before acting.
147    on_pane_double_click: Option<EventHandler<Point>>,
148    /// Keyboard events reaching the canvas root, after the canvas's own
149    /// Escape handling. [`Flow`] wires Delete/Backspace through this.
150    on_canvas_key_down: Option<Callback<Event<KeyboardData>>>,
151    /// Pointer moves while a node drag is in flight, in flow coordinates.
152    /// [`Flow`] applies the drag to its typed node list through this.
153    on_drag_move: Option<Callback<Point>>,
154    /// Content drawn inside the viewport transform, in flow coordinates.
155    world: Option<Element>,
156    /// Overlays drawn over the canvas in screen coordinates ([`Background`],
157    /// [`Controls`], [`MiniMap`], or your own — they can call [`use_flow`]).
158    ///
159    /// [`Background`]: crate::Background
160    /// [`Controls`]: crate::Controls
161    /// [`MiniMap`]: crate::MiniMap
162    /// [`use_flow`]: crate::use_flow
163    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    // Mirror config props into the shared config signal.
218    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    // Container geometry tracking.
234    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    // ---- Pointer state machine ----------------------------------------
251
252    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        // A node, handle, edge or overlay may have claimed this pointer
271        // already (children's handlers run first while bubbling).
272        if *interaction.peek() != Interaction::None {
273            return;
274        }
275        if evt.trigger_button() != Some(MouseButton::Primary) {
276            return;
277        }
278        // Offer the press to the application first; it may claim the pointer
279        // for a gesture of its own, in which case the pane stays out of it.
280        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        // A gesture belongs to the pointer that started it: a second finger
311        // must not steer the first one's pan.
312        if drag
313            .peek()
314            .pointer_id
315            .is_some_and(|id| id != evt.pointer_id())
316        {
317            return;
318        }
319        // Self-heal: if the pointer was released outside the container we
320        // never saw the pointerup.
321        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                // The press has to travel before it moves anything, so a
342                // sloppy click never nudges a node. Crossing the threshold is
343                // the moment the drag really starts — the snapshot-for-undo
344                // moment — so that is when `on_node_drag_start` fires.
345                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            // A click on empty canvas (no pan movement happened).
400            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                    // However it ended: the release point plus what (if
430                    // anything) completed. A `None` connection with a point is
431                    // the drop-on-empty-canvas hook.
432                    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        // A pinch (or ctrl/meta scroll) zooms about the pointer in either
464        // scroll mode.
465        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            // Shift turns a vertical wheel into horizontal travel, as
476            // everywhere else.
477            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    // Reading `interaction` here keeps cursor feedback classes fresh; it only
502    // changes on gesture start/end, never per pointer-move frame.
503    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/// An interactive node-graph canvas, in the spirit of react-flow.
555///
556/// Nodes and edges are owned by the caller as signals; the flow mutates them
557/// in response to user interaction (dragging, selection, connecting) and the
558/// caller can mutate them at any time (adding nodes, changing data…).
559///
560/// ```ignore
561/// let nodes = use_signal(|| vec![
562///     Node::new("1", "Input", (0.0, 0.0)).node_type("input"),
563///     Node::new("2", "Process", (0.0, 120.0)),
564/// ]);
565/// let edges = use_signal(|| vec![Edge::new("1", "2").animated(true)]);
566/// rsx! {
567///     Flow { nodes, edges, fit_view: true,
568///         Background {}
569///         Controls {}
570///         MiniMap {}
571///     }
572/// }
573/// ```
574#[component]
575pub fn Flow<T: Clone + PartialEq + 'static>(
576    /// The nodes, owned by the caller.
577    nodes: Signal<Vec<Node<T>>>,
578    /// The edges, owned by the caller.
579    edges: Signal<Vec<Edge>>,
580    /// How edges find their endpoints: [`AnchorMode::Handles`] (default) or
581    /// [`AnchorMode::Seats`] — solver-packed positions around each node's rim,
582    /// drawn with rim-aware curves and beads.
583    #[props(default)]
584    anchor: AnchorMode,
585    #[props(default = 0.25)] min_zoom: f64,
586    #[props(default = 4.0)] max_zoom: f64,
587    /// Pan the canvas by dragging empty space.
588    #[props(default = true)]
589    pan_on_drag: bool,
590    /// Zoom with the mouse wheel / trackpad.
591    #[props(default = true)]
592    zoom_on_scroll: bool,
593    /// Scrolling pans instead of zooming (ctrl/meta or a pinch zooms).
594    #[props(default = false)]
595    pan_on_scroll: bool,
596    /// Master switch for node dragging (individual nodes can also opt out).
597    #[props(default = true)]
598    nodes_draggable: bool,
599    /// How far (screen px) a press on a node must travel before it moves the
600    /// node, so a sloppy click never nudges one.
601    #[props(default = 0.0)]
602    drag_threshold: f64,
603    /// Snap radius (screen px) for completing a connection near a handle.
604    #[props(default = 28.0)]
605    connection_radius: f64,
606    /// Fit all nodes into view once nodes are measured after mount.
607    #[props(default = false)]
608    fit_view: bool,
609    #[props(default = 0.12)] fit_view_padding: f64,
610    /// Delete selected nodes/edges with Delete/Backspace.
611    #[props(default = true)]
612    delete_key: bool,
613    /// `id` attribute for the root element.
614    id: Option<String>,
615    /// Extra classes for the root element.
616    class: Option<String>,
617    /// Custom renderer for node contents. Receives a [`NodeViewCtx`]; fall
618    /// back to [`crate::DefaultNodeView`] for types you don't customize.
619    node_view: Option<Callback<NodeViewCtx<T>, Element>>,
620    /// Custom renderer for edges (SVG content).
621    edge_view: Option<Callback<EdgeViewCtx, Element>>,
622    /// Called when the user completes a connection between two handles. When
623    /// absent, the edge is added automatically.
624    on_connect: Option<EventHandler<Connection>>,
625    /// A connection drag has left a handle (started, not completed).
626    on_connect_start: Option<EventHandler<HandleKey>>,
627    /// A connection drag ended — wherever it ended. `connection` is `None`
628    /// when the release was over nothing, and the point says where: the hook
629    /// for "drop on empty canvas to create the node there".
630    on_connect_end: Option<EventHandler<ConnectEnd>>,
631    /// The application's say over which connections may complete. A target
632    /// that fails is never offered as a snap and never completes.
633    is_valid_connection: Option<Callback<Connection, bool>>,
634    /// A node drag actually began (the press travelled past
635    /// `drag_threshold`), with the ids being dragged: the moment to snapshot
636    /// for undo.
637    on_node_drag_start: Option<EventHandler<Vec<Id>>>,
638    /// A node drag ended, with the ids that were dragged. Positions are
639    /// already final in the node list: the moment to snap, settle, persist.
640    on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
641    /// Called when Delete/Backspace is pressed with a selection. When absent,
642    /// the selection (plus connected edges) is deleted automatically; when
643    /// present, nothing is deleted — call
644    /// [`FlowHandle::delete_selected`](crate::FlowHandle::delete_selected)
645    /// from the handler to perform the default cascade (after confirming,
646    /// snapshotting for undo…).
647    on_delete: Option<EventHandler<DeleteRequest>>,
648    on_node_click: Option<EventHandler<Id>>,
649    on_edge_click: Option<EventHandler<Id>>,
650    /// Click on empty canvas; the point is in flow coordinates.
651    on_pane_click: Option<EventHandler<Point>>,
652    /// Double-click on the canvas; the point is in flow coordinates.
653    on_pane_double_click: Option<EventHandler<Point>>,
654    /// Attach a [`FlowHandle`] (from [`crate::use_flow_handle`]) for
655    /// programmatic control: fit view, zoom, auto-layout…
656    handle: Option<FlowHandle<T>>,
657    /// Overlays such as [`crate::Background`], [`crate::Controls`],
658    /// [`crate::MiniMap`], or your own (they can call [`crate::use_flow`]).
659    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    // Wired to the canvas once it mounts (the core is created inside it).
686    let attach_core: Signal<Option<FlowCore>> = use_signal(|| None);
687
688    // Attach the programmatic handle, if provided.
689    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    // Initial fit-view: wait until the container and all nodes are measured.
702    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/// The node layer sandwiched between seat-anchored edges and their beads.
799///
800/// One component so the three share one solve: the edge curves render under
801/// the nodes, but the beads — the dots where a connection meets a rim — sit
802/// over them, because a bead is threaded on the rim, not tucked behind it.
803#[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    // The one expensive step, behind a memo: re-solves when node geometry or
813    // the edge list changes, never on pan or zoom. Applications with their
814    // own gesture policy run this solve themselves and hand the result to
815    // [`SeatEdges`]; here the edges signal is the whole story.
816    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    // `Flow`'s `edge_view` speaks the handle-mode context; hand it the seat
838    // geometry through the same shape. Views that want the full rim-aware
839    // geometry use [`SeatEdges`] directly.
840    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                // Seat-mode arrowheads are drawn geometry, not markers.
853                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        // The beads, over the nodes they are threaded on.
870        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/// Hands the canvas's core out to the owning [`Flow`], which renders above
896/// the canvas and so cannot `use_context` it.
897#[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/// The pannable/zoomable transform layer. Isolated so per-frame viewport
908/// updates re-render only this tiny component, not the node/edge layers.
909#[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/// A layer inside the canvas that shares the viewport transform: children are
923/// laid out in flow coordinates. Render as a child of [`Canvas`] or [`Flow`]
924/// for world-space overlays (annotations, guides, custom edge layers…).
925#[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    // Borrowed lookup index: this layer re-renders every frame while a node
978    // is dragged, and going through `resolve_anchor` would clone two key
979    // Strings per edge per frame.
980    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            // End the visible path at the handle's rim instead of its center
999            // so arrowheads stay in front of the handle dot.
1000            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        // Decorative for assistive tech: nodes expose the graph's content,
1024        // and edge hit-paths are pointer-only.
1025        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/// The dashed preview while dragging a new connection from a handle.
1046#[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
1078/// Default behavior when no `on_connect` handler is given: add the edge,
1079/// skipping exact duplicates and de-duplicating the generated id.
1080fn 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
1100/// What a delete keypress would remove, given the current selection.
1101fn 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}