use std::collections::{BTreeMap, HashMap, HashSet};
use std::hash::Hash;
use std::sync::{Arc, Mutex};
#[cfg(feature = "layout")]
pub use layout::layout;
pub use node::{FramedResponse, NodeCtx, NodeId, NodeInteraction};
pub use socket::layout::{grid::SocketGrid, SocketLayout};
pub use socket::{SocketKind, SocketResponses};
pub mod bezier;
pub mod edge;
#[cfg(feature = "layout")]
pub mod layout;
pub mod node;
pub mod socket;
pub struct Graph {
background: bool,
dot_grid: bool,
zoom_range: egui::Rangef,
max_inner_size: Option<egui::Vec2>,
center_view: bool,
id: egui::Id,
selected_nodes: Option<HashSet<NodeId>>,
immutable: bool,
}
#[derive(Clone, Default)]
pub struct GraphTempMemory {
node_sizes: NodeSizes,
selection: Selection,
pressed: Option<Pressed>,
sockets: HashMap<NodeId, NodeSockets>,
closest_socket: Option<socket::Socket>,
}
type NodeSizes = HashMap<NodeId, egui::Vec2>;
#[derive(Clone, Default)]
struct Selection {
nodes: HashSet<NodeId>,
changed: bool,
}
#[derive(Clone, Debug)]
struct Pressed {
over_selection_at_origin: bool,
origin_pos: egui::Pos2,
current_pos: egui::Pos2,
action: PressAction,
}
#[derive(Clone, Debug)]
enum PressAction {
DragNodes {
node: Option<PressedNode>,
},
Select,
Socket(socket::Socket),
}
#[derive(Clone, Debug)]
struct PressedNode {
id: NodeId,
position_at_origin: egui::Pos2,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct View {
pub scene_rect: egui::Rect,
#[cfg_attr(feature = "serde", serde(serialize_with = "serialize_sorted_layout"))]
pub layout: Layout,
}
#[cfg(feature = "serde")]
fn serialize_sorted_layout<S: serde::Serializer>(layout: &Layout, s: S) -> Result<S::Ok, S::Error> {
use serde::Serialize;
let sorted: BTreeMap<_, _> = layout.iter().collect();
sorted.serialize(s)
}
pub type Layout = HashMap<NodeId, egui::Pos2>;
pub struct Show<'a> {
graph_id: egui::Id,
graph_rect: egui::Rect,
selection_rect: Option<egui::Rect>,
select: bool,
closest_socket: Option<socket::Socket>,
socket_press_released: Option<socket::Socket>,
visited: &'a mut HashSet<NodeId>,
layout: &'a mut Layout,
immutable: bool,
}
#[derive(Clone)]
pub struct NodeSockets {
flow: egui::Direction,
inputs: BTreeMap<usize, egui::Pos2>,
outputs: BTreeMap<usize, egui::Pos2>,
}
pub struct NodesCtx<'a> {
pub graph_id: egui::Id,
graph_rect: egui::Rect,
selection_rect: Option<egui::Rect>,
select: bool,
socket_press_released: Option<socket::Socket>,
visited: &'a mut HashSet<NodeId>,
layout: &'a mut Layout,
pub immutable: bool,
}
pub struct EdgesCtx {
graph_id: egui::Id,
graph_rect: egui::Rect,
selection_rect: Option<egui::Rect>,
closest_socket: Option<socket::Socket>,
pub immutable: bool,
}
struct GraphInteraction {
pressed: Option<Pressed>,
socket_press_released: Option<socket::Socket>,
select: bool,
selection_rect: Option<egui::Rect>,
drag_nodes_delta: egui::Vec2,
}
pub struct GraphResponse<R> {
pub inner: R,
pub response: egui::Response,
pub selection_changed: Option<HashSet<NodeId>>,
}
impl Graph {
pub const DEFAULT_ZOOM_RANGE: egui::Rangef = egui::Rangef {
min: 0.25,
max: 1.0,
};
pub const DEFAULT_CENTER_VIEW: bool = false;
pub fn new(id_src: impl Hash) -> Self {
Self::from_id(id(id_src))
}
pub fn from_id(id: egui::Id) -> Self {
Self {
background: true,
dot_grid: true,
zoom_range: Self::DEFAULT_ZOOM_RANGE,
max_inner_size: None,
center_view: Self::DEFAULT_CENTER_VIEW,
id,
selected_nodes: None,
immutable: false,
}
}
pub fn background(mut self, show: bool) -> Self {
self.background = show;
self
}
pub fn dot_grid(mut self, show: bool) -> Self {
self.dot_grid = show;
self
}
pub fn zoom_range(mut self, zoom_range: impl Into<egui::Rangef>) -> Self {
self.zoom_range = zoom_range.into();
self
}
#[inline]
pub fn max_inner_size(mut self, max_inner_size: impl Into<egui::Vec2>) -> Self {
self.max_inner_size = Some(max_inner_size.into());
self
}
pub fn center_view(mut self, center_view: bool) -> Self {
self.center_view = center_view;
self
}
pub fn selected_nodes(mut self, nodes: HashSet<NodeId>) -> Self {
self.selected_nodes = Some(nodes);
self
}
pub fn immutable(mut self, immutable: bool) -> Self {
self.immutable = immutable;
self
}
pub fn show<R>(
mut self,
view: &mut View,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui, Show) -> R,
) -> GraphResponse<R> {
let graph_rect = ui.available_rect_before_wrap();
let View {
ref mut scene_rect,
ref mut layout,
} = *view;
let mut scene = egui::containers::Scene::new()
.zoom_range(self.zoom_range)
.drag_pan_buttons(egui::containers::DragPanButtons::MIDDLE);
if let Some(max_inner_size) = self.max_inner_size {
scene = scene.max_inner_size(max_inner_size);
}
let mut bounding_rect = None;
let scene_response = scene.show(ui, scene_rect, |ui| {
let mut selection_rect = None;
let mut select = false;
let mut closest_socket = None;
let mut socket_press_released = None;
let scene_response = ui.response();
let ptr_on_graph = scene_response.hovered();
let gmem_arc = memory(ui, self.id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
if let Some(nodes) = self.selected_nodes.take() {
gmem.selection.nodes = nodes;
}
gmem.selection.changed = false;
let pointer = ui.input(|i| i.pointer.clone());
if let Some(ptr_global) = pointer.interact_pos().or(pointer.hover_pos()) {
let ptr_graph = ui
.ctx()
.layer_transform_from_global(ui.layer_id())
.unwrap_or_default()
.mul_pos(ptr_global);
closest_socket = ui.response().hover_pos().and_then(|pos| {
find_closest_socket(pos, layout, &gmem, ui).map(|(socket, _dist_sqrd)| socket)
});
let closest_socket_for_interaction =
if self.immutable { None } else { closest_socket };
let interaction = graph_interaction(
layout,
&pointer,
closest_socket_for_interaction,
ptr_on_graph,
ptr_graph,
gmem.pressed.as_ref(),
);
if !self.immutable && interaction.drag_nodes_delta != egui::Vec2::ZERO {
if let Some(pressed) = gmem.pressed.as_ref() {
if let PressAction::DragNodes { .. } = pressed.action {
for &n_id in &gmem.selection.nodes {
if let Some(pos) = layout.get_mut(&n_id) {
*pos += interaction.drag_nodes_delta;
}
}
}
}
}
gmem.pressed = interaction.pressed;
gmem.closest_socket = closest_socket;
selection_rect = interaction.selection_rect;
select = interaction.select;
socket_press_released = interaction.socket_press_released;
}
let visible_rect = ui.clip_rect();
if self.background {
paint_background(visible_rect, ui);
}
if self.dot_grid {
paint_dot_grid(visible_rect, ui);
}
if let Some(sel_rect) = selection_rect {
paint_selection_area(sel_rect, ui);
}
let mut visited = HashSet::default();
let show = Show {
graph_id: self.id,
graph_rect,
selection_rect,
select,
closest_socket,
socket_press_released,
visited: &mut visited,
layout,
immutable: self.immutable,
};
std::mem::drop(gmem);
let output = content(ui, show);
prune_unused_nodes(self.id, &visited, ui);
bounding_rect = Some(ui.min_rect());
let gmem_arc = memory(ui, self.id);
let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
let selection_changed = if gmem.selection.changed {
Some(gmem.selection.nodes.clone())
} else {
None
};
(output, selection_changed)
});
if self.center_view {
if let Some(rect) = bounding_rect {
view.scene_rect = rect.expand(rect.width() * 0.1);
}
}
let (inner, selection_changed) = scene_response.inner;
GraphResponse {
inner,
response: scene_response.response,
selection_changed,
}
}
}
impl GraphTempMemory {
pub fn node_sizes(&self) -> &NodeSizes {
&self.node_sizes
}
}
impl NodeSockets {
pub fn input(&self, ix: usize) -> Option<(egui::Pos2, egui::Vec2)> {
self.inputs
.get(&ix)
.map(|&pos| (pos, input_normal(self.flow)))
}
pub fn output(&self, ix: usize) -> Option<(egui::Pos2, egui::Vec2)> {
self.outputs
.get(&ix)
.map(|&pos| (pos, output_normal(self.flow)))
}
pub fn inputs(&self) -> impl Iterator<Item = (usize, egui::Pos2, egui::Vec2)> + '_ {
let norm = input_normal(self.flow);
self.inputs.iter().map(move |(&ix, &pos)| (ix, pos, norm))
}
pub fn outputs(&self) -> impl Iterator<Item = (usize, egui::Pos2, egui::Vec2)> + '_ {
let norm = output_normal(self.flow);
self.outputs.iter().map(move |(&ix, &pos)| (ix, pos, norm))
}
}
fn input_normal(flow: egui::Direction) -> egui::Vec2 {
match flow {
egui::Direction::LeftToRight => egui::Vec2::new(-1.0, 0.0),
egui::Direction::RightToLeft => egui::Vec2::new(1.0, 0.0),
egui::Direction::TopDown => egui::Vec2::new(0.0, -1.0),
egui::Direction::BottomUp => egui::Vec2::new(0.0, 1.0),
}
}
fn output_normal(flow: egui::Direction) -> egui::Vec2 {
match flow {
egui::Direction::LeftToRight => egui::Vec2::new(1.0, 0.0),
egui::Direction::RightToLeft => egui::Vec2::new(-1.0, 0.0),
egui::Direction::TopDown => egui::Vec2::new(0.0, 1.0),
egui::Direction::BottomUp => egui::Vec2::new(0.0, -1.0),
}
}
impl<'a> Show<'a> {
pub fn nodes(
mut self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut NodesCtx, &mut egui::Ui),
) -> Self {
{
let Self {
graph_id,
graph_rect,
selection_rect,
select,
socket_press_released,
ref mut visited,
ref mut layout,
immutable,
..
} = self;
let mut ctx = NodesCtx {
graph_id,
graph_rect,
selection_rect,
select,
socket_press_released,
visited,
layout,
immutable,
};
content(&mut ctx, ui);
}
self
}
pub fn edges(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut EdgesCtx, &mut egui::Ui),
) -> Self {
{
let Self {
graph_rect,
graph_id,
selection_rect,
closest_socket,
immutable,
..
} = self;
let mut ctx = EdgesCtx {
graph_id,
graph_rect,
selection_rect,
closest_socket,
immutable,
};
content(&mut ctx, ui);
}
self
}
}
fn prune_unused_nodes(graph_id: egui::Id, visited: &HashSet<NodeId>, ui: &mut egui::Ui) {
let gmem_arc = memory(ui, graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.node_sizes.retain(|k, _| visited.contains(k));
gmem.selection.nodes.retain(|k| visited.contains(k));
if let Some(socket) = gmem.closest_socket.as_ref() {
if !visited.contains(&socket.node) {
gmem.closest_socket = None;
}
}
if let Some(pressed) = gmem.pressed.as_ref() {
match pressed.action {
PressAction::DragNodes {
node: Some(PressedNode { id: n, .. }),
}
| PressAction::Socket(socket::Socket { node: n, .. })
if !visited.contains(&n) =>
{
gmem.pressed = None
}
_ => (),
}
}
}
impl EdgesCtx {
pub fn input(
&self,
ui: &egui::Ui,
node: NodeId,
input: usize,
) -> Option<(egui::Pos2, egui::Vec2)> {
let gmem_arc = crate::memory(ui, self.graph_id);
let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.sockets
.get(&node)
.and_then(|sockets| sockets.input(input))
}
pub fn output(
&self,
ui: &egui::Ui,
node: NodeId,
output: usize,
) -> Option<(egui::Pos2, egui::Vec2)> {
let gmem_arc = memory(ui, self.graph_id);
let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.sockets
.get(&node)
.and_then(|sockets| sockets.output(output))
}
pub fn in_progress(&self, ui: &egui::Ui) -> Option<EdgeInProgress> {
let gmem_arc = memory(ui, self.graph_id);
let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
let pressed = gmem.pressed.as_ref()?;
let start = match pressed.action {
PressAction::Socket(socket) => {
let sockets = gmem.sockets.get(&socket.node)?;
let (pos, normal) = match socket.kind {
socket::SocketKind::Input => sockets.input(socket.index)?,
socket::SocketKind::Output => sockets.output(socket.index)?,
};
socket::PositionedSocket {
socket,
pos,
normal,
}
}
_ => return None,
};
let (end_pos, end_socket) = match gmem.closest_socket {
Some(socket) if socket.kind != start.socket.kind => {
let sockets = gmem.sockets.get(&socket.node)?;
let (pos, normal) = match socket.kind {
socket::SocketKind::Input => sockets.input(socket.index)?,
socket::SocketKind::Output => sockets.output(socket.index)?,
};
(pos, Some((socket.kind, normal)))
}
_ => (pressed.current_pos, None),
};
Some(EdgeInProgress {
start,
end_pos,
end_socket,
})
}
pub fn graph_rect(&self) -> egui::Rect {
self.graph_rect
}
}
pub struct EdgeInProgress {
pub start: socket::PositionedSocket,
pub end_pos: egui::Pos2,
pub end_socket: Option<(socket::SocketKind, egui::Vec2)>,
}
impl EdgeInProgress {
pub fn bezier_cubic(&self, curvature: f32) -> bezier::Cubic {
let start = (self.start.pos, self.start.normal);
let end_normal = self
.end_socket
.as_ref()
.map(|&(_, n)| n)
.unwrap_or(-self.start.normal);
let end = (self.end_pos, end_normal);
bezier::Cubic::from_edge_points(start, end, curvature)
}
pub fn show(&self, ui: &egui::Ui, curvature: f32) {
let dist_per_pt = crate::edge::Edge::DEFAULT_DISTANCE_PER_POINT;
let bezier = self.bezier_cubic(curvature);
let pts = bezier.flatten(dist_per_pt).collect();
let stroke = ui.visuals().widgets.active.fg_stroke;
ui.painter().add(egui::Shape::line(pts, stroke));
}
}
impl Default for View {
fn default() -> Self {
Self {
scene_rect: egui::Rect::ZERO,
layout: Default::default(),
}
}
}
fn find_closest_socket(
pos_graph: egui::Pos2,
layout: &Layout,
gmem: &GraphTempMemory,
ui: &egui::Ui,
) -> Option<(socket::Socket, f32)> {
let mut closest_socket = None;
let socket_radius = ui
.spacing()
.interact_size
.x
.min(ui.spacing().interact_size.y);
let visible_rect = ui.clip_rect();
let socket_radius_sq = socket_radius * socket_radius;
for (&n_id, &n_graph) in layout {
let n_screen = n_graph;
let size = match gmem.node_sizes.get(&n_id) {
None => continue,
Some(&size) => size,
};
let rect = egui::Rect::from_min_size(n_screen, size);
if !visible_rect.intersects(rect) {
continue;
}
let sockets = match gmem.sockets.get(&n_id) {
None => continue,
Some(sockets) => sockets,
};
for (ix, p, _) in sockets.inputs() {
let dist_sq = pos_graph.distance_sq(p);
if dist_sq < socket_radius_sq {
let socket = socket::Socket {
node: n_id,
kind: socket::SocketKind::Input,
index: ix,
};
closest_socket = match closest_socket {
None => Some((socket, dist_sq)),
Some((_, d_sq)) if dist_sq < d_sq => Some((socket, dist_sq)),
_ => closest_socket,
}
}
}
for (ix, p, _) in sockets.outputs() {
let dist_sq = pos_graph.distance_sq(p);
if dist_sq < socket_radius_sq {
let socket = socket::Socket {
node: n_id,
kind: socket::SocketKind::Output,
index: ix,
};
closest_socket = match closest_socket {
None => Some((socket, dist_sq)),
Some((_, d_sq)) if dist_sq < d_sq => Some((socket, dist_sq)),
_ => closest_socket,
}
}
}
}
closest_socket
}
fn graph_interaction(
layout: &Layout,
pointer: &egui::PointerState,
closest_socket: Option<socket::Socket>,
ptr_on_graph: bool,
ptr_graph: egui::Pos2,
pressed: Option<&Pressed>,
) -> GraphInteraction {
let mut select = false;
let mut socket_press_released = None;
let mut drag_nodes_delta = egui::Vec2::ZERO;
let mut selection_rect = None;
let pressed: Option<Pressed> = if let Some(pressed) = pressed {
match pressed.action {
PressAction::DragNodes {
node: Some(ref node),
} => {
let delta = ptr_graph - pressed.origin_pos;
let target = node.position_at_origin + delta;
if let Some(current) = layout.get(&node.id) {
drag_nodes_delta = target - *current;
}
}
PressAction::Select => {
let min = pressed.origin_pos;
let max = ptr_graph;
selection_rect = Some(egui::Rect::from_two_pos(min, max));
}
_ => (),
}
if pointer.primary_released() {
match pressed.action {
PressAction::Select => select = true,
PressAction::Socket(socket) => socket_press_released = Some(socket),
_ => (),
}
None
} else {
Some(Pressed {
current_pos: ptr_graph,
..pressed.clone()
})
}
} else if ptr_on_graph
&& pointer.button_down(egui::PointerButton::Primary)
&& pointer.button_pressed(egui::PointerButton::Primary)
{
let action = match closest_socket {
Some(socket) => PressAction::Socket(socket),
None => {
let min = ptr_graph;
let max = ptr_graph;
selection_rect = Some(egui::Rect::from_two_pos(min, max));
PressAction::Select
}
};
let pressed = Pressed {
over_selection_at_origin: false,
origin_pos: ptr_graph,
current_pos: ptr_graph,
action,
};
Some(pressed)
} else {
pressed.cloned()
};
GraphInteraction {
pressed,
socket_press_released,
select,
selection_rect,
drag_nodes_delta,
}
}
fn paint_dot_grid(visible_rect: egui::Rect, ui: &mut egui::Ui) {
let dot_step = ui.spacing().interact_size.y;
let vis = ui.style().noninteractive();
let x_dots = (visible_rect.min.x / dot_step) as i32..=(visible_rect.max.x / dot_step) as i32;
let y_dots = (visible_rect.min.y / dot_step) as i32..=(visible_rect.max.y / dot_step) as i32;
for x_dot in x_dots {
for y_dot in y_dots.clone() {
let x = x_dot as f32 * dot_step;
let y = y_dot as f32 * dot_step;
let r = egui::Rect::from_center_size([x, y].into(), [1.0; 2].into());
let color = vis.bg_stroke.color;
ui.painter().circle_filled(r.center(), 0.5, color);
}
}
}
fn paint_background(visible_rect: egui::Rect, ui: &mut egui::Ui) {
let vis = ui.style().noninteractive();
let stroke = egui::Stroke {
width: 0.0,
..vis.bg_stroke
};
let fill = vis.bg_fill;
ui.painter()
.rect(visible_rect, 0.0, fill, stroke, egui::StrokeKind::Inside);
}
fn paint_selection_area(sel_rect: egui::Rect, ui: &mut egui::Ui) {
let color = ui.visuals().weak_text_color();
let fill = color.linear_multiply(0.125);
let width = 1.0;
let stroke = egui::Stroke { width, color };
ui.painter()
.rect(sel_rect, 0.0, fill, stroke, egui::StrokeKind::Inside);
}
pub fn id(id_src: impl Hash) -> egui::Id {
egui::Id::new((std::any::TypeId::of::<Graph>(), id_src))
}
pub fn with_graph_memory<R>(
ctx: &egui::Context,
graph_id: egui::Id,
f: impl FnOnce(&GraphTempMemory) -> R,
) -> R {
let gmem_arc = ctx.data_mut(|d| {
d.get_temp_mut_or_default::<Arc<Mutex<GraphTempMemory>>>(graph_id)
.clone()
});
let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
f(&gmem)
}
pub fn is_node_selected(ui: &egui::Ui, graph_id: egui::Id, node_id: NodeId) -> bool {
let gmem_arc = memory(ui, graph_id);
let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.selection.nodes.contains(&node_id)
}
fn memory(ui: &egui::Ui, graph_id: egui::Id) -> Arc<Mutex<GraphTempMemory>> {
ui.ctx().data_mut(|d| {
d.get_temp_mut_or_default::<Arc<Mutex<GraphTempMemory>>>(graph_id)
.clone()
})
}