Documentation, readme

This commit is contained in:
Setzer22 2022-02-19 19:42:02 +01:00
parent db4c4f0f9f
commit e16968256a
8 changed files with 299 additions and 176 deletions

View File

@ -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 😄

View File

@ -9,8 +9,6 @@ use egui::*;
pub type PortLocations = std::collections::HashMap<AnyParameterId, Pos2>;
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<DataType, ValueType>(
&self,
ui: &mut Ui,
node_id: NodeId,
graph: &Graph<Self, DataType, ValueType>,
user_state: &Self::UserState,
) -> Vec<NodeResponse<Self::Response>>
where
Self::Response: UserResponseTrait;
}
impl<NodeData, DataType, ValueType, NodeKind, UserResponse, UserState>
GraphEditorState<NodeData, DataType, ValueType, NodeKind, UserState>
impl<NodeData, DataType, ValueType, NodeTemplate, UserResponse, UserState>
GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, UserState>
where
NodeData: NodeDataTrait<Response = UserResponse, UserState = UserState>,
UserResponse: UserResponseTrait,
ValueType: InputParamWidget,
NodeKind: NodeKindTrait<NodeData = NodeData, DataType = DataType, ValueType = ValueType>,
ValueType: WidgetValueTrait,
NodeTemplate: NodeTemplateTrait<NodeData = NodeData, DataType = DataType, ValueType = ValueType>,
DataType: DataTypeTrait,
{
#[must_use]
pub fn draw_graph_editor(
&mut self,
ctx: &CtxRef,
all_kinds: impl NodeKindIter<Item = NodeKind>,
all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
) -> GraphResponse<UserResponse> {
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<Response = UserResponse, UserState = UserState>,
UserResponse: UserResponseTrait,
ValueType: InputParamWidget,
ValueType: WidgetValueTrait,
DataType: DataTypeTrait,
{
pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0];

View File

@ -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<NodeData> {
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<DataType, ValueType> {
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<DataType> {
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<NodeData, DataType, ValueType> {
/// The [`Node`]s of the graph
pub nodes: SlotMap<NodeId, Node<NodeData>>,
/// The [`InputParam`]s of the graph
pub inputs: SlotMap<InputId, InputParam<DataType, ValueType>>,
/// The [`OutputParam`]s of the graph
pub outputs: SlotMap<OutputId, OutputParam<DataType>>,
// Connects the input of a node, to the output of its predecessor that
// produces it
pub connections: SecondaryMap<InputId, OutputId>,
}

View File

@ -1,96 +1,46 @@
#![forbid(unsafe_code)]
use slotmap::{SecondaryMap, SlotMap};
pub type SVec<T> = 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<T> = smallvec::SmallVec<[T; 4]>;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
pub struct Node<NodeData> {
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<DataType, ValueType> {
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<DataType> {
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<NodeData, DataType, ValueType> {
pub nodes: SlotMap<NodeId, Node<NodeData>>,
pub inputs: SlotMap<InputId, InputParam<DataType, ValueType>>,
pub outputs: SlotMap<OutputId, OutputParam<DataType>>,
// Connects the input of a node, to the output of its predecessor that
// produces it
connections: SecondaryMap<InputId, OutputId>,
}

View File

@ -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<NodeKind> {
query: String,
pub struct NodeFinder<NodeTemplate> {
pub query: String,
/// Reset every frame. When set, the node finder will be moved at that position
pub position: Option<Pos2>,
pub just_spawned: bool,
_phantom: PhantomData<NodeKind>,
_phantom: PhantomData<NodeTemplate>,
}
pub trait NodeKindIter {
type Item;
fn all_kinds(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_>;
}
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<Self::NodeData, Self::DataType, Self::ValueType>,
node_id: NodeId,
);
}
impl<NodeKind, NodeData> NodeFinder<NodeKind>
impl<NodeTemplate, NodeData> NodeFinder<NodeTemplate>
where
NodeKind: NodeKindTrait<NodeData = NodeData>,
NodeTemplate: NodeTemplateTrait<NodeData = NodeData>,
{
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<Item = NodeKind>,
) -> Option<NodeKind> {
all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
) -> Option<NodeTemplate> {
let background_color = color_from_hex("#3f3f3f").unwrap();
let text_color = color_from_hex("#fefefe").unwrap();

View File

@ -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<DataType, ValueType>(
&self,
ui: &mut egui::Ui,
node_id: NodeId,
graph: &Graph<Self, DataType, ValueType>,
user_state: &Self::UserState,
) -> Vec<NodeResponse<Self::Response>>
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<dyn Iterator<Item = &Self::Item> + '_>;
}
/// 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<Self::NodeData, Self::DataType, Self::ValueType>,
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 {}

View File

@ -10,7 +10,7 @@ pub struct PanZoom {
pub zoom: f32,
}
pub struct GraphEditorState<NodeData, DataType, ValueType, NodeKind, UserState> {
pub struct GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, UserState> {
pub graph: Graph<NodeData, DataType, ValueType>,
/// 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<NodeData, DataType, ValueType, NodeKind, UserState>
/// The position of each node.
pub node_positions: SecondaryMap<NodeId, egui::Pos2>,
/// The node finder is used to create new nodes.
pub node_finder: Option<NodeFinder<NodeKind>>,
pub node_finder: Option<NodeFinder<NodeTemplate>>,
/// The panning of the graph viewport.
pub pan_zoom: PanZoom,
pub user_state: UserState,

View File

@ -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<dyn Iterator<Item = &Self::Item> + '_> {
// 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<MyNodeData, MyDataType, MyValueType, MyNodeKind, MyGraphState>,
state: GraphEditorState<MyNodeData, MyDataType, MyValueType, MyNodeTemplate, MyGraphState>,
}
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