Working application
This commit is contained in:
21
egui_node_graph2/Cargo.toml
Normal file
21
egui_node_graph2/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "egui_node_graph2"
|
||||
description = "A helper library to create interactive node graphs using egui"
|
||||
homepage = "https://github.com/trevyn/egui_node_graph2"
|
||||
repository = "https://github.com/trevyn/egui_node_graph2"
|
||||
license = "MIT"
|
||||
version = "0.7.0"
|
||||
keywords = ["egui_node_graph", "ui", "egui", "graph", "node"]
|
||||
edition = "2021"
|
||||
readme = "../README.md"
|
||||
workspace = ".."
|
||||
|
||||
[features]
|
||||
persistence = ["serde", "slotmap/serde", "smallvec/serde", "egui/persistence"]
|
||||
|
||||
[dependencies]
|
||||
egui = "0.32"
|
||||
slotmap = { version = "1.0" }
|
||||
smallvec = { version = "1.10.0" }
|
||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
94
egui_node_graph2/src/color_hex_utils.rs
Normal file
94
egui_node_graph2/src/color_hex_utils.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use egui::Color32;
|
||||
|
||||
/// Converts a hex string with a leading '#' into a egui::Color32.
|
||||
/// - The first three channels are interpreted as R, G, B.
|
||||
/// - The fourth channel, if present, is used as the alpha value.
|
||||
/// - Both upper and lowercase characters can be used for the hex values.
|
||||
///
|
||||
/// *Adapted from: https://docs.rs/raster/0.1.0/src/raster/lib.rs.html#425-725.
|
||||
/// Credit goes to original authors.*
|
||||
pub fn color_from_hex(hex: &str) -> Result<Color32, String> {
|
||||
// Convert a hex string to decimal. Eg. "00" -> 0. "FF" -> 255.
|
||||
fn _hex_dec(hex_string: &str) -> Result<u8, String> {
|
||||
match u8::from_str_radix(hex_string, 16) {
|
||||
| Ok(o) => Ok(o),
|
||||
| Err(e) => Err(format!("Error parsing hex: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
if hex.len() == 9 && hex.starts_with('#') {
|
||||
// #FFFFFFFF (Red Green Blue Alpha)
|
||||
return Ok(Color32::from_rgba_premultiplied(
|
||||
_hex_dec(&hex[1..3])?,
|
||||
_hex_dec(&hex[3..5])?,
|
||||
_hex_dec(&hex[5..7])?,
|
||||
_hex_dec(&hex[7..9])?,
|
||||
));
|
||||
} else if hex.len() == 7 && hex.starts_with('#') {
|
||||
// #FFFFFF (Red Green Blue)
|
||||
return Ok(Color32::from_rgb(
|
||||
_hex_dec(&hex[1..3])?,
|
||||
_hex_dec(&hex[3..5])?,
|
||||
_hex_dec(&hex[5..7])?,
|
||||
));
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Error parsing hex: {}. Example of valid formats: #FFFFFF or #ffffffff",
|
||||
hex
|
||||
))
|
||||
}
|
||||
|
||||
/// Converts a Color32 into its canonical hexadecimal representation.
|
||||
/// - The color string will be preceded by '#'.
|
||||
/// - If the alpha channel is completely opaque, it will be ommitted.
|
||||
/// - Characters from 'a' to 'f' will be written in lowercase.
|
||||
#[allow(dead_code)]
|
||||
pub fn color_to_hex(color: Color32) -> String {
|
||||
if color.a() < 255 {
|
||||
format!(
|
||||
"#{:02x?}{:02x?}{:02x?}{:02x?}",
|
||||
color.r(),
|
||||
color.g(),
|
||||
color.b(),
|
||||
color.a()
|
||||
)
|
||||
} else {
|
||||
format!("#{:02x?}{:02x?}{:02x?}", color.r(), color.g(), color.b())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn test_color_from_and_to_hex() {
|
||||
assert_eq!(
|
||||
color_from_hex("#00ff00").unwrap(),
|
||||
Color32::from_rgb(0, 255, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
color_from_hex("#5577AA").unwrap(),
|
||||
Color32::from_rgb(85, 119, 170)
|
||||
);
|
||||
assert_eq!(
|
||||
color_from_hex("#E2e2e277").unwrap(),
|
||||
Color32::from_rgba_premultiplied(226, 226, 226, 119)
|
||||
);
|
||||
assert!(color_from_hex("abcdefgh").is_err());
|
||||
|
||||
assert_eq!(
|
||||
color_to_hex(Color32::from_rgb(0, 255, 0)),
|
||||
"#00ff00".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
color_to_hex(Color32::from_rgb(85, 119, 170)),
|
||||
"#5577aa".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
color_to_hex(Color32::from_rgba_premultiplied(226, 226, 226, 119)),
|
||||
"#e2e2e277".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
1356
egui_node_graph2/src/editor_ui.rs
Normal file
1356
egui_node_graph2/src/editor_ui.rs
Normal file
File diff suppressed because it is too large
Load Diff
10
egui_node_graph2/src/error.rs
Normal file
10
egui_node_graph2/src/error.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EguiGraphError {
|
||||
#[error("Node {0:?} has no parameter named {1}")]
|
||||
NoParameterNamed(NodeId, String),
|
||||
|
||||
#[error("Parameter {0:?} was not found in the graph.")]
|
||||
InvalidParameterId(AnyParameterId),
|
||||
}
|
||||
99
egui_node_graph2/src/graph.rs
Normal file
99
egui_node_graph2/src/graph.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 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,
|
||||
/// How many connections can be made with this input. `None` means no
|
||||
/// limit.
|
||||
pub max_connections: Option<NonZeroU32>,
|
||||
/// 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, Vec<OutputId>>,
|
||||
}
|
||||
337
egui_node_graph2/src/graph_impls.rs
Normal file
337
egui_node_graph2/src/graph_impls.rs
Normal file
@ -0,0 +1,337 @@
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<NodeData, DataType, ValueType> Graph<NodeData, DataType, ValueType> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: SlotMap::default(),
|
||||
inputs: SlotMap::default(),
|
||||
outputs: SlotMap::default(),
|
||||
connections: SecondaryMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_node(
|
||||
&mut self,
|
||||
label: String,
|
||||
user_data: NodeData,
|
||||
f: impl FnOnce(&mut Graph<NodeData, DataType, ValueType>, NodeId),
|
||||
) -> NodeId {
|
||||
let node_id = self.nodes.insert_with_key(|node_id| {
|
||||
Node {
|
||||
id: node_id,
|
||||
label,
|
||||
// These get filled in later by the user function
|
||||
inputs: Vec::default(),
|
||||
outputs: Vec::default(),
|
||||
user_data,
|
||||
}
|
||||
});
|
||||
|
||||
f(self, node_id);
|
||||
|
||||
node_id
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn add_wide_input_param(
|
||||
&mut self,
|
||||
node_id: NodeId,
|
||||
name: String,
|
||||
typ: DataType,
|
||||
value: ValueType,
|
||||
kind: InputParamKind,
|
||||
max_connections: Option<NonZeroU32>,
|
||||
shown_inline: bool,
|
||||
) -> InputId {
|
||||
let input_id = self.inputs.insert_with_key(|input_id| InputParam {
|
||||
id: input_id,
|
||||
typ,
|
||||
value,
|
||||
kind,
|
||||
node: node_id,
|
||||
max_connections,
|
||||
shown_inline,
|
||||
});
|
||||
self.nodes[node_id].inputs.push((name, input_id));
|
||||
input_id
|
||||
}
|
||||
|
||||
pub fn add_input_param(
|
||||
&mut self,
|
||||
node_id: NodeId,
|
||||
name: String,
|
||||
typ: DataType,
|
||||
value: ValueType,
|
||||
kind: InputParamKind,
|
||||
shown_inline: bool,
|
||||
) -> InputId {
|
||||
self.add_wide_input_param(
|
||||
node_id,
|
||||
name,
|
||||
typ,
|
||||
value,
|
||||
kind,
|
||||
NonZeroU32::new(1),
|
||||
shown_inline,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn remove_input_param(&mut self, param: InputId) {
|
||||
let node = self[param].node;
|
||||
self[node].inputs.retain(|(_, id)| *id != param);
|
||||
self.inputs.remove(param);
|
||||
self.connections.retain(|i, _| i != param);
|
||||
}
|
||||
|
||||
pub fn remove_output_param(&mut self, param: OutputId) {
|
||||
let node = self[param].node;
|
||||
self[node].outputs.retain(|(_, id)| *id != param);
|
||||
self.outputs.remove(param);
|
||||
for (_, conns) in &mut self.connections {
|
||||
conns.retain(|o| *o != param);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_output_param(
|
||||
&mut self,
|
||||
node_id: NodeId,
|
||||
name: String,
|
||||
typ: DataType,
|
||||
) -> OutputId {
|
||||
let output_id = self.outputs.insert_with_key(|output_id| OutputParam {
|
||||
id: output_id,
|
||||
node: node_id,
|
||||
typ,
|
||||
});
|
||||
self.nodes[node_id].outputs.push((name, output_id));
|
||||
output_id
|
||||
}
|
||||
|
||||
/// Removes a node from the graph with given `node_id`. This also removes
|
||||
/// any incoming or outgoing connections from that node
|
||||
///
|
||||
/// This function returns the list of connections that has been removed
|
||||
/// after deleting this node as input-output pairs. Note that one of the two
|
||||
/// ids in the pair (the one on `node_id`'s end) will be invalid after
|
||||
/// calling this function.
|
||||
pub fn remove_node(
|
||||
&mut self,
|
||||
node_id: NodeId,
|
||||
) -> (Node<NodeData>, Vec<(InputId, OutputId)>) {
|
||||
let mut disconnect_events = vec![];
|
||||
|
||||
for (i, conns) in &mut self.connections {
|
||||
conns.retain(|o| {
|
||||
if self.outputs[*o].node == node_id
|
||||
|| self.inputs[i].node == node_id
|
||||
{
|
||||
disconnect_events.push((i, *o));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: Collect is needed because we can't borrow the input ids while
|
||||
// we remove them inside the loop.
|
||||
for input in self[node_id].input_ids().collect::<SVec<_>>() {
|
||||
self.inputs.remove(input);
|
||||
}
|
||||
for output in self[node_id].output_ids().collect::<SVec<_>>() {
|
||||
self.outputs.remove(output);
|
||||
}
|
||||
let removed_node =
|
||||
self.nodes.remove(node_id).expect("Node should exist");
|
||||
|
||||
(removed_node, disconnect_events)
|
||||
}
|
||||
|
||||
pub fn remove_connection(
|
||||
&mut self,
|
||||
input_id: InputId,
|
||||
output_id: OutputId,
|
||||
) -> bool {
|
||||
self.connections
|
||||
.get_mut(input_id)
|
||||
.map(|conns| {
|
||||
let old_size = conns.len();
|
||||
conns.retain(|id| id != &output_id);
|
||||
|
||||
// connection removed if `conn` size changes
|
||||
old_size != conns.len()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn iter_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
|
||||
self.nodes.iter().map(|(id, _)| id)
|
||||
}
|
||||
|
||||
pub fn add_connection(
|
||||
&mut self,
|
||||
output: OutputId,
|
||||
input: InputId,
|
||||
pos: usize,
|
||||
) {
|
||||
if !self.connections.contains_key(input) {
|
||||
self.connections.insert(input, Vec::default());
|
||||
}
|
||||
|
||||
let max_connections = self
|
||||
.get_input(input)
|
||||
.max_connections
|
||||
.map(NonZeroU32::get)
|
||||
.unwrap_or(u32::MAX) as usize;
|
||||
let already_in = self.connections[input].contains(&output);
|
||||
|
||||
// connecting twice to the same port is a no-op
|
||||
// even for wide ports.
|
||||
if already_in {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.connections[input].len() == max_connections {
|
||||
// if full, replace the connected output
|
||||
self.connections[input][pos] = output;
|
||||
} else {
|
||||
// otherwise, insert at a selected position
|
||||
self.connections[input].insert(pos, output);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_connection_groups(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (InputId, Vec<OutputId>)> + '_ {
|
||||
self.connections.iter().map(|(i, conns)| (i, conns.clone()))
|
||||
}
|
||||
|
||||
pub fn iter_connections(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (InputId, OutputId)> + '_ {
|
||||
self.iter_connection_groups()
|
||||
.flat_map(|(i, conns)| conns.into_iter().map(move |o| (i, o)))
|
||||
}
|
||||
|
||||
pub fn connections(&self, input: InputId) -> Vec<OutputId> {
|
||||
self.connections.get(input).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn connection(&self, input: InputId) -> Option<OutputId> {
|
||||
let is_limit_1 =
|
||||
self.get_input(input).max_connections == NonZeroU32::new(1);
|
||||
let connections = self.connections(input);
|
||||
|
||||
if is_limit_1 && connections.len() == 1 {
|
||||
connections.into_iter().next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn any_param_type(
|
||||
&self,
|
||||
param: AnyParameterId,
|
||||
) -> Result<&DataType, EguiGraphError> {
|
||||
match param {
|
||||
| AnyParameterId::Input(input) =>
|
||||
self.inputs.get(input).map(|x| &x.typ),
|
||||
| AnyParameterId::Output(output) =>
|
||||
self.outputs.get(output).map(|x| &x.typ),
|
||||
}
|
||||
.ok_or(EguiGraphError::InvalidParameterId(param))
|
||||
}
|
||||
|
||||
pub fn try_get_input(
|
||||
&self,
|
||||
input: InputId,
|
||||
) -> Option<&InputParam<DataType, ValueType>> {
|
||||
self.inputs.get(input)
|
||||
}
|
||||
|
||||
pub fn get_input(
|
||||
&self,
|
||||
input: InputId,
|
||||
) -> &InputParam<DataType, ValueType> {
|
||||
&self.inputs[input]
|
||||
}
|
||||
|
||||
pub fn try_get_output(
|
||||
&self,
|
||||
output: OutputId,
|
||||
) -> Option<&OutputParam<DataType>> {
|
||||
self.outputs.get(output)
|
||||
}
|
||||
|
||||
pub fn get_output(&self, output: OutputId) -> &OutputParam<DataType> {
|
||||
&self.outputs[output]
|
||||
}
|
||||
}
|
||||
|
||||
impl<NodeData, DataType, ValueType> Default
|
||||
for Graph<NodeData, DataType, ValueType>
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<NodeData> Node<NodeData> {
|
||||
pub fn inputs<'a, DataType, DataValue>(
|
||||
&'a self,
|
||||
graph: &'a Graph<NodeData, DataType, DataValue>,
|
||||
) -> impl Iterator<Item = &'a InputParam<DataType, DataValue>> + 'a {
|
||||
self.input_ids().map(|id| graph.get_input(id))
|
||||
}
|
||||
|
||||
pub fn outputs<'a, DataType, DataValue>(
|
||||
&'a self,
|
||||
graph: &'a Graph<NodeData, DataType, DataValue>,
|
||||
) -> impl Iterator<Item = &'a OutputParam<DataType>> + 'a {
|
||||
self.output_ids().map(|id| graph.get_output(id))
|
||||
}
|
||||
|
||||
pub fn input_ids(&self) -> impl Iterator<Item = InputId> + '_ {
|
||||
self.inputs.iter().map(|(_name, id)| *id)
|
||||
}
|
||||
|
||||
pub fn output_ids(&self) -> impl Iterator<Item = OutputId> + '_ {
|
||||
self.outputs.iter().map(|(_name, id)| *id)
|
||||
}
|
||||
|
||||
pub fn get_input(&self, name: &str) -> Result<InputId, EguiGraphError> {
|
||||
self.inputs
|
||||
.iter()
|
||||
.find(|(param_name, _id)| param_name == name)
|
||||
.map(|x| x.1)
|
||||
.ok_or_else(|| {
|
||||
EguiGraphError::NoParameterNamed(self.id, name.into())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_output(&self, name: &str) -> Result<OutputId, EguiGraphError> {
|
||||
self.outputs
|
||||
.iter()
|
||||
.find(|(param_name, _id)| param_name == name)
|
||||
.map(|x| x.1)
|
||||
.ok_or_else(|| {
|
||||
EguiGraphError::NoParameterNamed(self.id, name.into())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<DataType, ValueType> InputParam<DataType, ValueType> {
|
||||
pub fn value(&self) -> &ValueType {
|
||||
&self.value
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> InputParamKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn node(&self) -> NodeId {
|
||||
self.node
|
||||
}
|
||||
}
|
||||
42
egui_node_graph2/src/id_type.rs
Normal file
42
egui_node_graph2/src/id_type.rs
Normal file
@ -0,0 +1,42 @@
|
||||
slotmap::new_key_type! { pub struct NodeId; }
|
||||
slotmap::new_key_type! { pub struct InputId; }
|
||||
slotmap::new_key_type! { pub struct OutputId; }
|
||||
|
||||
#[cfg_attr(
|
||||
feature = "persistence",
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum AnyParameterId {
|
||||
Input(InputId),
|
||||
Output(OutputId),
|
||||
}
|
||||
|
||||
impl AnyParameterId {
|
||||
pub fn assume_input(&self) -> InputId {
|
||||
match self {
|
||||
| AnyParameterId::Input(input) => *input,
|
||||
| AnyParameterId::Output(output) =>
|
||||
panic!("{:?} is not an InputId", output),
|
||||
}
|
||||
}
|
||||
pub fn assume_output(&self) -> OutputId {
|
||||
match self {
|
||||
| AnyParameterId::Output(output) => *output,
|
||||
| AnyParameterId::Input(input) =>
|
||||
panic!("{:?} is not an OutputId", input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OutputId> for AnyParameterId {
|
||||
fn from(output: OutputId) -> Self {
|
||||
Self::Output(output)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InputId> for AnyParameterId {
|
||||
fn from(input: InputId) -> Self {
|
||||
Self::Input(input)
|
||||
}
|
||||
}
|
||||
35
egui_node_graph2/src/index_impls.rs
Normal file
35
egui_node_graph2/src/index_impls.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use super::*;
|
||||
|
||||
macro_rules! impl_index_traits {
|
||||
($id_type:ty, $output_type:ty, $arena:ident) => {
|
||||
impl<A, B, C> std::ops::Index<$id_type> for Graph<A, B, C> {
|
||||
type Output = $output_type;
|
||||
|
||||
fn index(&self, index: $id_type) -> &Self::Output {
|
||||
self.$arena.get(index).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{} index error for {:?}. Has the value been deleted?",
|
||||
stringify!($id_type),
|
||||
index
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, C> std::ops::IndexMut<$id_type> for Graph<A, B, C> {
|
||||
fn index_mut(&mut self, index: $id_type) -> &mut Self::Output {
|
||||
self.$arena.get_mut(index).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{} index error for {:?}. Has the value been deleted?",
|
||||
stringify!($id_type),
|
||||
index
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_index_traits!(NodeId, Node<A>, nodes);
|
||||
impl_index_traits!(InputId, InputParam<B, C>, inputs);
|
||||
impl_index_traits!(OutputId, OutputParam<B>, outputs);
|
||||
47
egui_node_graph2/src/lib.rs
Normal file
47
egui_node_graph2/src/lib.rs
Normal file
@ -0,0 +1,47 @@
|
||||
#![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;
|
||||
mod scale;
|
||||
190
egui_node_graph2/src/node_finder.rs
Normal file
190
egui_node_graph2/src/node_finder.rs
Normal file
@ -0,0 +1,190 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use egui::*;
|
||||
|
||||
use crate::color_hex_utils::*;
|
||||
use crate::{CategoryTrait, NodeTemplateIter, NodeTemplateTrait};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(
|
||||
feature = "persistence",
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
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<NodeTemplate>,
|
||||
}
|
||||
|
||||
impl<NodeTemplate, NodeData, UserState, CategoryType> NodeFinder<NodeTemplate>
|
||||
where
|
||||
NodeTemplate: NodeTemplateTrait<
|
||||
NodeData = NodeData,
|
||||
UserState = UserState,
|
||||
CategoryType = CategoryType,
|
||||
>,
|
||||
CategoryType: CategoryTrait,
|
||||
{
|
||||
pub fn new_at(pos: Pos2) -> Self {
|
||||
NodeFinder {
|
||||
query: "".into(),
|
||||
position: Some(pos),
|
||||
just_spawned: true,
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the node selector panel with a search bar. Returns whether a node
|
||||
/// archetype was selected and, in that case, the finder should be hidden on
|
||||
/// the next frame.
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
|
||||
user_state: &mut UserState,
|
||||
) -> Option<NodeTemplate> {
|
||||
let background_color;
|
||||
let text_color;
|
||||
|
||||
if ui.visuals().dark_mode {
|
||||
background_color = color_from_hex("#3f3f3f").unwrap();
|
||||
text_color = color_from_hex("#fefefe").unwrap();
|
||||
} else {
|
||||
background_color = color_from_hex("#fefefe").unwrap();
|
||||
text_color = color_from_hex("#3f3f3f").unwrap();
|
||||
}
|
||||
|
||||
ui.visuals_mut().widgets.noninteractive.fg_stroke =
|
||||
Stroke::new(2.0, text_color);
|
||||
|
||||
let frame = Frame::dark_canvas(ui.style())
|
||||
.fill(background_color)
|
||||
.inner_margin(vec2(5.0, 5.0));
|
||||
|
||||
// The archetype that will be returned.
|
||||
let mut submitted_archetype = None;
|
||||
frame.show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
let resp = ui.text_edit_singleline(&mut self.query);
|
||||
if self.just_spawned {
|
||||
resp.request_focus();
|
||||
self.just_spawned = false;
|
||||
}
|
||||
let update_open = resp.changed();
|
||||
|
||||
let mut query_submit = resp.lost_focus()
|
||||
&& ui.input(|i| i.key_pressed(Key::Enter));
|
||||
|
||||
let max_height =
|
||||
ui.input(|i| f32::max(i.screen_rect.height() * 0.5, 200.));
|
||||
let scroll_area_width = resp.rect.width();
|
||||
|
||||
let all_kinds = all_kinds.all_kinds();
|
||||
let mut categories: BTreeMap<String, Vec<&NodeTemplate>> =
|
||||
Default::default();
|
||||
let mut orphan_kinds = Vec::new();
|
||||
|
||||
for kind in &all_kinds {
|
||||
let kind_categories =
|
||||
kind.node_finder_categories(user_state);
|
||||
|
||||
if kind_categories.is_empty() {
|
||||
orphan_kinds.push(kind);
|
||||
} else {
|
||||
for category in kind_categories {
|
||||
categories
|
||||
.entry(category.name())
|
||||
.or_default()
|
||||
.push(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame::default().inner_margin(vec2(10.0, 10.0)).show(
|
||||
ui,
|
||||
|ui| {
|
||||
ScrollArea::vertical()
|
||||
.max_height(max_height)
|
||||
.auto_shrink([true, false])
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(scroll_area_width);
|
||||
for (category, kinds) in categories {
|
||||
let filtered_kinds: Vec<_> = kinds
|
||||
.into_iter()
|
||||
.map(|kind| {
|
||||
let kind_name = kind
|
||||
.node_finder_label(user_state)
|
||||
.to_string();
|
||||
(kind, kind_name)
|
||||
})
|
||||
.filter(|(_kind, kind_name)| {
|
||||
kind_name.to_lowercase().contains(
|
||||
self.query
|
||||
.to_lowercase()
|
||||
.as_str(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !filtered_kinds.is_empty() {
|
||||
let default_open =
|
||||
!self.query.is_empty();
|
||||
|
||||
CollapsingHeader::new(&category)
|
||||
.default_open(default_open)
|
||||
.open(
|
||||
update_open
|
||||
.then_some(default_open),
|
||||
)
|
||||
.show(ui, |ui| {
|
||||
for (kind, kind_name) in
|
||||
filtered_kinds
|
||||
{
|
||||
if ui
|
||||
.selectable_label(
|
||||
false, kind_name,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
submitted_archetype =
|
||||
Some(kind.clone());
|
||||
} else if query_submit {
|
||||
submitted_archetype =
|
||||
Some(kind.clone());
|
||||
query_submit = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for kind in orphan_kinds {
|
||||
let kind_name = kind
|
||||
.node_finder_label(user_state)
|
||||
.to_string();
|
||||
|
||||
if ui
|
||||
.selectable_label(false, kind_name)
|
||||
.clicked()
|
||||
{
|
||||
submitted_archetype =
|
||||
Some(kind.clone());
|
||||
} else if query_submit {
|
||||
submitted_archetype =
|
||||
Some(kind.clone());
|
||||
query_submit = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
submitted_archetype
|
||||
}
|
||||
}
|
||||
128
egui_node_graph2/src/scale.rs
Normal file
128
egui_node_graph2/src/scale.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use egui::epaint::{CornerRadiusF32, Shadow};
|
||||
use egui::style::WidgetVisuals;
|
||||
use egui::{CornerRadius, Margin, Stroke, Style, Vec2};
|
||||
|
||||
// Copied from https://github.com/gzp-crey/shine
|
||||
|
||||
pub trait Scale {
|
||||
fn scale(&mut self, amount: f32);
|
||||
|
||||
fn scaled(&self, amount: f32) -> Self
|
||||
where
|
||||
Self: Clone,
|
||||
{
|
||||
let mut scaled = self.clone();
|
||||
scaled.scale(amount);
|
||||
scaled
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for Vec2 {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
self.x *= amount;
|
||||
self.y *= amount;
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for Margin {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
self.left = (self.leftf() * amount).round() as _;
|
||||
self.right = (self.rightf() * amount).round() as _;
|
||||
self.top = (self.topf() * amount).round() as _;
|
||||
self.bottom = (self.bottomf() * amount).round() as _;
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for CornerRadiusF32 {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
self.ne *= amount;
|
||||
self.nw *= amount;
|
||||
self.se *= amount;
|
||||
self.sw *= amount;
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for CornerRadius {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
self.nw = (self.nw as f32 * amount) as u8;
|
||||
self.ne = (self.ne as f32 * amount) as u8;
|
||||
self.sw = (self.sw as f32 * amount) as u8;
|
||||
self.se = (self.se as f32 * amount) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for Stroke {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
self.width *= amount;
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for Shadow {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
self.spread =
|
||||
((self.spread as f32) * amount.clamp(0.4, 1.)).round() as _;
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for WidgetVisuals {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
self.bg_stroke.scale(amount);
|
||||
self.fg_stroke.scale(amount);
|
||||
self.corner_radius.scale(amount);
|
||||
self.expansion *= amount.clamp(0.4, 1.);
|
||||
}
|
||||
}
|
||||
|
||||
impl Scale for Style {
|
||||
fn scale(&mut self, amount: f32) {
|
||||
if let Some(ov_font_id) = &mut self.override_font_id {
|
||||
ov_font_id.size *= amount;
|
||||
}
|
||||
|
||||
for text_style in self.text_styles.values_mut() {
|
||||
text_style.size *= amount;
|
||||
}
|
||||
|
||||
self.spacing.item_spacing.scale(amount);
|
||||
self.spacing.window_margin.scale(amount);
|
||||
self.spacing.button_padding.scale(amount);
|
||||
self.spacing.menu_margin *= amount;
|
||||
self.spacing.indent *= amount;
|
||||
self.spacing.interact_size.scale(amount);
|
||||
self.spacing.slider_width *= amount;
|
||||
self.spacing.slider_rail_height *= amount;
|
||||
self.spacing.combo_width *= amount;
|
||||
self.spacing.text_edit_width *= amount;
|
||||
self.spacing.icon_width *= amount;
|
||||
self.spacing.icon_width_inner *= amount;
|
||||
self.spacing.icon_spacing *= amount;
|
||||
self.spacing.default_area_size *= amount;
|
||||
self.spacing.tooltip_width *= amount;
|
||||
self.spacing.menu_width *= amount;
|
||||
self.spacing.menu_spacing *= amount;
|
||||
self.spacing.combo_height *= amount;
|
||||
|
||||
self.spacing.scroll.bar_width *= amount;
|
||||
self.spacing.scroll.floating_allocated_width *= amount;
|
||||
self.spacing.scroll.floating_width *= amount;
|
||||
|
||||
self.interaction.resize_grab_radius_side *= amount;
|
||||
self.interaction.resize_grab_radius_corner *= amount;
|
||||
|
||||
self.visuals.widgets.noninteractive.scale(amount);
|
||||
self.visuals.widgets.inactive.scale(amount);
|
||||
self.visuals.widgets.hovered.scale(amount);
|
||||
self.visuals.widgets.active.scale(amount);
|
||||
self.visuals.widgets.open.scale(amount);
|
||||
|
||||
self.visuals.selection.stroke.scale(amount);
|
||||
|
||||
self.visuals.menu_corner_radius *= amount;
|
||||
self.visuals.resize_corner_size *= amount;
|
||||
self.visuals.text_cursor.stroke.width *= amount;
|
||||
self.visuals.clip_rect_margin *= amount;
|
||||
self.visuals.window_corner_radius.scale(amount);
|
||||
self.visuals.window_shadow.scale(amount);
|
||||
self.visuals.popup_shadow.scale(amount);
|
||||
}
|
||||
}
|
||||
290
egui_node_graph2/src/traits.rs
Normal file
290
egui_node_graph2/src/traits.rs
Normal file
@ -0,0 +1,290 @@
|
||||
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.
|
||||
///
|
||||
/// The [`Default`] trait bound is required to circumvent borrow checker issues
|
||||
/// using `std::mem::take` Otherwise, it would be impossible to pass the
|
||||
/// `node_data` parameter during `value_widget`. The default value is never
|
||||
/// used, so the implementation is not important, but it should be reasonably
|
||||
/// cheap to construct.
|
||||
pub trait WidgetValueTrait: Default {
|
||||
type Response;
|
||||
type UserState;
|
||||
type NodeData;
|
||||
|
||||
/// This method will be called for each input parameter with a widget with
|
||||
/// an disconnected input only. To display UI for connected inputs use
|
||||
/// [`WidgetValueTrait::value_widget_connected`]. The return value is a
|
||||
/// vector of custom response objects which can be used to implement
|
||||
/// handling of side effects. If unsure, the response Vec can be empty.
|
||||
fn value_widget(
|
||||
&mut self,
|
||||
param_name: &str,
|
||||
node_id: NodeId,
|
||||
ui: &mut egui::Ui,
|
||||
user_state: &mut Self::UserState,
|
||||
node_data: &Self::NodeData,
|
||||
) -> Vec<Self::Response>;
|
||||
|
||||
/// This method will be called for each input parameter with a widget with a
|
||||
/// connected input only. To display UI for diconnected inputs use
|
||||
/// [`WidgetValueTrait::value_widget`]. The return value is a vector of
|
||||
/// custom response objects which can be used to implement handling of
|
||||
/// side effects. If unsure, the response Vec can be empty.
|
||||
///
|
||||
/// Shows the input name label by default.
|
||||
fn value_widget_connected(
|
||||
&mut self,
|
||||
param_name: &str,
|
||||
_node_id: NodeId,
|
||||
ui: &mut egui::Ui,
|
||||
_user_state: &mut Self::UserState,
|
||||
_node_data: &Self::NodeData,
|
||||
) -> Vec<Self::Response> {
|
||||
ui.label(param_name);
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<UserState>: PartialEq + Eq {
|
||||
/// The associated port color of this datatype
|
||||
fn data_type_color(&self, user_state: &mut UserState) -> egui::Color32;
|
||||
|
||||
/// The name of this datatype. Return type is specified as Cow<str> because
|
||||
/// some implementations will need to allocate a new string to provide an
|
||||
/// answer while others won't.
|
||||
///
|
||||
/// ## Example (borrowed value)
|
||||
/// Use this when you can get the name of the datatype from its fields or as
|
||||
/// a &'static str. Prefer this method when possible.
|
||||
/// ```ignore
|
||||
/// pub struct DataType { name: String }
|
||||
///
|
||||
/// impl DataTypeTrait<()> for DataType {
|
||||
/// fn name(&self) -> std::borrow::Cow<str> {
|
||||
/// Cow::Borrowed(&self.name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Example (owned value)
|
||||
/// Use this when you can't derive the name of the datatype from its fields.
|
||||
/// ```ignore
|
||||
/// pub struct DataType { some_tag: i32 }
|
||||
///
|
||||
/// impl DataTypeTrait<()> for DataType {
|
||||
/// fn name(&self) -> std::borrow::Cow<str> {
|
||||
/// Cow::Owned(format!("Super amazing type #{}", self.some_tag))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn name(&self) -> std::borrow::Cow<'_, 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;
|
||||
/// Must be set to the custom user `DataType` type
|
||||
type DataType;
|
||||
/// Must be set to the custom user `ValueType` type
|
||||
type ValueType;
|
||||
|
||||
/// Additional UI elements to draw in the nodes, after the parameters.
|
||||
fn bottom_ui(
|
||||
&self,
|
||||
ui: &mut egui::Ui,
|
||||
node_id: NodeId,
|
||||
graph: &Graph<Self, Self::DataType, Self::ValueType>,
|
||||
user_state: &mut Self::UserState,
|
||||
) -> Vec<NodeResponse<Self::Response, Self>>
|
||||
where
|
||||
Self::Response: UserResponseTrait;
|
||||
|
||||
/// UI to draw on the top bar of the node.
|
||||
fn top_bar_ui(
|
||||
&self,
|
||||
_ui: &mut egui::Ui,
|
||||
_node_id: NodeId,
|
||||
_graph: &Graph<Self, Self::DataType, Self::ValueType>,
|
||||
_user_state: &mut Self::UserState,
|
||||
) -> Vec<NodeResponse<Self::Response, Self>>
|
||||
where
|
||||
Self::Response: UserResponseTrait,
|
||||
{
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// UI to draw for each output
|
||||
///
|
||||
/// Defaults to showing param_name as a simple label.
|
||||
fn output_ui(
|
||||
&self,
|
||||
ui: &mut egui::Ui,
|
||||
_node_id: NodeId,
|
||||
_graph: &Graph<Self, Self::DataType, Self::ValueType>,
|
||||
_user_state: &mut Self::UserState,
|
||||
param_name: &str,
|
||||
) -> Vec<NodeResponse<Self::Response, Self>>
|
||||
where
|
||||
Self::Response: UserResponseTrait,
|
||||
{
|
||||
ui.label(param_name);
|
||||
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Set background color on titlebar
|
||||
/// If the return value is None, the default color is set.
|
||||
fn titlebar_color(
|
||||
&self,
|
||||
_ui: &egui::Ui,
|
||||
_node_id: NodeId,
|
||||
_graph: &Graph<Self, Self::DataType, Self::ValueType>,
|
||||
_user_state: &mut Self::UserState,
|
||||
) -> Option<egui::Color32> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Separator to put between elements in the node.
|
||||
///
|
||||
/// Invoked between inputs, outputs and bottom UI. Useful for
|
||||
/// complicated UIs that start to lose structure without explicit
|
||||
/// separators. The `param_id` argument is the id of input or output
|
||||
/// *preceeding* the separator.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
fn separator(
|
||||
&self,
|
||||
_ui: &mut egui::Ui,
|
||||
_node_id: NodeId,
|
||||
_param_id: AnyParameterId,
|
||||
_graph: &Graph<Self, Self::DataType, Self::ValueType>,
|
||||
_user_state: &mut Self::UserState,
|
||||
) {
|
||||
}
|
||||
|
||||
fn can_delete(
|
||||
&self,
|
||||
_node_id: NodeId,
|
||||
_graph: &Graph<Self, Self::DataType, Self::ValueType>,
|
||||
_user_state: &mut Self::UserState,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) -> Vec<Self::Item>;
|
||||
}
|
||||
|
||||
/// Describes a category of nodes.
|
||||
///
|
||||
/// Used by [`NodeTemplateTrait::node_finder_categories`] to categorize nodes
|
||||
/// templates into groups.
|
||||
///
|
||||
/// If all nodes in a program are known beforehand, it's usefult to define
|
||||
/// an enum containing all categories and implement [`CategoryTrait`] for it.
|
||||
/// This will make it impossible to accidentally create a new category by
|
||||
/// mis-typing an existing one, like in the case of using string types.
|
||||
pub trait CategoryTrait {
|
||||
/// Name of the category.
|
||||
fn name(&self) -> String;
|
||||
}
|
||||
|
||||
impl CategoryTrait for () {
|
||||
fn name(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CategoryTrait for &str {
|
||||
fn name(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl CategoryTrait for String {
|
||||
fn name(&self) -> String {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
/// Must be set to the custom user `UserState` type
|
||||
type UserState;
|
||||
/// Must be a type that implements the [`CategoryTrait`] trait.
|
||||
///
|
||||
/// `&'static str` is a good default if you intend to simply type out
|
||||
/// the categories of your node. Use `()` if you don't need categories
|
||||
/// at all.
|
||||
type CategoryType;
|
||||
|
||||
/// Returns a descriptive name for the node kind, used in the node finder.
|
||||
///
|
||||
/// The return type is Cow<str> to allow returning owned or borrowed values
|
||||
/// more flexibly. Refer to the documentation for `DataTypeTrait::name` for
|
||||
/// more information
|
||||
fn node_finder_label(
|
||||
&self,
|
||||
user_state: &mut Self::UserState,
|
||||
) -> std::borrow::Cow<'_, str>;
|
||||
|
||||
/// Vec of categories to which the node belongs.
|
||||
///
|
||||
/// It's often useful to organize similar nodes into categories, which will
|
||||
/// then be used by the node finder to show a more manageable UI, especially
|
||||
/// if the node template are numerous.
|
||||
fn node_finder_categories(
|
||||
&self,
|
||||
_user_state: &mut Self::UserState,
|
||||
) -> Vec<Self::CategoryType> {
|
||||
Vec::default()
|
||||
}
|
||||
|
||||
/// Returns a descriptive name for the node kind, used in the graph.
|
||||
fn node_graph_label(&self, user_state: &mut Self::UserState) -> String;
|
||||
|
||||
/// Returns the user data for this node kind.
|
||||
fn user_data(&self, user_state: &mut Self::UserState) -> 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>,
|
||||
user_state: &mut Self::UserState,
|
||||
node_id: NodeId,
|
||||
);
|
||||
}
|
||||
|
||||
/// The custom user response types when drawing nodes in the graph must
|
||||
/// implement this trait.
|
||||
pub trait UserResponseTrait: Clone + std::fmt::Debug {}
|
||||
144
egui_node_graph2/src/ui_state.rs
Normal file
144
egui_node_graph2/src/ui_state.rs
Normal file
@ -0,0 +1,144 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Rect, Style, Ui, Vec2};
|
||||
#[cfg(feature = "persistence")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
use crate::scale::Scale;
|
||||
|
||||
const MIN_ZOOM: f32 = 0.2;
|
||||
const MAX_ZOOM: f32 = 2.0;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||
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.
|
||||
pub node_order: Vec<NodeId>,
|
||||
/// An ongoing connection interaction: The mouse has dragged away from a
|
||||
/// port and the user is holding the click
|
||||
pub connection_in_progress: Option<(NodeId, AnyParameterId)>,
|
||||
/// The currently selected node. Some interface actions depend on the
|
||||
/// currently selected node.
|
||||
pub selected_nodes: Vec<NodeId>,
|
||||
/// The mouse drag start position for an ongoing box selection.
|
||||
pub ongoing_box_selection: Option<egui::Pos2>,
|
||||
/// 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<NodeTemplate>>,
|
||||
/// The panning of the graph viewport.
|
||||
pub pan_zoom: PanZoom,
|
||||
pub _user_state: PhantomData<fn() -> UserState>,
|
||||
}
|
||||
|
||||
impl<NodeData, DataType, ValueType, NodeKind, UserState>
|
||||
GraphEditorState<NodeData, DataType, ValueType, NodeKind, UserState>
|
||||
{
|
||||
pub fn new(default_zoom: f32) -> Self {
|
||||
Self {
|
||||
pan_zoom: PanZoom::new(default_zoom),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<NodeData, DataType, ValueType, NodeKind, UserState> Default
|
||||
for GraphEditorState<NodeData, DataType, ValueType, NodeKind, UserState>
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
node_order: Default::default(),
|
||||
connection_in_progress: Default::default(),
|
||||
selected_nodes: Default::default(),
|
||||
ongoing_box_selection: Default::default(),
|
||||
node_positions: Default::default(),
|
||||
node_finder: Default::default(),
|
||||
pan_zoom: Default::default(),
|
||||
_user_state: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
fn _default_clip_rect() -> Rect {
|
||||
Rect::NOTHING
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||
pub struct PanZoom {
|
||||
pub pan: Vec2,
|
||||
pub zoom: f32,
|
||||
#[cfg_attr(
|
||||
feature = "persistence",
|
||||
serde(skip, default = "_default_clip_rect")
|
||||
)]
|
||||
pub clip_rect: Rect,
|
||||
#[cfg_attr(feature = "persistence", serde(skip, default))]
|
||||
pub zoomed_style: Arc<Style>,
|
||||
#[cfg_attr(feature = "persistence", serde(skip, default))]
|
||||
pub started: bool,
|
||||
}
|
||||
|
||||
impl Default for PanZoom {
|
||||
fn default() -> Self {
|
||||
PanZoom {
|
||||
pan: Vec2::ZERO,
|
||||
zoom: 1.0,
|
||||
clip_rect: Rect::NOTHING,
|
||||
zoomed_style: Default::default(),
|
||||
started: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PanZoom {
|
||||
pub fn new(zoom: f32) -> PanZoom {
|
||||
let style: Style = Default::default();
|
||||
PanZoom {
|
||||
pan: Vec2::ZERO,
|
||||
zoom,
|
||||
clip_rect: Rect::NOTHING,
|
||||
zoomed_style: Arc::new(style.scaled(1.0)),
|
||||
started: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zoom(
|
||||
&mut self,
|
||||
clip_rect: Rect,
|
||||
style: &Arc<Style>,
|
||||
zoom_delta: f32,
|
||||
) {
|
||||
self.clip_rect = clip_rect;
|
||||
let new_zoom = (self.zoom * zoom_delta).clamp(MIN_ZOOM, MAX_ZOOM);
|
||||
self.zoomed_style = Arc::new(style.scaled(new_zoom));
|
||||
self.zoom = new_zoom;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_zoomed<R, F>(
|
||||
default_style: Arc<Style>,
|
||||
zoomed_style: Arc<Style>,
|
||||
ui: &mut Ui,
|
||||
add_content: F,
|
||||
) -> R
|
||||
where
|
||||
F: FnOnce(&mut Ui) -> R,
|
||||
{
|
||||
*ui.style_mut() = (*zoomed_style).clone();
|
||||
let response = add_content(ui);
|
||||
*ui.style_mut() = (*default_style).clone();
|
||||
|
||||
response
|
||||
}
|
||||
15
egui_node_graph2/src/utils.rs
Normal file
15
egui_node_graph2/src/utils.rs
Normal file
@ -0,0 +1,15 @@
|
||||
pub trait ColorUtils {
|
||||
/// Multiplies the color rgb values by `factor`, keeping alpha untouched.
|
||||
fn lighten(&self, factor: f32) -> Self;
|
||||
}
|
||||
|
||||
impl ColorUtils for egui::Color32 {
|
||||
fn lighten(&self, factor: f32) -> Self {
|
||||
egui::Color32::from_rgba_premultiplied(
|
||||
(self.r() as f32 * factor) as u8,
|
||||
(self.g() as f32 * factor) as u8,
|
||||
(self.b() as f32 * factor) as u8,
|
||||
self.a(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user