use crate::Theme;
use egui::{Color32, Pos2, Ui};
pub use egui_snarl::ui::{PinInfo, SnarlViewer};
pub use egui_snarl::{InPin, InPinId, NodeId, OutPin, OutPinId, Snarl};
#[derive(Debug, Clone)]
pub enum NodeGraphEvent<T> {
NodeAdded(NodeId),
NodeRemoved(NodeId, T),
NodeSelected(NodeId),
Connected { from: OutPinId, to: InPinId },
Disconnected { from: OutPinId, to: InPinId },
Action(String),
}
#[derive(Clone)]
pub struct MenuAction {
pub id: String,
pub label: String,
pub icon: Option<String>,
pub enabled: bool,
}
impl MenuAction {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
label: label.into(),
icon: None,
enabled: true,
}
}
pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
#[derive(Clone)]
pub struct NodeGraphStyle {
pub wire_width: Option<f32>,
pub pin_fill: Option<Color32>,
pub pin_size: Option<f32>,
}
impl Default for NodeGraphStyle {
fn default() -> Self {
Self {
wire_width: None,
pin_fill: None,
pin_size: None,
}
}
}
impl NodeGraphStyle {
pub fn new() -> Self {
Self::default()
}
pub fn wire_width(mut self, width: f32) -> Self {
self.wire_width = Some(width);
self
}
pub fn pin_fill(mut self, color: Color32) -> Self {
self.pin_fill = Some(color);
self
}
pub fn pin_size(mut self, size: f32) -> Self {
self.pin_size = Some(size);
self
}
fn build_snarl_style(&self, theme: &Theme) -> egui_snarl::ui::SnarlStyle {
let mut style = egui_snarl::ui::SnarlStyle::new();
style.wire_width = self.wire_width;
style.pin_fill = Some(self.pin_fill.unwrap_or(theme.primary));
style.pin_size = self.pin_size;
style
}
}
pub struct NodeGraph<T> {
inner: Snarl<T>,
style: NodeGraphStyle,
}
impl<T> Default for NodeGraph<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> NodeGraph<T> {
pub fn new() -> Self {
Self {
inner: Snarl::new(),
style: NodeGraphStyle::default(),
}
}
pub fn from_snarl(snarl: Snarl<T>) -> Self {
Self {
inner: snarl,
style: NodeGraphStyle::default(),
}
}
pub fn with_style(mut self, style: NodeGraphStyle) -> Self {
self.style = style;
self
}
pub fn inner(&self) -> &Snarl<T> {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut Snarl<T> {
&mut self.inner
}
pub fn style(&self) -> &NodeGraphStyle {
&self.style
}
pub fn style_mut(&mut self) -> &mut NodeGraphStyle {
&mut self.style
}
pub fn insert(&mut self, pos: Pos2, node: T) -> NodeId {
self.inner.insert_node(pos, node)
}
pub fn remove(&mut self, id: NodeId) -> T {
self.inner.remove_node(id)
}
pub fn try_remove(&mut self, id: NodeId) -> Option<T> {
if self.inner.get_node(id).is_some() {
Some(self.inner.remove_node(id))
} else {
None
}
}
pub fn get(&self, id: NodeId) -> Option<&T> {
self.inner.get_node(id)
}
pub fn get_mut(&mut self, id: NodeId) -> Option<&mut T> {
self.inner.get_node_mut(id)
}
pub fn connect(&mut self, from: OutPinId, to: InPinId) {
self.inner.connect(from, to);
}
pub fn disconnect(&mut self, from: OutPinId, to: InPinId) {
self.inner.disconnect(from, to);
}
pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &T)> + '_ {
self.inner.node_ids()
}
pub fn node_count(&self) -> usize {
self.inner.node_ids().count()
}
}
pub struct NodeGraphArea<'a, T> {
graph: &'a mut NodeGraph<T>,
graph_actions: Vec<MenuAction>,
node_actions: Vec<MenuAction>,
}
impl<'a, T> NodeGraphArea<'a, T> {
pub fn new(graph: &'a mut NodeGraph<T>) -> Self {
Self {
graph,
graph_actions: Vec::new(),
node_actions: Vec::new(),
}
}
pub fn graph_action(mut self, action: MenuAction) -> Self {
self.graph_actions.push(action);
self
}
pub fn graph_actions(mut self, actions: impl IntoIterator<Item = MenuAction>) -> Self {
self.graph_actions.extend(actions);
self
}
pub fn node_action(mut self, action: MenuAction) -> Self {
self.node_actions.push(action);
self
}
pub fn node_actions(mut self, actions: impl IntoIterator<Item = MenuAction>) -> Self {
self.node_actions.extend(actions);
self
}
pub fn show<V>(self, ui: &mut Ui, viewer: &mut V)
where
V: SnarlViewer<T>,
{
let theme = Theme::current(ui.ctx());
let snarl_style = self.graph.style.build_snarl_style(&theme);
self.graph
.inner
.show(viewer, &snarl_style, egui::Id::new("node_graph"), ui);
}
pub fn show_with<V, Msg>(
self,
ctx: &mut egui_cha::ViewCtx<'_, Msg>,
viewer: &mut V,
on_event: impl Fn(NodeGraphEvent<T>) -> Msg,
) where
V: SnarlViewer<T>,
T: Clone,
{
let theme = Theme::current(ctx.ui.ctx());
let snarl_style = self.graph.style.build_snarl_style(&theme);
let mut event_viewer = EventCapturingViewer {
inner: viewer,
events: Vec::new(),
graph_actions: &self.graph_actions,
node_actions: &self.node_actions,
};
self.graph.inner.show(
&mut event_viewer,
&snarl_style,
egui::Id::new("node_graph"),
ctx.ui,
);
for event in event_viewer.events {
ctx.emit(on_event(event));
}
}
}
struct EventCapturingViewer<'a, V, T> {
inner: &'a mut V,
events: Vec<NodeGraphEvent<T>>,
graph_actions: &'a [MenuAction],
node_actions: &'a [MenuAction],
}
impl<'a, V, T> SnarlViewer<T> for EventCapturingViewer<'a, V, T>
where
V: SnarlViewer<T>,
T: Clone,
{
fn title(&mut self, node: &T) -> String {
self.inner.title(node)
}
fn inputs(&mut self, node: &T) -> usize {
self.inner.inputs(node)
}
fn outputs(&mut self, node: &T) -> usize {
self.inner.outputs(node)
}
fn show_input(
&mut self,
pin: &InPin,
ui: &mut Ui,
snarl: &mut Snarl<T>,
) -> impl egui_snarl::ui::SnarlPin + 'static {
self.inner.show_input(pin, ui, snarl)
}
fn show_output(
&mut self,
pin: &OutPin,
ui: &mut Ui,
snarl: &mut Snarl<T>,
) -> impl egui_snarl::ui::SnarlPin + 'static {
self.inner.show_output(pin, ui, snarl)
}
fn connect(&mut self, from: &OutPin, to: &InPin, snarl: &mut Snarl<T>) {
self.events.push(NodeGraphEvent::Connected {
from: from.id,
to: to.id,
});
self.inner.connect(from, to, snarl);
}
fn disconnect(&mut self, from: &OutPin, to: &InPin, snarl: &mut Snarl<T>) {
self.events.push(NodeGraphEvent::Disconnected {
from: from.id,
to: to.id,
});
self.inner.disconnect(from, to, snarl);
}
fn has_graph_menu(&mut self, pos: Pos2, snarl: &mut Snarl<T>) -> bool {
!self.graph_actions.is_empty() || self.inner.has_graph_menu(pos, snarl)
}
fn show_graph_menu(&mut self, pos: Pos2, ui: &mut Ui, snarl: &mut Snarl<T>) {
for action in self.graph_actions {
let label = if let Some(icon) = &action.icon {
format!("{} {}", icon, action.label)
} else {
action.label.clone()
};
let button = ui.add_enabled(action.enabled, egui::Button::new(&label));
if button.clicked() {
self.events.push(NodeGraphEvent::Action(action.id.clone()));
ui.close();
}
}
if !self.graph_actions.is_empty() && self.inner.has_graph_menu(pos, snarl) {
ui.separator();
}
if self.inner.has_graph_menu(pos, snarl) {
self.inner.show_graph_menu(pos, ui, snarl);
}
}
fn has_node_menu(&mut self, node: &T) -> bool {
!self.node_actions.is_empty() || self.inner.has_node_menu(node)
}
fn show_node_menu(
&mut self,
node: NodeId,
_inputs: &[InPin],
_outputs: &[OutPin],
ui: &mut Ui,
snarl: &mut Snarl<T>,
) {
for action in self.node_actions {
let label = if let Some(icon) = &action.icon {
format!("{} {}", icon, action.label)
} else {
action.label.clone()
};
let button = ui.add_enabled(action.enabled, egui::Button::new(&label));
if button.clicked() {
self.events.push(NodeGraphEvent::Action(action.id.clone()));
ui.close();
}
}
if !self.node_actions.is_empty() {
ui.separator();
}
if ui.button("Delete Node").clicked() {
let removed = snarl.remove_node(node);
self.events.push(NodeGraphEvent::NodeRemoved(node, removed));
ui.close();
}
}
}
pub mod presets {
use super::*;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum PinType {
Flow,
Number,
Text,
Bool,
Color,
Texture,
Audio,
Video,
Midi,
Custom(u8),
}
impl PinType {
pub fn default_color(&self) -> Color32 {
match self {
PinType::Flow => Color32::WHITE,
PinType::Number => Color32::from_rgb(100, 200, 100),
PinType::Text => Color32::from_rgb(200, 200, 100),
PinType::Bool => Color32::from_rgb(200, 100, 100),
PinType::Color => Color32::from_rgb(200, 100, 200),
PinType::Texture => Color32::from_rgb(100, 150, 200),
PinType::Audio => Color32::from_rgb(100, 200, 200),
PinType::Video => Color32::from_rgb(200, 150, 100),
PinType::Midi => Color32::from_rgb(150, 100, 200),
PinType::Custom(_) => Color32::GRAY,
}
}
}
}