Nodes are ordinary iced widgets - sliders, text inputs, whatever your view
builds - placed on an infinite zoom/pan canvas and wired together through typed
pins. The widget holds no graph state: your application owns the data model,
and connections, moves, and deletes arrive as messages, like any other iced
widget. Everything on the canvas is drawn by a single WGPU pipeline as signed
distance fields, so it stays sharp at every zoom level and handles graphs with
hundreds of nodes.
Getting started
use *;
use ;
node(..) and edge(..) are builders: chain .style(..) for per-node and
per-edge looks, starting from presets like NodeStyle::input or
EdgeStyle::error that derive from the theme like iced's button::success.
Ids are yours: name the node, pin, edge and anchor id types once on an
Ids
marker (usize everywhere by default). The
crate docs cover styling, connection
validation, and the callback contract in detail. A complete runnable graph
with typed pin payloads and live evaluation is
examples/basic.rs
(cargo run -p iced_nodegraph --example basic).
Demos
Every demo runs natively and in the browser.
| Demo | Live | Shows |
|---|---|---|
hello_world |
run | Command palette, theme switching, selection, clone/delete, persistence |
styling |
run | Style presets and live node styling controls |
interaction |
run | Pin direction and type rules, connection validation, snap feedback |
500_nodes |
run | 500 nodes / 640 edges stress test with a runtime stats overlay |
shader_editor |
run | Visual WGSL editor: the graph compiles to a running shader |
Controls
| Action | Mouse / Keyboard | Touch |
|---|---|---|
| Pan | Right mouse drag | One-finger drag on empty canvas, or two-finger drag |
| Zoom | Scroll wheel (zooms at cursor) | Two-finger pinch |
| Connect | Drag from pin to pin | Drag from pin to pin |
| Disconnect | Click a connected pin to unplug | Tap a connected pin to unplug |
| Fork edge | Shift+drag from a connected pin | - |
| Move node | Drag node (hold Alt to ignore the snap grid) | Drag node |
| Resize node | Drag the bottom-right grip (needs Node::resizable; hold Alt to ignore the snap grid) |
Drag the bottom-right grip |
| Move frame with contents | Drag a frame's empty area | Same |
| Box select | Left drag on empty canvas | - (empty-canvas drag pans) |
| Add to selection | Shift+click | - |
| Clear selection | Escape | - |
| Select all | Ctrl+A | - |
| Clone selection | Ctrl+D (web: Alt+D) | - |
| Delete selection | Delete / Backspace (web: Delete) | - |
| Frame everything | Home | - |
| Frame selection | F | - |
| Cut edges | Ctrl+click an edge, or Ctrl+drag across edges | - |
| Add routing anchor | Drag a cable mid-run | Same |
| Re-route | Drag a cable where it wraps an anchor | Same |
| Attach to anchor | Drop either drag onto an anchor | Same |
| Detach from anchor | Right-click a cable's wrap | - |
| Delete anchor | Right-click an anchor's core | - |
| Move anchor | Drag an anchor's core (hold Alt to ignore the snap grid) | Same |
| Jump the camera | Click in the minimap | Tap in the minimap |
| Pan via minimap | Drag in the minimap | Drag in the minimap |
Ctrl is Cmd on macOS. On the web, clone avoids Ctrl/Cmd+D (the browser's
bookmark shortcut) and delete drops the Backspace alternative (legacy
back-navigation). Every binding is host-rebindable through
NodeGraph::keymap - see the Keymap type. Connections snap while dragging
near a compatible pin - like plugging in a cable - rather than on mouse
release, and compatible targets pulse during the drag. A route attaches and
detaches the same way, during the drag rather than on release.
Anchor delete and detach ride the pan button as a travel-free click, so a
right-DRAG starting on an anchor still pans; they follow a rebound
pan_button. The routing gestures need on_anchor_create, on_route_attach
and on_route_detach wired, anchor delete needs on_anchor_delete, and moving
an anchor needs on_anchor_move - which also gates the core's hover highlight
and the grab cursor, so a host wiring delete but not move gets a working delete
on a core that never lights up. An unwired gesture leaves its zone inert rather
than reporting into the void.
The minimap is opt-in through NodeGraph::minimap; without it the two map rows
do not apply and the corner stays empty canvas. It takes a press before any
node under it does, and reports the camera it steers through on_camera.
How it works
Your app owns the graph. The widget is stateless between frames: it renders
the nodes and edges you pass in and reports intent (on_connect, on_move,
on_delete, ...) through callbacks. Your update logic applies the change and
the next view reflects it - the plain iced loop, no hidden state to sync.
One GPU pipeline. Node bodies, edges, pins, shadows, and the background
grid are compiled into a single signed-distance-field scene and rendered by
the in-tree iced_nodegraph_sdf
crate. A compute-shader tile index culls per pixel, and animated stroke
patterns drive their own redraws. Details in
ARCHITECTURE.md.
Typed coordinates. Screen space and world space are distinct
euclid types, so mixing them up is a compile error.
Compatibility
- Targets iced 0.14 with the wgpu renderer (the SDF pipeline has no tiny-skia fallback).
- Native: Windows, macOS, Linux. Web: WebAssembly on WebGPU-capable browsers - there is no WebGL fallback, so Chrome/Chromium is recommended.
- Pre-1.0: minor releases may change the API. See the CHANGELOG.
Development
The workspace contains the widget (iced_nodegraph), the SDF renderer
(iced_nodegraph_sdf), and the demos.
ICED_TEST_BACKEND=tiny-skia
AGENTS.md has the full pre-push list, including the ignored orbit sweep and the workspace, bench and wasm checks.