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}