use crate::socket::{SocketKind, SocketResponses};
use crate::NodesCtx;
use std::hash::Hash;
use std::ops::{Deref, DerefMut};
pub fn egui_id(graph_id: egui::Id, node_id: NodeId) -> egui::Id {
graph_id.with(node_id.0)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NodeInteraction {
pub selected: bool,
pub in_selection_rect: bool,
pub hovered: bool,
}
pub struct Node {
id: NodeId,
inputs: usize,
outputs: usize,
collapsed: bool,
flow: egui::Direction,
socket_radius: f32,
socket_color: Option<egui::Color32>,
max_width: Option<f32>,
animation_time: f32,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct NodeId(pub u64);
pub struct NodeResponse<T> {
response: egui::InnerResponse<T>,
sockets: SocketResponses,
selection_changed: bool,
selected: bool,
removed: bool,
edge_event: Option<EdgeEvent>,
}
pub struct FramedResponse<T> {
pub inner: egui::InnerResponse<T>,
pub sockets: crate::SocketLayout,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum EdgeEvent {
Started { kind: SocketKind, index: usize },
Ended { kind: SocketKind, index: usize },
Cancelled,
}
pub struct NodeCtx<'a> {
ui: &'a mut egui::Ui,
interaction: NodeInteraction,
min_size: egui::Vec2,
graph_id: egui::Id,
node_id: NodeId,
immutable: bool,
flow: egui::Direction,
inputs: usize,
outputs: usize,
}
impl Node {
const COLLAPSED_SOCKET_GAP_FACTOR: f32 = 0.25;
pub fn new(id_src: impl Hash) -> Self {
Self::from_id(NodeId::new(id_src))
}
pub fn from_id(id: NodeId) -> Self {
Self {
id,
max_width: None,
socket_color: None,
inputs: 0,
outputs: 0,
collapsed: false,
flow: egui::Direction::LeftToRight,
socket_radius: 3.0,
animation_time: 0.1,
}
}
pub fn max_width(mut self, w: f32) -> Self {
self.max_width = Some(w);
self
}
pub fn inputs(mut self, n: usize) -> Self {
self.inputs = n;
self
}
pub fn outputs(mut self, n: usize) -> Self {
self.outputs = n;
self
}
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
}
pub fn flow(mut self, flow: egui::Direction) -> Self {
self.flow = flow;
self
}
pub fn socket_color(mut self, color: egui::Color32) -> Self {
self.socket_color = Some(color);
self
}
pub fn socket_radius(mut self, radius: f32) -> Self {
self.socket_radius = radius;
self
}
pub fn animation_time(mut self, time: f32) -> Self {
self.animation_time = time;
self
}
pub fn show<R>(
self,
ctx: &mut NodesCtx,
ui: &mut egui::Ui,
content: impl FnOnce(NodeCtx<'_>) -> FramedResponse<R>,
) -> NodeResponse<R> {
self.show_impl(ctx, ui, Box::new(content) as Box<_>)
}
fn show_impl<'a, R>(
self,
ctx: &mut NodesCtx,
ui: &mut egui::Ui,
content: Box<dyn FnOnce(NodeCtx<'_>) -> FramedResponse<R> + 'a>,
) -> NodeResponse<R> {
let layout = &mut ctx.layout;
ctx.visited.insert(self.id);
let target_pos_graph = layout.entry(self.id).or_insert_with(|| {
let clip_rect = ui.clip_rect();
let mut pos = clip_rect.center();
if ui.rect_contains_pointer(clip_rect) {
if let Some(ptr) = ui.response().hover_pos() {
pos = ptr;
}
}
egui::Pos2::new(pos.x, pos.y)
});
let is_selected = crate::is_node_selected(ui, ctx.graph_id, self.id);
let is_primary_down = ui.input(|i| i.pointer.primary_down());
let animation_time = if is_selected && is_primary_down {
0.0
} else {
self.animation_time
};
let pos_graph = {
let idx = ctx.graph_id.with(self.id.0).with("x");
let idy = ctx.graph_id.with(self.id.0).with("y");
let ctx = ui.ctx();
let x = ctx.animate_value_with_time(idx, target_pos_graph.x, animation_time);
let y = ctx.animate_value_with_time(idy, target_pos_graph.y, animation_time);
egui::Pos2::new(x, y)
};
let min_item_spacing = ui.spacing().item_spacing.x.min(ui.spacing().item_spacing.y);
let min_interact_len = ui
.spacing()
.interact_size
.x
.min(ui.spacing().interact_size.y);
let mut min_size = egui::Vec2::splat(min_interact_len);
let max_sockets = std::cmp::max(self.inputs, self.outputs);
let min_socket_gap = min_interact_len + min_item_spacing;
let win_corner_radius = ui.visuals().window_corner_radius.ne as f32;
let socket_padding = win_corner_radius + min_interact_len * 0.5;
if max_sockets > 1 {
let socket_gap_factor = if self.collapsed {
Self::COLLAPSED_SOCKET_GAP_FACTOR
} else {
1.0
};
let min_len = (max_sockets - 1) as f32 * min_socket_gap * socket_gap_factor
+ socket_padding * 2.0;
match self.flow {
egui::Direction::LeftToRight | egui::Direction::RightToLeft => {
min_size.y = min_size.y.max(min_len);
}
egui::Direction::TopDown | egui::Direction::BottomUp => {
min_size.x = min_size.x.max(min_len);
}
}
}
let max_w = self.max_width.unwrap_or(ui.spacing().text_edit_width);
let max_size = egui::Vec2::new(max_w, ctx.graph_rect.height());
let mut selection_changed = false;
let (mut selected, in_selection_rect) = {
let gmem_arc = crate::memory(ui, ctx.graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
let in_selection_rect = match ctx.selection_rect {
Some(sel_rect) if ui.input(|i| !i.modifiers.shift) => {
let size = gmem
.node_sizes
.get(&self.id)
.cloned()
.unwrap_or(egui::Vec2::ZERO);
let rect = egui::Rect::from_min_size(pos_graph, size);
sel_rect.intersects(rect)
}
_ => false,
};
if ctx.select {
if in_selection_rect && ui.input(|i| !i.modifiers.shift) {
selection_changed |= gmem.selection.nodes.insert(self.id);
} else if !ui.input(|i| i.modifiers.ctrl) {
selection_changed |= gmem.selection.nodes.remove(&self.id);
}
}
let selected = gmem.selection.nodes.contains(&self.id);
(selected, in_selection_rect)
};
let gap = egui::Vec2::splat(win_corner_radius * 2.0);
let content_min_size = min_size - gap;
let put_size = egui::Vec2::new(max_size.x, min_size.y);
let put_rect = egui::Rect::from_min_size(pos_graph, put_size);
let scene_layer = ui.layer_id();
let node_id = self.id;
let egui_id = egui_id(ctx.graph_id, node_id);
let socket_layer = egui::LayerId::new(scene_layer.order, egui_id.with("sockets"));
ui.ctx().set_sublayer(scene_layer, socket_layer);
if let Some(transform) = ui.ctx().layer_transform_to_global(scene_layer) {
ui.ctx().set_transform_layer(socket_layer, transform);
}
let frame_layer = egui::LayerId::new(scene_layer.order, egui_id);
ui.ctx().set_sublayer(scene_layer, frame_layer);
if let Some(transform) = ui.ctx().layer_transform_to_global(scene_layer) {
ui.ctx().set_transform_layer(frame_layer, transform);
}
let builder = egui::UiBuilder::new()
.max_rect(put_rect)
.layer_id(frame_layer)
.sense(egui::Sense::click_and_drag());
let immutable = ctx.immutable;
let inner_response = ui.scope_builder(builder, |ui| {
let hovered = ui.response().hovered();
let node_ctx = NodeCtx {
ui,
interaction: NodeInteraction {
selected,
in_selection_rect,
hovered,
},
min_size: content_min_size,
graph_id: ctx.graph_id,
node_id,
immutable,
flow: self.flow,
inputs: self.inputs,
outputs: self.outputs,
};
content(node_ctx)
});
let FramedResponse {
inner: content_inner_response,
sockets: socket_layout,
} = inner_response.inner;
let mut response = inner_response
.response
.union(content_inner_response.response);
let content_output = content_inner_response.inner;
let mut edge_event = None;
{
let gmem_arc = crate::memory(ui, ctx.graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.node_sizes.insert(self.id, response.rect.size());
let ctrl_down = ui.input(|i| i.modifiers.ctrl);
let pointer = &ui.input(|i| i.pointer.clone());
if response.is_pointer_button_down_on() && pointer.primary_pressed() {
let was_selected = gmem.selection.nodes.contains(&self.id);
if ctrl_down && was_selected {
selection_changed |= gmem.selection.nodes.remove(&self.id);
selected = false;
} else {
if !ctrl_down && !was_selected {
gmem.selection.changed |= !gmem.selection.nodes.is_empty();
gmem.selection.nodes.clear();
}
selection_changed |= gmem.selection.nodes.insert(self.id);
selected = true;
if !immutable && gmem.pressed.is_none() {
let ptr_graph = response.hover_pos().unwrap_or_default();
gmem.pressed = Some(crate::Pressed {
over_selection_at_origin: true,
origin_pos: ptr_graph,
current_pos: ptr_graph,
action: crate::PressAction::DragNodes {
node: Some(crate::PressedNode {
id: self.id,
position_at_origin: pos_graph,
}),
},
});
}
}
} else if !immutable
&& !response.is_pointer_button_down_on()
&& pointer.primary_pressed()
{
if let Some(ref pressed) = gmem.pressed {
if let crate::PressAction::Socket(socket) = pressed.action {
if self.id == socket.node {
let kind = socket.kind;
let index = socket.index;
edge_event = Some(EdgeEvent::Started { kind, index });
}
}
}
if !ctrl_down
&& !gmem
.pressed
.as_ref()
.map(|p| p.over_selection_at_origin)
.unwrap_or(false)
&& !gmem.selection.nodes.contains(&self.id)
{
selection_changed |= gmem.selection.nodes.remove(&self.id);
selected = false;
}
} else if !immutable {
if let Some(r) = ctx.socket_press_released {
if let Some(c) = gmem.closest_socket {
if r.kind == c.kind && self.id == r.node {
edge_event = Some(EdgeEvent::Cancelled);
} else if self.id == c.node && ui.input(|i| i.pointer.primary_released()) {
let kind = c.kind;
let index = c.index;
edge_event = Some(EdgeEvent::Ended { kind, index });
}
} else if edge_event.is_none() && self.id == r.node {
edge_event = Some(EdgeEvent::Cancelled);
}
}
}
}
let node_sockets = socket_layout.resolve(self.flow, response.rect, socket_padding);
let socket_color = self.socket_color.unwrap_or(ui.visuals().text_color());
let socket_responses = crate::socket::show(
ui,
ctx.graph_id,
self.id,
egui_id,
socket_layer,
response.rect,
&node_sockets,
socket_color,
self.socket_radius,
);
let removed = if !immutable
&& selected
&& !ui.ctx().egui_wants_keyboard_input()
&& ui.input(|i| i.key_pressed(egui::Key::Delete) | i.key_pressed(egui::Key::Backspace))
{
let gmem_arc = crate::memory(ui, ctx.graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
selection_changed |= gmem.selection.nodes.remove(&self.id);
selected = false;
true
} else {
false
};
if selection_changed {
let gmem_arc = crate::memory(ui, ctx.graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.selection.changed = true;
}
if selection_changed || removed || edge_event.is_some() {
response.mark_changed();
}
NodeResponse {
response: egui::InnerResponse::new(content_output, response),
sockets: socket_responses,
selection_changed,
selected,
removed,
edge_event,
}
}
}
impl NodeId {
pub fn new(id_src: impl Hash) -> Self {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
id_src.hash(&mut hasher);
NodeId(hasher.finish())
}
pub const fn from_u64(v: u64) -> Self {
NodeId(v)
}
pub const fn value(self) -> u64 {
self.0
}
}
impl From<u64> for NodeId {
fn from(v: u64) -> Self {
NodeId(v)
}
}
impl<R> NodeResponse<R> {
pub fn selection(&self) -> Option<bool> {
if self.selection_changed {
Some(self.selected)
} else {
None
}
}
pub fn selected(&self) -> bool {
self.selected
}
pub fn removed(&self) -> bool {
self.removed
}
pub fn inner(&self) -> &R {
&self.response.inner
}
pub fn inner_mut(&mut self) -> &mut R {
&mut self.response.inner
}
pub fn into_inner(self) -> egui::InnerResponse<R> {
self.response
}
pub fn edge_event(&self) -> Option<EdgeEvent> {
self.edge_event
}
pub fn sockets(&self) -> &SocketResponses {
&self.sockets
}
}
impl<R> Deref for NodeResponse<R> {
type Target = egui::Response;
fn deref(&self) -> &Self::Target {
&self.response.response
}
}
impl<R> DerefMut for NodeResponse<R> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.response.response
}
}
impl<'a> NodeCtx<'a> {
pub fn interaction(&self) -> NodeInteraction {
self.interaction
}
pub fn style(&self) -> &egui::Style {
self.ui.style()
}
pub fn graph_id(&self) -> egui::Id {
self.graph_id
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
pub fn egui_id(&self) -> egui::Id {
egui_id(self.graph_id(), self.node_id())
}
pub fn framed<T>(
self,
content: impl FnOnce(&mut egui::Ui, &mut crate::SocketLayout) -> T,
) -> FramedResponse<T> {
let frame = default_frame(self.style(), self.interaction);
self.framed_with(frame, content)
}
pub fn framed_with<T>(
self,
frame: egui::Frame,
content: impl FnOnce(&mut egui::Ui, &mut crate::SocketLayout) -> T,
) -> FramedResponse<T> {
let min_size = self.min_size;
let immutable = self.immutable;
let mut socket_layout =
crate::SocketLayout::evenly_spaced(self.flow, self.inputs, self.outputs);
let builder = egui::UiBuilder::new().sense(egui::Sense::click_and_drag());
let inner_response = frame.show(self.ui, |ui| {
ui.scope_builder(builder, |ui| {
ui.set_min_size(min_size);
if immutable {
ui.disable();
}
content(ui, &mut socket_layout)
})
});
let response = inner_response.response.union(inner_response.inner.response);
let content_output = inner_response.inner.inner;
FramedResponse {
inner: egui::InnerResponse::new(content_output, response),
sockets: socket_layout,
}
}
}
pub fn default_frame(style: &egui::Style, interaction: NodeInteraction) -> egui::Frame {
let mut frame = egui::Frame::window(style);
frame.shadow.offset = [2, 2];
frame.shadow.spread = (frame.shadow.spread as f32 * 0.25) as u8;
frame.stroke.width = style.visuals.selection.stroke.width;
if interaction.selected {
frame.stroke = style.visuals.selection.stroke;
} else if interaction.in_selection_rect {
let color = style.visuals.weak_text_color();
frame.shadow.color = color;
frame.stroke = style.visuals.selection.stroke;
frame.stroke.color = color;
}
frame
}