Skip to main content

iced_nodegraph/node_graph/
mod.rs

1//! Node graph widget and core types.
2//!
3//! This module provides the main [`NodeGraph`] widget for building interactive
4//! node-based editors. It handles rendering, user interaction, and event dispatch.
5//!
6//! ## Quick Start
7//!
8//! ```ignore
9//! use iced_nodegraph::{node_graph, PinRef};
10//!
11//! let mut ng = node_graph()
12//!     .on_connect(|from, to| Message::Connected { from, to })
13//!     .on_move(|delta, node_ids| Message::NodesMoved { delta, node_ids });
14//!
15//! ng.push_node(node(0, Point::new(100.0, 100.0), my_node_content));
16//! ng.push_edge(edge!(PinRef::new(0, 0), PinRef::new(1, 0)));
17//! ```
18//!
19//! ## Architecture
20//!
21//! - [`NodeGraph`] - The main widget container
22//! - [`PinRef`] - Type-safe reference to a pin (generic over ID types)
23//! - [`Camera2D`](camera::Camera2D) - Zoom and pan state management
24//!
25//! ## Event Handling
26//!
27//! Interaction is reported through individual callbacks: `on_connect()`,
28//! `on_move()`, `on_select()`, `on_clone()`, `on_delete()`.
29//! Move and select work without the app keeping its own model; the app receives
30//! data on commit. Live drag callbacks (`on_drag_start/update/end`) additionally
31//! report an in-progress drag so it can be observed as it happens.
32//!
33//! ## Styling
34//!
35//! Visual appearance is controlled per element through status-driven closures:
36//! - [`Node::style`] - per-node body style; [`Node::pin_style`] - the node's pins
37//! - [`Edge::style`] - per-edge style
38//! - [`NodeGraph::graph_style`] / [`NodeGraph::dragging_edge_style`] - graph chrome
39
40use std::collections::{HashMap, HashSet};
41use std::fmt::Debug;
42use std::hash::Hash;
43use std::time::Duration;
44
45use iced::{Length, Point, Size, Vector};
46
47use crate::ids::{EdgeId, NodeId, PinId};
48use crate::node_pin::{PinEnd, PinInfo};
49use crate::style::{EdgeStatus, EdgeStyle, GraphStyle, NodeStatus, NodeStyle, PinStatus, PinStyle};
50
51/// Per-node style callback: theme + status -> resolved style. Used by [`Node`].
52pub(crate) type NodeStyleFn<'a, Theme> = Box<dyn Fn(&Theme, NodeStatus) -> NodeStyle + 'a>;
53/// Per-edge style callback: theme + status + both endpoint pin infos (in draw
54/// order: start = output side, end = input side) -> resolved style. Used by
55/// [`Edge`].
56pub(crate) type EdgeStyleFn<'a, P, UI, Theme> =
57    Box<dyn Fn(&Theme, EdgeStatus, PinInfo<'_, P, UI>, PinInfo<'_, P, UI>) -> EdgeStyle + 'a>;
58/// Per-node pin style callback: theme + this pin's info + the other endpoint's
59/// info (the drag source during an edge drag, else `None`) + status -> resolved
60/// pin style. The node styles all of its pins (pins carry no style of their
61/// own). Used by [`Node::pin_style`].
62pub(crate) type PinStyleFn<'a, P, UI, Theme> = Box<
63    dyn Fn(&Theme, &PinInfo<'_, P, UI>, Option<&PinInfo<'_, P, UI>>, PinStatus) -> PinStyle + 'a,
64>;
65/// Drag-edge style callback: theme + the source pin's info -> resolved style. A
66/// freshly dragged edge has no status. Used by [`NodeGraph::dragging_edge_style`].
67pub(crate) type DragEdgeStyleFn<'a, P, UI, Theme> =
68    Box<dyn Fn(&Theme, PinInfo<'_, P, UI>) -> EdgeStyle + 'a>;
69
70/// A node to push onto the graph: id, position, content element, an optional
71/// per-node style closure, and an optional closure styling all of its pins.
72/// Build with [`node`] + [`Node::style`]/[`Node::pin_style`], then add via
73/// [`NodeGraph::push_node`]. Looks like its own widget even though the body and
74/// pins are drawn by the graph.
75pub struct Node<'a, N, P, UI, Message, Theme, Renderer> {
76    id: N,
77    position: Point,
78    element: iced::Element<'a, Message, Theme, Renderer>,
79    style_fn: Option<NodeStyleFn<'a, Theme>>,
80    pin_style_fn: Option<PinStyleFn<'a, P, UI, Theme>>,
81}
82
83/// Creates a [`Node`] with default (theme) styling.
84pub fn node<'a, N, P, UI, Message, Theme, Renderer>(
85    id: N,
86    position: Point,
87    element: impl Into<iced::Element<'a, Message, Theme, Renderer>>,
88) -> Node<'a, N, P, UI, Message, Theme, Renderer> {
89    Node {
90        id,
91        position,
92        element: element.into(),
93        style_fn: None,
94        pin_style_fn: None,
95    }
96}
97
98impl<'a, N, P, UI, Message, Theme, Renderer> Node<'a, N, P, UI, Message, Theme, Renderer> {
99    /// Sets the per-node style closure: receives the theme and the node's
100    /// [`NodeStatus`], returns the resolved style. Layer over the built-in
101    /// default:
102    /// ```ignore
103    /// node(0, pos, el).style(|theme, status| NodeStyle {
104    ///     fill_color: Color::WHITE.into(),
105    ///     ..default_node_style(theme, status)
106    /// })
107    /// ```
108    pub fn style(mut self, f: impl Fn(&Theme, NodeStatus) -> NodeStyle + 'a) -> Self {
109        self.style_fn = Some(Box::new(f));
110        self
111    }
112
113    /// Sets the closure that styles all of this node's pins: receives the theme,
114    /// this pin's [`PinInfo`] view (direction, user info, id), the other
115    /// endpoint's info (the drag source during an edge drag, else `None`) and
116    /// the pin's [`PinStatus`], returns the resolved pin style.
117    /// ```ignore
118    /// node(0, pos, el).pin_style(|theme, pin, other, status| PinStyle {
119    ///     color: color_for(pin.info()).into(),
120    ///     ..default_pin_style(theme, status)
121    /// })
122    /// ```
123    pub fn pin_style(
124        mut self,
125        f: impl Fn(&Theme, &PinInfo<'_, P, UI>, Option<&PinInfo<'_, P, UI>>, PinStatus) -> PinStyle + 'a,
126    ) -> Self {
127        self.pin_style_fn = Some(Box::new(f));
128        self
129    }
130}
131
132/// An edge to push onto the graph: a user id, endpoint pin references, and an
133/// optional per-edge status-driven style closure. Build with [`edge`] +
134/// [`Edge::style`], then add via [`NodeGraph::push_edge`]. The id is the user's
135/// own (e.g. a DB key); it travels with the edge, symmetric to [`node`].
136pub struct Edge<'a, N, P, E, UI, Theme> {
137    id: E,
138    from: PinRef<N, P>,
139    to: PinRef<N, P>,
140    style_fn: Option<EdgeStyleFn<'a, P, UI, Theme>>,
141}
142
143/// Creates an [`Edge`] with the given id and default (theme) styling.
144///
145/// The id comes last so the common no-id case reads cleanly via the `edge!`
146/// macro: `edge!(from, to)` expands to `edge(from, to, ())`.
147pub fn edge<'a, N, P, E, UI, Theme>(
148    from: PinRef<N, P>,
149    to: PinRef<N, P>,
150    id: E,
151) -> Edge<'a, N, P, E, UI, Theme> {
152    Edge {
153        id,
154        from,
155        to,
156        style_fn: None,
157    }
158}
159
160/// Builds an [`Edge`], defaulting the id to `()` when omitted.
161///
162/// ```ignore
163/// edge!(PinRef::new(0, 0), PinRef::new(1, 0))       // id = ()
164/// edge!(PinRef::new(0, 0), PinRef::new(1, 0), my_id) // id = my_id
165/// ```
166#[macro_export]
167macro_rules! edge {
168    ($from:expr, $to:expr $(,)?) => {
169        $crate::edge($from, $to, ())
170    };
171    ($from:expr, $to:expr, $id:expr $(,)?) => {
172        $crate::edge($from, $to, $id)
173    };
174}
175
176impl<'a, N, P, E, UI, Theme> Edge<'a, N, P, E, UI, Theme> {
177    /// Sets the per-edge style closure: theme, [`EdgeStatus`], and both endpoint
178    /// [`PinInfo`]s in draw order (start = output side, end = input side) ->
179    /// resolved style.
180    pub fn style(
181        mut self,
182        f: impl Fn(&Theme, EdgeStatus, PinInfo<'_, P, UI>, PinInfo<'_, P, UI>) -> EdgeStyle + 'a,
183    ) -> Self {
184        self.style_fn = Some(Box::new(f));
185        self
186    }
187}
188
189pub mod camera;
190pub(crate) mod euclid;
191pub(crate) mod input;
192pub(crate) mod state;
193pub(crate) mod widget;
194
195/// Shared per-frame rendering context for all primitives.
196#[derive(Debug, Clone, Copy)]
197pub(crate) struct RenderContext {
198    pub camera_zoom: f32,
199    pub camera_position: euclid::WorldPoint,
200    /// Screen-space top-left of the widget within the window. SDF screen
201    /// mapping must offset by this so layers align with Iced content when the
202    /// graph is not at the window origin (e.g. below a toolbar).
203    pub viewport_origin: euclid::ScreenVector,
204    pub time: f32,
205}
206
207/// Counts for one element kind in a frame: how many exist, how many are in view,
208/// and how many were culled (off-screen). `total == in_view + culled`.
209#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
210pub struct Counts {
211    /// Total elements of this kind in the graph.
212    pub total: usize,
213    /// Elements whose screen bounds intersect the viewport.
214    pub in_view: usize,
215    /// Elements fully off-screen.
216    pub culled: usize,
217}
218
219/// One timed slice of the per-frame CPU work, in the order it runs.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct OpTiming {
222    /// Stable label of the operation (e.g. `"geometry"`, `"edges"`).
223    pub label: &'static str,
224    /// CPU time the operation took this frame.
225    pub duration: Duration,
226}
227
228/// Per-frame diagnostics for the graph, delivered to [`NodeGraph::on_info`].
229///
230/// `nodes`/`pins`/`edges` are [`Counts`]; `timings` is the CPU cost of each draw
231/// operation in stack order (geometry, background, foreground, sdf prepare) and
232/// sums to roughly the per-frame CPU time. `sdf_entries`/`sdf_tiles` are the
233/// SDF pipeline counters. All timings are CPU-side; no GPU profiling is done.
234///
235/// Reported one frame behind: the values are measured during `draw` and
236/// delivered on the next redraw, mirroring the controlled `on_pan` pattern.
237#[derive(Debug, Clone, PartialEq)]
238pub struct GraphInfo {
239    /// Node counts (total / in view / culled).
240    pub nodes: Counts,
241    /// Pin counts across all nodes.
242    pub pins: Counts,
243    /// Edge counts.
244    pub edges: Counts,
245    /// Per-operation CPU timings, in stack order.
246    pub timings: Vec<OpTiming>,
247    /// SDF draw entries submitted this frame.
248    pub sdf_entries: u32,
249    /// SDF tiles the index covered this frame.
250    pub sdf_tiles: u32,
251}
252
253/// Identifies what an in-progress drag is moving. Delivered to the
254/// [`on_drag_start`](NodeGraph::on_drag_start) callback so the app can observe a
255/// drag live (e.g. to broadcast it), alongside the commit-on-drop callbacks.
256///
257/// Ids are the user's own node/pin id types (`N`/`P`), matching the rest of the
258/// callback API (e.g. [`PinRef`]); both default to `usize`.
259#[derive(Debug, Clone)]
260pub enum DragInfo<N = usize, P = usize> {
261    /// Dragging a single node.
262    Node { node_id: N },
263    /// Dragging a group of selected nodes.
264    Group { node_ids: Vec<N> },
265    /// Dragging an edge from a pin (the source node and pin).
266    Edge { from_node: N, from_pin: P },
267    /// Box selection drag, anchored at this world-space corner.
268    BoxSelect { start_x: f32, start_y: f32 },
269}
270
271/// Type-safe reference to a pin: a `node_id` paired with a `pin_id`, generic over
272/// your id types.
273///
274/// The fields are public by design. `PinRef` is a transparent id pair with no
275/// invariants to uphold: any node/pin id combination is structurally valid, and
276/// whether two pins may actually connect is decided elsewhere (e.g. via
277/// [`can_connect`](NodeGraph::can_connect)). Build it with a struct literal or
278/// [`PinRef::new`], and match or destructure it freely.
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
280pub struct PinRef<N, P> {
281    /// The node's user id.
282    pub node_id: N,
283    /// The pin's user id within its node.
284    pub pin_id: P,
285}
286
287impl<N: Clone, P: Clone> PinRef<N, P> {
288    /// Creates a pin reference from a node id and a pin id.
289    pub fn new(node_id: N, pin_id: P) -> Self {
290        Self { node_id, pin_id }
291    }
292}
293
294/// Node graph widget with generic ID types.
295///
296/// # Type Parameters
297/// - `N`: Node ID type (defaults to `usize`)
298/// - `P`: Pin ID type (defaults to `usize`)
299/// - `Message`: Application message type
300/// - `Theme`: Iced theme type (defaults to `iced::Theme`)
301/// - `Renderer`: Iced renderer type (defaults to `iced::Renderer`)
302///
303/// Users can provide their own ID types by implementing [`NodeId`], [`PinId`]
304/// and [`EdgeId`].
305#[allow(missing_debug_implementations)]
306pub struct NodeGraph<
307    'a,
308    N = usize,
309    P = usize,
310    UI = (),
311    Message = (),
312    Theme = iced::Theme,
313    Renderer = iced::Renderer,
314    E = (),
315> where
316    N: NodeId,
317    P: PinId,
318    E: EdgeId,
319{
320    pub(super) size: Size<Length>,
321    /// Nodes with position, element, and config overrides.
322    /// Config fields set to Some() override theme defaults.
323    /// None fields use `default_node_style()` values at render time.
324    pub(super) nodes: Vec<(
325        N,
326        Point,
327        iced::Element<'a, Message, Theme, Renderer>,
328        Option<NodeStyleFn<'a, Theme>>,
329        Option<PinStyleFn<'a, P, UI, Theme>>,
330    )>,
331    /// Id -> index map backing `node_index`: O(1) lookups and deterministic
332    /// duplicate detection in `push_node` (first push wins).
333    node_lookup: HashMap<N, usize>,
334    /// Edges with user-defined pin references and config overrides.
335    /// Pin IDs are resolved to local indices at render time.
336    /// Config fields set to Some() override theme defaults.
337    /// None fields use `default_edge_style()` values at render time.
338    pub(super) edges: Vec<(
339        E,
340        PinRef<N, P>,
341        PinRef<N, P>,
342        Option<EdgeStyleFn<'a, P, UI, Theme>>,
343    )>,
344    graph_style: Option<Box<dyn Fn(&Theme) -> GraphStyle + 'a>>,
345    on_connect: Option<Box<dyn Fn(PinRef<N, P>, PinRef<N, P>) -> Message + 'a>>,
346    on_disconnect: Option<Box<dyn Fn(PinRef<N, P>, PinRef<N, P>) -> Message + 'a>>,
347    on_move: Option<Box<dyn Fn(Vector, Vec<N>) -> Message + 'a>>,
348    on_select: Option<Box<dyn Fn(Vec<N>) -> Message + 'a>>,
349    on_clone: Option<Box<dyn Fn(Vec<N>) -> Message + 'a>>,
350    on_delete: Option<Box<dyn Fn(Vec<N>) -> Message + 'a>>,
351    /// External selection using internal indices.
352    /// Populated by `selection()` method which converts user IDs to indices.
353    external_selection: Option<HashSet<usize>>,
354    // Live drag callbacks: fire continuously during a drag (start/update/end),
355    // in addition to the commit-on-drop on_move. They make live
356    // observation of an in-progress drag possible (e.g. collaborative broadcast),
357    // which is the app's concern, not the widget's.
358    on_drag_start: Option<Box<dyn Fn(DragInfo<N, P>) -> Message + 'a>>,
359    on_drag_update: Option<Box<dyn Fn(Point) -> Message + 'a>>,
360    on_drag_end: Option<Box<dyn Fn() -> Message + 'a>>,
361    /// Commit callback for pan/zoom: fires with the new camera (position, zoom)
362    /// when the user finishes a pan drag or zooms. The host stores it and feeds
363    /// it back via `view()`, mirroring `on_move` / `selection`.
364    on_pan: Option<Box<dyn Fn(Point, f32) -> Message + 'a>>,
365    /// Per-frame diagnostics callback (element counts + CPU op timings).
366    on_info: Option<Box<dyn Fn(GraphInfo) -> Message + 'a>>,
367    /// Style callback for box selection overlay.
368    /// Returns (fill_color, border_color).
369    pub(super) box_select_style_fn: Option<Box<dyn Fn(&Theme) -> (iced::Color, iced::Color) + 'a>>,
370    /// Style callback for edge cutting tool overlay.
371    /// Returns the line color.
372    pub(super) cutting_tool_style_fn: Option<Box<dyn Fn(&Theme) -> iced::Color + 'a>>,
373    /// Style for the edge being dragged (theme -> resolved style). The graph
374    /// injects the source pin's color for inheriting (TRANSPARENT) stroke ends.
375    pub(super) dragging_edge_style_fn: Option<DragEdgeStyleFn<'a, P, UI, Theme>>,
376    /// Host-controlled camera (world position + zoom). The widget syncs its
377    /// internal camera to this whenever the host changes it, while still running
378    /// pan/zoom interaction internally and committing via `on_pan`. Mirrors the
379    /// `selection()` / `on_select` controlled pattern.
380    pub(super) view: Option<(Point, f32)>,
381    /// Custom validation callback for pin connection compatibility.
382    /// When set, it is authoritative in `compute_valid_targets` (the built-in
383    /// direction check only applies as the default when this is unset).
384    pub(super) can_connect:
385        Option<Box<dyn Fn(PinEnd<'_, N, P, UI>, PinEnd<'_, N, P, UI>) -> bool + 'a>>,
386    /// Key and pointer bindings; platform defaults unless overridden via
387    /// [`keymap`](Self::keymap).
388    pub(super) keymap: input::Keymap,
389}
390
391impl<N, P, E, UI, Message, Theme, Renderer> Default
392    for NodeGraph<'_, N, P, UI, Message, Theme, Renderer, E>
393where
394    N: NodeId,
395    P: PinId,
396    E: EdgeId,
397    Renderer: iced_wgpu::core::renderer::Renderer,
398{
399    fn default() -> Self {
400        Self {
401            size: Size::new(Length::Fill, Length::Fill),
402            nodes: Vec::new(),
403            node_lookup: HashMap::new(),
404            edges: Vec::new(),
405            graph_style: None,
406            on_connect: None,
407            on_disconnect: None,
408            on_move: None,
409            on_select: None,
410            on_clone: None,
411            on_delete: None,
412            external_selection: None,
413            on_drag_start: None,
414            on_drag_update: None,
415            on_drag_end: None,
416            on_pan: None,
417            on_info: None,
418            box_select_style_fn: None,
419            cutting_tool_style_fn: None,
420            dragging_edge_style_fn: None,
421            view: None,
422            can_connect: None,
423            keymap: input::Keymap::default(),
424        }
425    }
426}
427
428impl<'a, N, P, E, UI, Message, Theme, Renderer> NodeGraph<'a, N, P, UI, Message, Theme, Renderer, E>
429where
430    N: NodeId + 'static,
431    P: PinId + 'static,
432    E: EdgeId + 'static,
433    Renderer: iced_wgpu::core::renderer::Renderer,
434{
435    /// Sets the host-controlled camera (world position + zoom).
436    ///
437    /// The widget snaps its camera to this whenever the host changes the value,
438    /// while still running pan/zoom interaction internally and committing through
439    /// [`on_pan`](Self::on_pan). This is the controlled-component counterpart to
440    /// `on_pan`, exactly like `selection()` is to `on_select`: feed back what
441    /// `on_pan` reports and the view stays in sync; push a new value (e.g. a reset
442    /// to origin) and the view snaps there.
443    pub fn view(mut self, position: Point, zoom: f32) -> Self {
444        self.view = Some((position, zoom));
445        self
446    }
447
448    /// Adds a node with the given ID and default styling.
449    ///
450    /// The node will use theme defaults from `default_node_style()`.
451    ///
452    /// Node IDs must be unique: a duplicate push is ignored (the first node
453    /// with the id wins), and debug builds assert on it. Prefer a stable id
454    /// from your data (a DB key, `uuid::Uuid`, a typed newtype) over a
455    /// hand-managed counter.
456    pub fn push_node(&mut self, node: Node<'a, N, P, UI, Message, Theme, Renderer>) {
457        match self.node_lookup.entry(node.id.clone()) {
458            std::collections::hash_map::Entry::Occupied(_) => {
459                debug_assert!(
460                    false,
461                    "duplicate node id {:?}: node ids must be unique; \
462                     the duplicate push is ignored (first wins)",
463                    node.id,
464                );
465            }
466            std::collections::hash_map::Entry::Vacant(entry) => {
467                entry.insert(self.nodes.len());
468                self.nodes.push((
469                    node.id,
470                    node.position,
471                    node.element,
472                    node.style_fn,
473                    node.pin_style_fn,
474                ));
475            }
476        }
477    }
478
479    /// Adds an edge to the graph.
480    ///
481    /// Pin IDs are resolved to local indices at render time; the widget
482    /// normalizes orientation so the output pin is the edge start (output ->
483    /// input).
484    pub fn push_edge(&mut self, edge: Edge<'a, N, P, E, UI, Theme>) {
485        self.edges
486            .push((edge.id, edge.from, edge.to, edge.style_fn));
487    }
488
489    /// The user node id stored at an internal index.
490    pub(super) fn node_id_at(&self, index: usize) -> Option<&N> {
491        self.nodes.get(index).map(|(id, ..)| id)
492    }
493
494    /// Clones the user node id stored at an internal index.
495    pub(super) fn index_to_node_id(&self, index: usize) -> Option<N> {
496        self.node_id_at(index).cloned()
497    }
498
499    /// The internal index of a node by its user id. Linear scan: the node Vec is
500    /// the single source of truth, the index is a transient render-time detail.
501    pub(super) fn node_index(&self, id: &N) -> Option<usize> {
502        self.node_lookup.get(id).copied()
503    }
504
505    /// Sets the graph chrome style (background, etc.) as a theme-derived closure.
506    ///
507    /// Mirrors the other style setters (`box_select_style`, `dragging_edge_style`,
508    /// `cutting_tool_style`) and the per-node/edge/pin `.style()` closures: every
509    /// style entry point on the widget is a `Fn(&Theme) -> _`. For a static style,
510    /// ignore the theme argument: `.graph_style(|_| GraphStyle { ..base })`.
511    pub fn graph_style(mut self, f: impl Fn(&Theme) -> GraphStyle + 'a) -> Self {
512        self.graph_style = Some(Box::new(f));
513        self
514    }
515
516    /// Sets a style callback for the box selection overlay.
517    ///
518    /// The callback receives the theme and returns (fill_color, border_color).
519    ///
520    /// # Example
521    /// ```ignore
522    /// node_graph()
523    ///     .box_select_style(|theme| {
524    ///         (Color::from_rgba(0.3, 0.6, 1.0, 0.2), Color::from_rgb(0.3, 0.6, 1.0))
525    ///     })
526    /// ```
527    pub fn box_select_style(
528        mut self,
529        f: impl Fn(&Theme) -> (iced::Color, iced::Color) + 'a,
530    ) -> Self {
531        self.box_select_style_fn = Some(Box::new(f));
532        self
533    }
534
535    /// Sets the style of the edge being dragged (before it connects). Receives
536    /// the theme and the source pin, so the closure can derive the stroke from
537    /// the pin's info (e.g. a port-typed color) for both ends of the loose edge.
538    pub fn dragging_edge_style(
539        mut self,
540        f: impl Fn(&Theme, PinInfo<'_, P, UI>) -> EdgeStyle + 'a,
541    ) -> Self {
542        self.dragging_edge_style_fn = Some(Box::new(f));
543        self
544    }
545
546    /// Sets a style callback for the edge cutting tool overlay.
547    ///
548    /// The callback receives the theme and returns the line color.
549    ///
550    /// # Example
551    /// ```ignore
552    /// node_graph()
553    ///     .cutting_tool_style(|theme| Color::from_rgb(1.0, 0.3, 0.3))
554    /// ```
555    pub fn cutting_tool_style(mut self, f: impl Fn(&Theme) -> iced::Color + 'a) -> Self {
556        self.cutting_tool_style_fn = Some(Box::new(f));
557        self
558    }
559
560    /// Sets a validation callback for pin connection compatibility.
561    ///
562    /// When set, this callback is authoritative: it receives both endpoints as
563    /// [`PinEnd`] views (node id, pin id, direction, occupancy, user info) and
564    /// returns `true` if they can connect.
565    ///
566    /// # Warning
567    ///
568    /// Setting this REPLACES the built-in checks; they do not auto-compose, and
569    /// there is no opt-out flag. A closure that only inspects payloads would re-allow
570    /// same-direction, self-node, and double-booked-input connections. Re-include the
571    /// built-in rules with
572    /// [`default_can_connect`](crate::connection::default_can_connect):
573    ///
574    /// ```ignore
575    /// use iced_nodegraph::connection::default_can_connect;
576    /// ng.can_connect(|from, to| default_can_connect(from, to) && from.info() == to.info());
577    /// ```
578    ///
579    /// Or pick individual predicates ([`direction_ok`](crate::connection::direction_ok),
580    /// [`not_same_node`](crate::connection::not_same_node),
581    /// [`input_not_occupied`](crate::connection::input_not_occupied)).
582    ///
583    /// When not set, the widget applies `default_can_connect` (direction, not-same-
584    /// node, one-edge-per-input).
585    pub fn can_connect(
586        mut self,
587        f: impl Fn(PinEnd<'_, N, P, UI>, PinEnd<'_, N, P, UI>) -> bool + 'a,
588    ) -> Self {
589        self.can_connect = Some(Box::new(f));
590        self
591    }
592
593    /// Overrides the key and pointer bindings.
594    ///
595    /// The default [`Keymap`](crate::Keymap) is platform-aware (e.g. clone is
596    /// `Alt+D` on the web because browsers reserve `Cmd/Ctrl+D`); pass a
597    /// modified copy to rebind or disable individual actions:
598    ///
599    /// ```
600    /// use iced_nodegraph::{Keymap, node_graph};
601    /// use iced_wgpu::Renderer;
602    ///
603    /// let keymap = Keymap {
604    ///     select_all: None, // disable Select All
605    ///     ..Keymap::default()
606    /// };
607    /// let graph = node_graph::<(), iced::Theme, Renderer>().keymap(keymap);
608    /// ```
609    pub fn keymap(mut self, keymap: input::Keymap) -> Self {
610        self.keymap = keymap;
611        self
612    }
613
614    /// Sets a callback for when an edge is connected between two pins.
615    ///
616    /// `from` is always the OUTPUT pin and `to` always the INPUT pin, whichever way
617    /// the user dragged: the widget normalizes orientation to the rendered data
618    /// flow. So `to` is the key when enforcing one edge per input (see the
619    /// crate-level "What the host owns").
620    ///
621    /// Fires on SNAP during a drag, not on release - a single drag can emit several
622    /// connect/disconnect pairs as the edge snaps and unsnaps. Treat it as live
623    /// state, not a commit.
624    ///
625    /// Required to start an edge drag: without this callback, pressing a pin selects
626    /// its node instead (a dropped edge could not be persisted anyway).
627    pub fn on_connect(mut self, f: impl Fn(PinRef<N, P>, PinRef<N, P>) -> Message + 'a) -> Self {
628        self.on_connect = Some(Box::new(f));
629        self
630    }
631
632    /// Sets a callback for when an edge is disconnected between two pins.
633    ///
634    /// Like [`on_connect`](Self::on_connect), the pair is normalized output-first
635    /// (`from` = output, `to` = input).
636    pub fn on_disconnect(mut self, f: impl Fn(PinRef<N, P>, PinRef<N, P>) -> Message + 'a) -> Self {
637        self.on_disconnect = Some(Box::new(f));
638        self
639    }
640
641    /// Sets a callback for when one or more nodes are dragged to a new position.
642    ///
643    /// The callback receives the movement delta in world coordinates and the list
644    /// of moved node IDs. Dragging a single node reports that one node; dragging a
645    /// selection reports the whole group. In both cases the app applies the same
646    /// delta to every listed node.
647    ///
648    /// Required for node dragging: node positions live in the host, so without this
649    /// callback a drag has nowhere to land and the widget keeps nodes stationary
650    /// (selection still works).
651    pub fn on_move(mut self, f: impl Fn(Vector, Vec<N>) -> Message + 'a) -> Self {
652        self.on_move = Some(Box::new(f));
653        self
654    }
655
656    /// Sets a callback for when the selection changes.
657    ///
658    /// The callback receives the list of currently selected node IDs.
659    /// Fires on click-select, box-select, and Shift+click multi-select.
660    ///
661    /// The widget keeps its own selection regardless; to make the host the source
662    /// of truth, feed the reported value back via [`selection`](Self::selection).
663    pub fn on_select(mut self, f: impl Fn(Vec<N>) -> Message + 'a) -> Self {
664        self.on_select = Some(Box::new(f));
665        self
666    }
667
668    /// Sets a callback for when the user requests to clone selected nodes (Ctrl+D).
669    ///
670    /// The callback receives the list of node IDs to clone.
671    /// The application is responsible for creating the actual clones.
672    pub fn on_clone(mut self, f: impl Fn(Vec<N>) -> Message + 'a) -> Self {
673        self.on_clone = Some(Box::new(f));
674        self
675    }
676
677    /// Sets a callback for when the user requests to delete selected nodes (Delete key).
678    ///
679    /// The callback receives the list of node IDs to delete.
680    /// The application is responsible for removing the nodes from its data model.
681    pub fn on_delete(mut self, f: impl Fn(Vec<N>) -> Message + 'a) -> Self {
682        self.on_delete = Some(Box::new(f));
683        self
684    }
685
686    /// Sets a callback for when a drag operation starts.
687    /// Used for real-time collaboration to broadcast drag state to other users.
688    pub fn on_drag_start(mut self, f: impl Fn(DragInfo<N, P>) -> Message + 'a) -> Self {
689        self.on_drag_start = Some(Box::new(f));
690        self
691    }
692
693    /// Sets a callback for drag position updates.
694    ///
695    /// Called frequently during a drag with the current cursor position in world
696    /// coordinates as a [`Point`] (a semantic type, matching `on_move`'s
697    /// `Vector`, rather than a bare `(f32, f32)` tuple).
698    pub fn on_drag_update(mut self, f: impl Fn(Point) -> Message + 'a) -> Self {
699        self.on_drag_update = Some(Box::new(f));
700        self
701    }
702
703    /// Sets a callback for when a drag operation ends.
704    pub fn on_drag_end(mut self, f: impl Fn() -> Message + 'a) -> Self {
705        self.on_drag_end = Some(Box::new(f));
706        self
707    }
708
709    /// Sets the commit callback for pan/zoom.
710    ///
711    /// Fires with the new camera position and zoom when the user finishes a pan
712    /// drag or zooms (zoom shifts position too, so both report together). Store
713    /// the value and feed it back via [`view`](Self::view) to keep the controlled
714    /// camera in sync.
715    pub fn on_pan(mut self, f: impl Fn(Point, f32) -> Message + 'a) -> Self {
716        self.on_pan = Some(Box::new(f));
717        self
718    }
719
720    /// Sets the per-frame diagnostics callback.
721    ///
722    /// Fires once per redraw with a [`GraphInfo`]: element counts (total / in
723    /// view / culled) and the CPU time of each draw operation, in stack order.
724    /// Values are measured during `draw` and delivered on the next redraw (one
725    /// frame behind), so a live readout should keep requesting redraws. CPU-side
726    /// only; no GPU profiling.
727    pub fn on_info(mut self, f: impl Fn(GraphInfo) -> Message + 'a) -> Self {
728        self.on_info = Some(Box::new(f));
729        self
730    }
731
732    /// Sets the host-controlled selection using user node IDs.
733    ///
734    /// The IDs are converted to internal indices; unknown IDs are ignored.
735    ///
736    /// Optional: the widget tracks selection internally and reports it through
737    /// [`on_select`](Self::on_select), so an uncontrolled graph works without this.
738    /// Feed it only when the host is the source of truth - to drive selection
739    /// programmatically (select-all, clear, restore from a save). This is the
740    /// controlled-component counterpart to `on_select`, exactly like
741    /// [`view`](Self::view) is to `on_pan`.
742    pub fn selection<'b>(mut self, selection: impl IntoIterator<Item = &'b N>) -> Self
743    where
744        N: 'b,
745    {
746        let indices: HashSet<usize> = selection
747            .into_iter()
748            .filter_map(|id| self.node_index(id))
749            .collect();
750        self.external_selection = Some(indices);
751        self
752    }
753
754    /// Sets the width of the node graph widget.
755    pub fn width(mut self, width: impl Into<Length>) -> Self {
756        self.size.width = width.into();
757        self
758    }
759
760    /// Sets the height of the node graph widget.
761    pub fn height(mut self, height: impl Into<Length>) -> Self {
762        self.size.height = height.into();
763        self
764    }
765
766    pub(super) fn elements_iter(
767        &self,
768    ) -> impl Iterator<Item = (Point, &iced::Element<'a, Message, Theme, Renderer>)> {
769        self.nodes.iter().map(|(_, p, e, _, _)| (*p, e))
770    }
771
772    pub(super) fn elements_iter_mut(
773        &mut self,
774    ) -> impl Iterator<Item = (Point, &mut iced::Element<'a, Message, Theme, Renderer>)> {
775        self.nodes.iter_mut().map(|(_, p, e, _, _)| (*p, e))
776    }
777
778    pub(super) fn on_connect_handler(
779        &self,
780    ) -> Option<&Box<dyn Fn(PinRef<N, P>, PinRef<N, P>) -> Message + 'a>> {
781        self.on_connect.as_ref()
782    }
783    pub(super) fn on_disconnect_handler(
784        &self,
785    ) -> Option<&Box<dyn Fn(PinRef<N, P>, PinRef<N, P>) -> Message + 'a>> {
786        self.on_disconnect.as_ref()
787    }
788    pub(super) fn on_move_handler(&self) -> Option<&Box<dyn Fn(Vector, Vec<N>) -> Message + 'a>> {
789        self.on_move.as_ref()
790    }
791    pub(super) fn on_select_handler(&self) -> Option<&Box<dyn Fn(Vec<N>) -> Message + 'a>> {
792        self.on_select.as_ref()
793    }
794    pub(super) fn on_clone_handler(&self) -> Option<&Box<dyn Fn(Vec<N>) -> Message + 'a>> {
795        self.on_clone.as_ref()
796    }
797    pub(super) fn on_delete_handler(&self) -> Option<&Box<dyn Fn(Vec<N>) -> Message + 'a>> {
798        self.on_delete.as_ref()
799    }
800    pub(super) fn on_drag_start_handler(
801        &self,
802    ) -> Option<&Box<dyn Fn(DragInfo<N, P>) -> Message + 'a>> {
803        self.on_drag_start.as_ref()
804    }
805    pub(super) fn on_drag_update_handler(&self) -> Option<&Box<dyn Fn(Point) -> Message + 'a>> {
806        self.on_drag_update.as_ref()
807    }
808    pub(super) fn on_drag_end_handler(&self) -> Option<&Box<dyn Fn() -> Message + 'a>> {
809        self.on_drag_end.as_ref()
810    }
811    pub(super) fn get_external_selection(&self) -> Option<&HashSet<usize>> {
812        self.external_selection.as_ref()
813    }
814
815    pub(super) fn on_pan_handler(&self) -> Option<&Box<dyn Fn(Point, f32) -> Message + 'a>> {
816        self.on_pan.as_ref()
817    }
818    pub(super) fn on_info_handler(&self) -> Option<&Box<dyn Fn(GraphInfo) -> Message + 'a>> {
819        self.on_info.as_ref()
820    }
821    pub(super) fn view_value(&self) -> Option<(Point, f32)> {
822        self.view
823    }
824
825    /// Translates a list of internal node indices to user IDs.
826    /// Returns empty vec if any translation fails.
827    pub(super) fn translate_node_ids(&self, indices: &[usize]) -> Vec<N> {
828        indices
829            .iter()
830            .filter_map(|&idx| self.index_to_node_id(idx))
831            .collect()
832    }
833}