Working application

This commit is contained in:
elvis
2025-10-17 21:42:41 +02:00
parent 3472a31584
commit 617af61d7c
35 changed files with 8725 additions and 0 deletions

View 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()
);
}
}

File diff suppressed because it is too large Load Diff

View 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),
}

View 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>>,
}

View 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
}
}

View 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)
}
}

View 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);

View 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;

View 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
}
}

View 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);
}
}

View 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 {}

View 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
}

View 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(),
)
}
}