diff --git a/README.md b/README.md index b808d36..93eabe1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,62 @@ -# egui_node_graph -Build your node graph applications in Rust, using egui +# Egui Node Graph +> There you have it! Now go build your next awesome node graph thing in Rust 🦀 + +[![Latest version](https://img.shields.io/crates/v/egui_node_graph.svg)](https://crates.io/crates/egui_node_graph) +[![Documentation](https://docs.rs/egui_node_graph/badge.svg)](https://docs.rs/egui_node_graph) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) + +**Egui node graph** is a featureful, customizable library to create node graph +applications using [egui](https://github.com/emilk/egui). The library takes care +of presenting a node graph to your users, and allows customizing many aspects of +the interaction, creating the semantics you want for your specific application. + +## Features and goals +This crate is meant to be a solid base for anyone wanting to expose a node graph +interface to their users. Its main design goal is to be completely agnostic to +the semantics of the graph, be it game logic, audio production, dialog trees, +shader generators... we have you covered! + +The purpose of this library is to draw your graphs and handle the common user +interaction, like adding new nodes, moving nodes or creating connections. All +the additional functionality is provided by the user by means of custom user +types implementing several traits. + +## Usage +To see a node graph in action, simply clone this repository and launch the +example using `cargo run`. This should open a window with an empty canvas. Right +clicking anywhere on the screen will bring up the *node finder* menu. + +The [application code in the example](https://github.com/setzer22/egui_node_graph/blob/main/egui_node_graph_example/src/app.rs) +is thoroughly commented and serves as a good introduction to embedding this +library in your egui project. + +## A note on API visibility +Contrary to the general tendency in the Rust ecosytem, this library exposes all +types and fields that may be remotely relevant to a user as public. This is done +with the intent to be as flexible as possible, so no implementation details are +hidden from users who wish to tinker with the internals. Note that this crate +forbids use of `unsafe` so there is no risk of introducing UB by breaking any of +its invariants. + +That being said, for the most typical use cases, you will want to stick to the +customization options this crate provides for you: The generic types in the +`GraphEditorState` object and their associated traits are the main API, all of +the other types and fields in this crate should be considered an implementation +detail. The example project contains a detailed explanation of all the +customization options and how are users supposed to interact with this crate. + +Finally, this does not change the fact that this crate follows semantic +versioning, as is usual in the Rust ecosystem. Any change to a public field is +still considered breaking. + +## Use cases + +**Egui node graph** is the library powering the graph user interface of +[Blackjack](https://github.com/setzer22/blackjack), a 3d procedural modelling +software built in Rust using egui, rend3 and wgpu. +![Main interface of blackjack](https://raw.githubusercontent.com/setzer22/blackjack/main/doc/resources/showcase.png) +Are you using this crate for something cool? Add yourself to this section by sending a PR! + +## Contributing +Contributions are welcome! Before writing a PR, please get in touch by filing an issue 😄 \ No newline at end of file diff --git a/egui_node_graph/src/editor_ui.rs b/egui_node_graph/src/editor_ui.rs index 7bc418f..8fc52fd 100644 --- a/egui_node_graph/src/editor_ui.rs +++ b/egui_node_graph/src/editor_ui.rs @@ -9,8 +9,6 @@ use egui::*; pub type PortLocations = std::collections::HashMap; -pub trait UserResponseTrait: Clone + Copy + std::fmt::Debug + PartialEq + Eq {} - /// Nodes communicate certain events to the parent graph when drawn. There is /// one special `User` variant which can be used by users as the return value /// when executing some custom actions in the UI of the node. @@ -43,52 +41,20 @@ pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> { pub pan: egui::Vec2, } -pub trait InputParamWidget { - fn value_widget(&mut self, param_name: &str, ui: &mut Ui); -} - -pub trait DataTypeTrait: PartialEq + Eq { - // The associated port color of this datatype - fn data_type_color(&self) -> egui::Color32; - - // The name of this datatype - fn name(&self) -> &str; -} - -pub trait NodeDataTrait -where - Self: Sized, -{ - type Response; - type UserState; - - /// Additional UI elements to draw in the nodes, after the parameters. - fn bottom_ui( - &self, - ui: &mut Ui, - node_id: NodeId, - graph: &Graph, - user_state: &Self::UserState, - - ) -> Vec> - where - Self::Response: UserResponseTrait; -} - -impl - GraphEditorState +impl + GraphEditorState where NodeData: NodeDataTrait, UserResponse: UserResponseTrait, - ValueType: InputParamWidget, - NodeKind: NodeKindTrait, + ValueType: WidgetValueTrait, + NodeTemplate: NodeTemplateTrait, DataType: DataTypeTrait, { #[must_use] pub fn draw_graph_editor( &mut self, ctx: &CtxRef, - all_kinds: impl NodeKindIter, + all_kinds: impl NodeTemplateIter, ) -> GraphResponse { let mouse = &ctx.input().pointer; let cursor_pos = mouse.hover_pos().unwrap_or(Pos2::ZERO); @@ -277,7 +243,7 @@ impl<'a, NodeData, DataType, ValueType, UserResponse, UserState> where NodeData: NodeDataTrait, UserResponse: UserResponseTrait, - ValueType: InputParamWidget, + ValueType: WidgetValueTrait, DataType: DataTypeTrait, { pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0]; diff --git a/egui_node_graph/src/graph.rs b/egui_node_graph/src/graph.rs new file mode 100644 index 0000000..f043660 --- /dev/null +++ b/egui_node_graph/src/graph.rs @@ -0,0 +1,91 @@ +use super::*; + +#[cfg(feature = "persistence")] +use serde::{Deserialize, Serialize}; + +/// A node inside the [`Graph`]. Nodes have input and output parameters, stored +/// as ids. They also contain a custom `NodeData` struct with whatever data the +/// user wants to store per-node. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] +pub struct Node { + pub id: NodeId, + pub label: String, + pub inputs: Vec<(String, InputId)>, + pub outputs: Vec<(String, OutputId)>, + pub user_data: NodeData, +} + +/// The three kinds of input params. These describe how the graph must behave +/// with respect to inline widgets and connections for this parameter. +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] +pub enum InputParamKind { + /// No constant value can be set. Only incoming connections can produce it + ConnectionOnly, + /// Only a constant value can be set. No incoming connections accepted. + ConstantOnly, + /// Both incoming connections and constants are accepted. Connections take + /// precedence over the constant values. + ConnectionOrConstant, +} + +#[cfg(feature = "persistence")] +fn shown_inline_default() -> bool { + true +} + +/// An input parameter. Input parameters are inside a node, and represent data +/// that this node receives. Unlike their [`OutputParam`] counterparts, input +/// parameters also display an inline widget which allows setting its "value". +/// The `DataType` generic parameter is used to restrict the range of input +/// connections for this parameter, and the `ValueType` is use to represent the +/// data for the inline widget (i.e. constant) value. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] +pub struct InputParam { + pub id: InputId, + /// The data type of this node. Used to determine incoming connections. This + /// should always match the type of the InputParamValue, but the property is + /// not actually enforced. + pub typ: DataType, + /// The constant value stored in this parameter. + pub value: ValueType, + /// The input kind. See [`InputParamKind`] + pub kind: InputParamKind, + /// Back-reference to the node containing this parameter. + pub node: NodeId, + /// When true, the node is shown inline inside the node graph. + #[cfg_attr(feature = "persistence", serde(default = "shown_inline_default"))] + pub shown_inline: bool, +} + +/// An output parameter. Output parameters are inside a node, and represent the +/// data that the node produces. Output parameters can be linked to the input +/// parameters of other nodes. Unlike an [`InputParam`], output parameters +/// cannot have a constant inline value. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] +pub struct OutputParam { + pub id: OutputId, + /// Back-reference to the node containing this parameter. + pub node: NodeId, + pub typ: DataType, +} + +/// The graph, containing nodes, input parameters and output parameters. Because +/// graphs are full of self-referential structures, this type uses the `slotmap` +/// crate to represent all the inner references in the data. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] +pub struct Graph { + /// The [`Node`]s of the graph + pub nodes: SlotMap>, + /// The [`InputParam`]s of the graph + pub inputs: SlotMap>, + /// The [`OutputParam`]s of the graph + pub outputs: SlotMap>, + // Connects the input of a node, to the output of its predecessor that + // produces it + pub connections: SecondaryMap, +} \ No newline at end of file diff --git a/egui_node_graph/src/lib.rs b/egui_node_graph/src/lib.rs index 3cfb7e8..2b11be6 100644 --- a/egui_node_graph/src/lib.rs +++ b/egui_node_graph/src/lib.rs @@ -1,96 +1,46 @@ +#![forbid(unsafe_code)] + use slotmap::{SecondaryMap, SlotMap}; +pub type SVec = smallvec::SmallVec<[T; 4]>; + +/// Contains the main definitions for the node graph model. +pub mod graph; +pub use graph::*; + +/// Type declarations for the different id types (node, input, output) pub mod id_type; pub use id_type::*; +/// Implements the index trait for the Graph type, allowing indexing by all +/// three id types pub mod index_impls; +/// Implementing the main methods for the `Graph` pub mod graph_impls; +/// Custom error types, crate-wide pub mod error; pub use error::*; +/// The main struct in the library, contains all the necessary state to draw the +/// UI graph pub mod ui_state; pub use ui_state::*; +/// The node finder is a tiny widget allowing to create new node types pub mod node_finder; pub use node_finder::*; +/// The inner details of the egui implementation. Most egui code lives here. pub mod editor_ui; pub use editor_ui::*; +/// Several traits that must be implemented by the user to customize the +/// behavior of this library. +pub mod traits; +pub use traits::*; + mod utils; mod color_hex_utils; - -#[cfg(feature = "persistence")] -use serde::{Deserialize, Serialize}; - -pub type SVec = smallvec::SmallVec<[T; 4]>; - -#[derive(Debug, Clone)] -#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] -pub struct Node { - pub id: NodeId, - pub label: String, - pub inputs: Vec<(String, InputId)>, - pub outputs: Vec<(String, OutputId)>, - pub user_data: NodeData, -} - -/// There are three kinds of input params -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] -pub enum InputParamKind { - /// No constant value can be set. Only incoming connections can produce it - ConnectionOnly, - /// Only a constant value can be set. No incoming connections accepted. - ConstantOnly, - /// Both incoming connections and constants are accepted. Connections take - /// precedence over the constant values. - ConnectionOrConstant, -} - -#[cfg(feature = "persistence")] -fn shown_inline_default() -> bool { - true -} - -#[derive(Debug, Clone)] -#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] -pub struct InputParam { - pub id: InputId, - /// The data type of this node. Used to determine incoming connections. This - /// should always match the type of the InputParamValue, but the property is - /// not actually enforced. - pub typ: DataType, - /// The constant value stored in this parameter. - pub value: ValueType, - /// The input kind. See [`InputParamKind`] - pub kind: InputParamKind, - /// Back-reference to the node containing this parameter. - pub node: NodeId, - /// When true, the node is shown inline inside the node graph. - #[cfg_attr(feature = "persistence", serde(default = "shown_inline_default"))] - pub shown_inline: bool, -} - -#[derive(Debug, Clone)] -#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] -pub struct OutputParam { - pub id: OutputId, - /// Back-reference to the node containing this parameter. - pub node: NodeId, - pub typ: DataType, -} - -#[derive(Debug, Clone)] -#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))] -pub struct Graph { - pub nodes: SlotMap>, - pub inputs: SlotMap>, - pub outputs: SlotMap>, - // Connects the input of a node, to the output of its predecessor that - // produces it - connections: SecondaryMap, -} diff --git a/egui_node_graph/src/node_finder.rs b/egui_node_graph/src/node_finder.rs index 24cb478..854a426 100644 --- a/egui_node_graph/src/node_finder.rs +++ b/egui_node_graph/src/node_finder.rs @@ -1,49 +1,20 @@ use std::marker::PhantomData; -use crate::{color_hex_utils::*, Graph, NodeId}; +use crate::{color_hex_utils::*, NodeTemplateIter, NodeTemplateTrait}; use egui::*; -pub struct NodeFinder { - query: String, +pub struct NodeFinder { + pub query: String, /// Reset every frame. When set, the node finder will be moved at that position pub position: Option, pub just_spawned: bool, - _phantom: PhantomData, + _phantom: PhantomData, } -pub trait NodeKindIter { - type Item; - fn all_kinds(&self) -> Box + '_>; -} - -pub trait NodeKindTrait: Clone { - type NodeData; - type DataType; - type ValueType; - - /// Returns a descriptive name for the node kind, used in the node finder. - fn node_finder_label(&self) -> &str; - - /// Returns a descriptive name for the node kind, used in the graph. - fn node_graph_label(&self) -> String; - - /// Returns the user data for this node kind. - fn user_data(&self) -> Self::NodeData; - - /// This function is run when this node kind gets added to the graph. The - /// node will be empty by default, and this function can be used to fill its - /// parameters. - fn build_node( - &self, - graph: &mut Graph, - node_id: NodeId, - ); -} - -impl NodeFinder +impl NodeFinder where - NodeKind: NodeKindTrait, + NodeTemplate: NodeTemplateTrait, { pub fn new_at(pos: Pos2) -> Self { NodeFinder { @@ -60,8 +31,8 @@ where pub fn show( &mut self, ui: &mut Ui, - all_kinds: impl NodeKindIter, - ) -> Option { + all_kinds: impl NodeTemplateIter, + ) -> Option { let background_color = color_from_hex("#3f3f3f").unwrap(); let text_color = color_from_hex("#fefefe").unwrap(); diff --git a/egui_node_graph/src/traits.rs b/egui_node_graph/src/traits.rs new file mode 100644 index 0000000..c6f52d8 --- /dev/null +++ b/egui_node_graph/src/traits.rs @@ -0,0 +1,85 @@ +use super::*; + +/// This trait must be implemented by the `ValueType` generic parameter of the +/// [`Graph`]. The trait allows drawing custom inline widgets for the different +/// types of the node graph. +pub trait WidgetValueTrait { + fn value_widget(&mut self, param_name: &str, ui: &mut egui::Ui); +} + +/// This trait must be implemented by the `DataType` generic parameter of the +/// [`Graph`]. This trait tells the library how to visually expose data types +/// to the user. +pub trait DataTypeTrait: PartialEq + Eq { + // The associated port color of this datatype + fn data_type_color(&self) -> egui::Color32; + + // The name of this datatype + fn name(&self) -> &str; +} + +/// This trait must be implemented for the `NodeData` generic parameter of the +/// [`Graph`]. This trait allows customizing some aspects of the node drawing. +pub trait NodeDataTrait +where + Self: Sized, +{ + /// Must be set to the custom user `NodeResponse` type + type Response; + /// Must be set to the custom user `UserState` type + type UserState; + + /// Additional UI elements to draw in the nodes, after the parameters. + fn bottom_ui( + &self, + ui: &mut egui::Ui, + node_id: NodeId, + graph: &Graph, + user_state: &Self::UserState, + ) -> Vec> + where + Self::Response: UserResponseTrait; +} + +/// This trait can be implemented by any user type. The trait tells the library +/// how to enumerate the node templates it will present to the user as part of +/// the node finder. +pub trait NodeTemplateIter { + type Item; + fn all_kinds(&self) -> Box + '_>; +} + +/// This trait must be implemented by the `NodeTemplate` generic parameter of +/// the [`GraphEditorState`]. It allows the customization of node templates. A +/// node template is what describes what kinds of nodes can be added to the +/// graph, what is their name, and what are their input / output parameters. +pub trait NodeTemplateTrait: Clone { + /// Must be set to the custom user `NodeData` type + type NodeData; + /// Must be set to the custom user `DataType` type + type DataType; + /// Must be set to the custom user `ValueType` type + type ValueType; + + /// Returns a descriptive name for the node kind, used in the node finder. + fn node_finder_label(&self) -> &str; + + /// Returns a descriptive name for the node kind, used in the graph. + fn node_graph_label(&self) -> String; + + /// Returns the user data for this node kind. + fn user_data(&self) -> Self::NodeData; + + /// This function is run when this node kind gets added to the graph. The + /// node will be empty by default, and this function can be used to fill its + /// parameters. + fn build_node( + &self, + graph: &mut Graph, + node_id: NodeId, + ); +} + +/// The custom user response types when drawing nodes in the graph must +/// implement this trait. +pub trait UserResponseTrait: Clone + Copy + std::fmt::Debug + PartialEq + Eq {} \ No newline at end of file diff --git a/egui_node_graph/src/ui_state.rs b/egui_node_graph/src/ui_state.rs index 2905ffc..d60dc7b 100644 --- a/egui_node_graph/src/ui_state.rs +++ b/egui_node_graph/src/ui_state.rs @@ -10,7 +10,7 @@ pub struct PanZoom { pub zoom: f32, } -pub struct GraphEditorState { +pub struct GraphEditorState { pub graph: Graph, /// Nodes are drawn in this order. Draw order is important because nodes /// that are drawn last are on top. @@ -24,7 +24,7 @@ pub struct GraphEditorState /// The position of each node. pub node_positions: SecondaryMap, /// The node finder is used to create new nodes. - pub node_finder: Option>, + pub node_finder: Option>, /// The panning of the graph viewport. pub pan_zoom: PanZoom, pub user_state: UserState, diff --git a/egui_node_graph_example/src/app.rs b/egui_node_graph_example/src/app.rs index b8ac887..81d92dc 100644 --- a/egui_node_graph_example/src/app.rs +++ b/egui_node_graph_example/src/app.rs @@ -32,11 +32,11 @@ pub enum MyValueType { Scalar { value: f32 }, } -/// NodeKind is a mechanism to define node "templates". It's what the graph will -/// display in the "new node" popup. The user code needs to tell the library how -/// to convert a NodeKind into a Node. +/// NodeTemplate is a mechanism to define node templates. It's what the graph +/// will display in the "new node" popup. The user code needs to tell the +/// library how to convert a NodeTemplate into a Node. #[derive(Clone, Copy)] -pub enum MyNodeKind { +pub enum MyNodeTemplate { AddScalar, SubtractScalar, VectorTimesScalar, @@ -82,17 +82,17 @@ impl DataTypeTrait for MyDataType { // A trait for the node kinds, which tells the library how to build new nodes // from the templates in the node finder -impl NodeKindTrait for MyNodeKind { +impl NodeTemplateTrait for MyNodeTemplate { type NodeData = MyNodeData; type DataType = MyDataType; type ValueType = MyValueType; fn node_finder_label(&self) -> &str { match self { - MyNodeKind::AddScalar => "Scalar add", - MyNodeKind::SubtractScalar => "Scalar subtract", - MyNodeKind::VectorTimesScalar => "Vector times scalar", - MyNodeKind::AddVector => "Vector subtract", + MyNodeTemplate::AddScalar => "Scalar add", + MyNodeTemplate::SubtractScalar => "Scalar subtract", + MyNodeTemplate::VectorTimesScalar => "Vector times scalar", + MyNodeTemplate::AddVector => "Vector subtract", } } @@ -151,7 +151,7 @@ impl NodeKindTrait for MyNodeKind { } match self { - MyNodeKind::AddScalar => { + MyNodeTemplate::AddScalar => { // The first input param doesn't use the macro so we can comment // it in more detail. graph.add_input_param( @@ -172,17 +172,17 @@ impl NodeKindTrait for MyNodeKind { input!(scalar "B"); output!(scalar "out"); } - MyNodeKind::SubtractScalar => { + MyNodeTemplate::SubtractScalar => { input!(scalar "A"); input!(scalar "B"); output!(scalar "out"); } - MyNodeKind::VectorTimesScalar => { + MyNodeTemplate::VectorTimesScalar => { input!(scalar "scalar"); input!(vector "vector"); output!(vector "out"); } - MyNodeKind::AddVector => { + MyNodeTemplate::AddVector => { input!(vector "v1"); input!(vector "v2"); output!(vector "out"); @@ -191,9 +191,9 @@ impl NodeKindTrait for MyNodeKind { } } -pub struct AllMyNodeKinds; -impl NodeKindIter for AllMyNodeKinds { - type Item = MyNodeKind; +pub struct AllMyNodeTemplates; +impl NodeTemplateIter for AllMyNodeTemplates { + type Item = MyNodeTemplate; fn all_kinds(&self) -> Box + '_> { // This function must return a list of node kinds, which the node finder @@ -204,17 +204,17 @@ impl NodeKindIter for AllMyNodeKinds { // over return parameters, so you can't return an iterator. Box::new( [ - MyNodeKind::AddScalar, - MyNodeKind::SubtractScalar, - MyNodeKind::VectorTimesScalar, - MyNodeKind::AddVector, + MyNodeTemplate::AddScalar, + MyNodeTemplate::SubtractScalar, + MyNodeTemplate::VectorTimesScalar, + MyNodeTemplate::AddVector, ] .iter(), ) } } -impl InputParamWidget for MyValueType { +impl WidgetValueTrait for MyValueType { fn value_widget(&mut self, param_name: &str, ui: &mut egui::Ui) { // This trait is used to tell the library which UI to display for the // inline parameter widgets. @@ -293,7 +293,7 @@ impl NodeDataTrait for MyNodeData { pub struct NodeGraphExample { // The `GraphEditorState` is the top-level object. You "register" all your // custom types by specifying it as its generic parameters. - state: GraphEditorState, + state: GraphEditorState, } impl Default for NodeGraphExample { @@ -312,7 +312,7 @@ impl epi::App for NodeGraphExample { /// Called each time the UI needs repainting, which may be many times per second. /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) { - let graph_response = self.state.draw_graph_editor(ctx, AllMyNodeKinds); + let graph_response = self.state.draw_graph_editor(ctx, AllMyNodeTemplates); for node_response in graph_response.node_responses { // Here, we ignore all other graph events. But you may find // some use for them. For example, by playing a sound when a new