mirror of
https://github.com/eliasstepanik/egui_node_graph.git
synced 2026-01-11 22:08:28 +00:00
Cleanup blackjack-specific responses. Add custom user responses
This commit is contained in:
parent
a40d2343f3
commit
db4c4f0f9f
@ -9,25 +9,36 @@ use egui::*;
|
|||||||
|
|
||||||
pub type PortLocations = std::collections::HashMap<AnyParameterId, Pos2>;
|
pub type PortLocations = std::collections::HashMap<AnyParameterId, Pos2>;
|
||||||
|
|
||||||
pub enum DrawGraphNodeResponse {
|
pub trait UserResponseTrait: Clone + Copy + std::fmt::Debug + PartialEq + Eq {}
|
||||||
|
|
||||||
|
/// Nodes communicate certain events to the parent graph when drawn. There is
|
||||||
|
/// one special `User` variant which can be used by users as the return value
|
||||||
|
/// when executing some custom actions in the UI of the node.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum NodeResponse<UserResponse: UserResponseTrait> {
|
||||||
ConnectEventStarted(NodeId, AnyParameterId),
|
ConnectEventStarted(NodeId, AnyParameterId),
|
||||||
ConnectEventEnded(AnyParameterId),
|
ConnectEventEnded(AnyParameterId),
|
||||||
SetActiveNode(NodeId),
|
|
||||||
SelectNode(NodeId),
|
SelectNode(NodeId),
|
||||||
RunNodeSideEffect(NodeId),
|
|
||||||
ClearActiveNode,
|
|
||||||
DeleteNode(NodeId),
|
DeleteNode(NodeId),
|
||||||
DisconnectEvent(InputId),
|
DisconnectEvent(InputId),
|
||||||
/// Emitted when a node is interacted with, and should be raised
|
/// Emitted when a node is interacted with, and should be raised
|
||||||
RaiseNode(NodeId),
|
RaiseNode(NodeId),
|
||||||
|
User(UserResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The return value of [`draw_graph_editor`]. This value can be used to make
|
||||||
|
/// user code react to specific events that happened when drawing the graph.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GraphResponse<UserResponse: UserResponseTrait> {
|
||||||
|
pub node_responses: Vec<NodeResponse<UserResponse>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> {
|
pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> {
|
||||||
pub position: &'a mut Pos2,
|
pub position: &'a mut Pos2,
|
||||||
pub graph: &'a mut Graph<NodeData, DataType, ValueType>,
|
pub graph: &'a mut Graph<NodeData, DataType, ValueType>,
|
||||||
pub port_locations: &'a mut PortLocations,
|
pub port_locations: &'a mut PortLocations,
|
||||||
pub node_id: NodeId,
|
pub node_id: NodeId,
|
||||||
pub ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
pub ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||||
pub active: bool,
|
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub pan: egui::Vec2,
|
pub pan: egui::Vec2,
|
||||||
}
|
}
|
||||||
@ -44,18 +55,41 @@ pub trait DataTypeTrait: PartialEq + Eq {
|
|||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<NodeData, DataType, ValueType, NodeKind>
|
pub trait NodeDataTrait
|
||||||
GraphEditorState<NodeData, DataType, ValueType, NodeKind>
|
|
||||||
where
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
type Response;
|
||||||
|
type UserState;
|
||||||
|
|
||||||
|
/// Additional UI elements to draw in the nodes, after the parameters.
|
||||||
|
fn bottom_ui<DataType, ValueType>(
|
||||||
|
&self,
|
||||||
|
ui: &mut Ui,
|
||||||
|
node_id: NodeId,
|
||||||
|
graph: &Graph<Self, DataType, ValueType>,
|
||||||
|
user_state: &Self::UserState,
|
||||||
|
|
||||||
|
) -> Vec<NodeResponse<Self::Response>>
|
||||||
|
where
|
||||||
|
Self::Response: UserResponseTrait;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<NodeData, DataType, ValueType, NodeKind, UserResponse, UserState>
|
||||||
|
GraphEditorState<NodeData, DataType, ValueType, NodeKind, UserState>
|
||||||
|
where
|
||||||
|
NodeData: NodeDataTrait<Response = UserResponse, UserState = UserState>,
|
||||||
|
UserResponse: UserResponseTrait,
|
||||||
ValueType: InputParamWidget,
|
ValueType: InputParamWidget,
|
||||||
NodeKind: NodeKindTrait<NodeData = NodeData, DataType = DataType, ValueType = ValueType>,
|
NodeKind: NodeKindTrait<NodeData = NodeData, DataType = DataType, ValueType = ValueType>,
|
||||||
DataType: DataTypeTrait,
|
DataType: DataTypeTrait,
|
||||||
{
|
{
|
||||||
|
#[must_use]
|
||||||
pub fn draw_graph_editor(
|
pub fn draw_graph_editor(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &CtxRef,
|
ctx: &CtxRef,
|
||||||
all_kinds: impl NodeKindIter<Item = NodeKind>,
|
all_kinds: impl NodeKindIter<Item = NodeKind>,
|
||||||
) {
|
) -> GraphResponse<UserResponse> {
|
||||||
let mouse = &ctx.input().pointer;
|
let mouse = &ctx.input().pointer;
|
||||||
let cursor_pos = mouse.hover_pos().unwrap_or(Pos2::ZERO);
|
let cursor_pos = mouse.hover_pos().unwrap_or(Pos2::ZERO);
|
||||||
|
|
||||||
@ -64,7 +98,7 @@ where
|
|||||||
|
|
||||||
// The responses returned from node drawing have side effects that are best
|
// The responses returned from node drawing have side effects that are best
|
||||||
// executed at the end of this function.
|
// executed at the end of this function.
|
||||||
let mut delayed_responses: Vec<DrawGraphNodeResponse> = vec![];
|
let mut delayed_responses: Vec<NodeResponse<UserResponse>> = vec![];
|
||||||
|
|
||||||
// Used to detect when the background was clicked, to dismiss certain selfs
|
// Used to detect when the background was clicked, to dismiss certain selfs
|
||||||
let mut click_on_background = false;
|
let mut click_on_background = false;
|
||||||
@ -85,17 +119,13 @@ where
|
|||||||
port_locations: &mut port_locations,
|
port_locations: &mut port_locations,
|
||||||
node_id,
|
node_id,
|
||||||
ongoing_drag: self.connection_in_progress,
|
ongoing_drag: self.connection_in_progress,
|
||||||
active: self
|
|
||||||
.active_node
|
|
||||||
.map(|active| active == node_id)
|
|
||||||
.unwrap_or(false),
|
|
||||||
selected: self
|
selected: self
|
||||||
.selected_node
|
.selected_node
|
||||||
.map(|selected| selected == node_id)
|
.map(|selected| selected == node_id)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
pan: self.pan_zoom.pan,
|
pan: self.pan_zoom.pan,
|
||||||
}
|
}
|
||||||
.show(ui);
|
.show(ui, &self.user_state);
|
||||||
|
|
||||||
// Actions executed later
|
// Actions executed later
|
||||||
delayed_responses.extend(responses);
|
delayed_responses.extend(responses);
|
||||||
@ -153,12 +183,12 @@ where
|
|||||||
|
|
||||||
/* Handle responses from drawing nodes */
|
/* Handle responses from drawing nodes */
|
||||||
|
|
||||||
for response in delayed_responses {
|
for response in delayed_responses.iter().copied() {
|
||||||
match response {
|
match response {
|
||||||
DrawGraphNodeResponse::ConnectEventStarted(node_id, port) => {
|
NodeResponse::ConnectEventStarted(node_id, port) => {
|
||||||
self.connection_in_progress = Some((node_id, port));
|
self.connection_in_progress = Some((node_id, port));
|
||||||
}
|
}
|
||||||
DrawGraphNodeResponse::ConnectEventEnded(locator) => {
|
NodeResponse::ConnectEventEnded(locator) => {
|
||||||
let in_out = match (
|
let in_out = match (
|
||||||
self.connection_in_progress
|
self.connection_in_progress
|
||||||
.map(|(_node, param)| param)
|
.map(|(_node, param)| param)
|
||||||
@ -177,34 +207,19 @@ where
|
|||||||
self.graph.add_connection(output, input)
|
self.graph.add_connection(output, input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DrawGraphNodeResponse::SetActiveNode(node_id) => {
|
NodeResponse::SelectNode(node_id) => {
|
||||||
self.active_node = Some(node_id);
|
|
||||||
}
|
|
||||||
DrawGraphNodeResponse::SelectNode(node_id) => {
|
|
||||||
self.selected_node = Some(node_id);
|
self.selected_node = Some(node_id);
|
||||||
}
|
}
|
||||||
DrawGraphNodeResponse::ClearActiveNode => {
|
NodeResponse::DeleteNode(node_id) => {
|
||||||
self.active_node = None;
|
|
||||||
}
|
|
||||||
DrawGraphNodeResponse::RunNodeSideEffect(node_id) => {
|
|
||||||
self.run_side_effect = Some(node_id);
|
|
||||||
}
|
|
||||||
DrawGraphNodeResponse::DeleteNode(node_id) => {
|
|
||||||
self.graph.remove_node(node_id);
|
self.graph.remove_node(node_id);
|
||||||
self.node_positions.remove(node_id);
|
self.node_positions.remove(node_id);
|
||||||
// Make sure to not leave references to old nodes hanging
|
// Make sure to not leave references to old nodes hanging
|
||||||
if self.active_node.map(|x| x == node_id).unwrap_or(false) {
|
|
||||||
self.active_node = None;
|
|
||||||
}
|
|
||||||
if self.selected_node.map(|x| x == node_id).unwrap_or(false) {
|
if self.selected_node.map(|x| x == node_id).unwrap_or(false) {
|
||||||
self.selected_node = None;
|
self.selected_node = None;
|
||||||
}
|
}
|
||||||
if self.run_side_effect.map(|x| x == node_id).unwrap_or(false) {
|
|
||||||
self.run_side_effect = None;
|
|
||||||
}
|
|
||||||
self.node_order.retain(|id| *id != node_id);
|
self.node_order.retain(|id| *id != node_id);
|
||||||
}
|
}
|
||||||
DrawGraphNodeResponse::DisconnectEvent(input_id) => {
|
NodeResponse::DisconnectEvent(input_id) => {
|
||||||
let corresp_output = self
|
let corresp_output = self
|
||||||
.graph
|
.graph
|
||||||
.connection(input_id)
|
.connection(input_id)
|
||||||
@ -214,7 +229,7 @@ where
|
|||||||
self.connection_in_progress =
|
self.connection_in_progress =
|
||||||
Some((other_node, AnyParameterId::Output(corresp_output)));
|
Some((other_node, AnyParameterId::Output(corresp_output)));
|
||||||
}
|
}
|
||||||
DrawGraphNodeResponse::RaiseNode(node_id) => {
|
NodeResponse::RaiseNode(node_id) => {
|
||||||
let old_pos = self
|
let old_pos = self
|
||||||
.node_order
|
.node_order
|
||||||
.iter()
|
.iter()
|
||||||
@ -223,6 +238,9 @@ where
|
|||||||
self.node_order.remove(old_pos);
|
self.node_order.remove(old_pos);
|
||||||
self.node_order.push(node_id);
|
self.node_order.push(node_id);
|
||||||
}
|
}
|
||||||
|
NodeResponse::User(_) => {
|
||||||
|
// These are handled by the user code.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,34 +265,42 @@ where
|
|||||||
self.selected_node = None;
|
self.selected_node = None;
|
||||||
self.node_finder = None;
|
self.node_finder = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GraphResponse {
|
||||||
|
node_responses: delayed_responses,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, NodeData, DataType, ValueType> GraphNodeWidget<'a, NodeData, DataType, ValueType>
|
impl<'a, NodeData, DataType, ValueType, UserResponse, UserState>
|
||||||
|
GraphNodeWidget<'a, NodeData, DataType, ValueType>
|
||||||
where
|
where
|
||||||
|
NodeData: NodeDataTrait<Response = UserResponse, UserState = UserState>,
|
||||||
|
UserResponse: UserResponseTrait,
|
||||||
ValueType: InputParamWidget,
|
ValueType: InputParamWidget,
|
||||||
DataType: DataTypeTrait,
|
DataType: DataTypeTrait,
|
||||||
{
|
{
|
||||||
pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0];
|
pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0];
|
||||||
|
|
||||||
pub fn show(self, ui: &mut Ui) -> Vec<DrawGraphNodeResponse> {
|
pub fn show(
|
||||||
|
self,
|
||||||
|
ui: &mut Ui,
|
||||||
|
user_state: &UserState,
|
||||||
|
) -> Vec<NodeResponse<UserResponse>> {
|
||||||
let mut child_ui = ui.child_ui_with_id_source(
|
let mut child_ui = ui.child_ui_with_id_source(
|
||||||
Rect::from_min_size(*self.position + self.pan, Self::MAX_NODE_SIZE.into()),
|
Rect::from_min_size(*self.position + self.pan, Self::MAX_NODE_SIZE.into()),
|
||||||
Layout::default(),
|
Layout::default(),
|
||||||
self.node_id,
|
self.node_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
Self::show_graph_node(self, &mut child_ui)
|
Self::show_graph_node(self, &mut child_ui, user_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws this node. Also fills in the list of port locations with all of its ports.
|
/// Draws this node. Also fills in the list of port locations with all of its ports.
|
||||||
/// Returns a response showing whether a drag event was started.
|
/// Returns responses indicating multiple events.
|
||||||
/// Parameters:
|
fn show_graph_node(self, ui: &mut Ui, user_state: &UserState) -> Vec<NodeResponse<UserResponse>> {
|
||||||
/// - **ongoing_drag**: Is there a port drag event currently going on?
|
|
||||||
fn show_graph_node(self, ui: &mut Ui) -> Vec<DrawGraphNodeResponse> {
|
|
||||||
let margin = egui::vec2(15.0, 5.0);
|
let margin = egui::vec2(15.0, 5.0);
|
||||||
let _field_separation = 5.0;
|
let mut responses = Vec::new();
|
||||||
let mut responses = Vec::<DrawGraphNodeResponse>::new();
|
|
||||||
|
|
||||||
let background_color = color_from_hex("#3f3f3f").unwrap();
|
let background_color = color_from_hex("#3f3f3f").unwrap();
|
||||||
let titlebar_color = background_color.lighten(0.8);
|
let titlebar_color = background_color.lighten(0.8);
|
||||||
@ -333,33 +359,12 @@ where
|
|||||||
output_port_heights.push((height_before + height_after) / 2.0);
|
output_port_heights.push((height_before + height_after) / 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Restore button row
|
responses.extend(
|
||||||
// Button row
|
self.graph[self.node_id]
|
||||||
ui.horizontal(|ui| {
|
.user_data
|
||||||
// Show 'Enable' button for nodes that output a mesh
|
.bottom_ui(ui, self.node_id, self.graph, user_state)
|
||||||
if self.graph[self.node_id].can_be_enabled(self.graph) {
|
.into_iter(),
|
||||||
ui.horizontal(|ui| {
|
);
|
||||||
if !self.active {
|
|
||||||
if ui.button("👁 Set active").clicked() {
|
|
||||||
responses.push(DrawGraphNodeResponse::SetActiveNode(self.node_id));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let button = egui::Button::new(
|
|
||||||
RichText::new("👁 Active").color(egui::Color32::BLACK),
|
|
||||||
)
|
|
||||||
.fill(egui::Color32::GOLD);
|
|
||||||
if ui.add(button).clicked() {
|
|
||||||
responses.push(DrawGraphNodeResponse::ClearActiveNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Show 'Run' button for executable nodes
|
|
||||||
if self.graph[self.node_id].is_executable() && ui.button("⛭ Run").clicked() {
|
|
||||||
responses.push(DrawGraphNodeResponse::RunNodeSideEffect(self.node_id));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Second pass, iterate again to draw the ports. This happens outside
|
// Second pass, iterate again to draw the ports. This happens outside
|
||||||
@ -370,18 +375,19 @@ where
|
|||||||
let port_right = outer_rect.right();
|
let port_right = outer_rect.right();
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn draw_port<NodeData, DataType, ValueType>(
|
fn draw_port<NodeData, DataType, ValueType, UserResponse>(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
graph: &Graph<NodeData, DataType, ValueType>,
|
graph: &Graph<NodeData, DataType, ValueType>,
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
port_pos: Pos2,
|
port_pos: Pos2,
|
||||||
responses: &mut Vec<DrawGraphNodeResponse>,
|
responses: &mut Vec<NodeResponse<UserResponse>>,
|
||||||
param_id: AnyParameterId,
|
param_id: AnyParameterId,
|
||||||
port_locations: &mut PortLocations,
|
port_locations: &mut PortLocations,
|
||||||
ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||||||
is_connected_input: bool,
|
is_connected_input: bool,
|
||||||
) where
|
) where
|
||||||
DataType: DataTypeTrait,
|
DataType: DataTypeTrait,
|
||||||
|
UserResponse: UserResponseTrait,
|
||||||
{
|
{
|
||||||
let port_type = graph.any_param_type(param_id).unwrap();
|
let port_type = graph.any_param_type(param_id).unwrap();
|
||||||
|
|
||||||
@ -404,13 +410,9 @@ where
|
|||||||
|
|
||||||
if resp.drag_started() {
|
if resp.drag_started() {
|
||||||
if is_connected_input {
|
if is_connected_input {
|
||||||
responses.push(DrawGraphNodeResponse::DisconnectEvent(
|
responses.push(NodeResponse::DisconnectEvent(param_id.assume_input()));
|
||||||
param_id.assume_input(),
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
responses.push(DrawGraphNodeResponse::ConnectEventStarted(
|
responses.push(NodeResponse::ConnectEventStarted(node_id, param_id));
|
||||||
node_id, param_id,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,7 +423,7 @@ where
|
|||||||
&& resp.hovered()
|
&& resp.hovered()
|
||||||
&& ui.input().pointer.any_released()
|
&& ui.input().pointer.any_released()
|
||||||
{
|
{
|
||||||
responses.push(DrawGraphNodeResponse::ConnectEventEnded(param_id));
|
responses.push(NodeResponse::ConnectEventEnded(param_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -540,7 +542,7 @@ where
|
|||||||
|
|
||||||
// Titlebar buttons
|
// Titlebar buttons
|
||||||
if Self::close_button(ui, outer_rect).clicked() {
|
if Self::close_button(ui, outer_rect).clicked() {
|
||||||
responses.push(DrawGraphNodeResponse::DeleteNode(self.node_id));
|
responses.push(NodeResponse::DeleteNode(self.node_id));
|
||||||
};
|
};
|
||||||
|
|
||||||
let window_response = ui.interact(
|
let window_response = ui.interact(
|
||||||
@ -552,7 +554,7 @@ where
|
|||||||
// Movement
|
// Movement
|
||||||
*self.position += window_response.drag_delta();
|
*self.position += window_response.drag_delta();
|
||||||
if window_response.drag_delta().length_sq() > 0.0 {
|
if window_response.drag_delta().length_sq() > 0.0 {
|
||||||
responses.push(DrawGraphNodeResponse::RaiseNode(self.node_id));
|
responses.push(NodeResponse::RaiseNode(self.node_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node selection
|
// Node selection
|
||||||
@ -560,8 +562,8 @@ where
|
|||||||
// HACK: Only set the select response when no other response is active.
|
// HACK: Only set the select response when no other response is active.
|
||||||
// This prevents some issues.
|
// This prevents some issues.
|
||||||
if responses.is_empty() && window_response.clicked_by(PointerButton::Primary) {
|
if responses.is_empty() && window_response.clicked_by(PointerButton::Primary) {
|
||||||
responses.push(DrawGraphNodeResponse::SelectNode(self.node_id));
|
responses.push(NodeResponse::SelectNode(self.node_id));
|
||||||
responses.push(DrawGraphNodeResponse::RaiseNode(self.node_id));
|
responses.push(NodeResponse::RaiseNode(self.node_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
responses
|
responses
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
use egui::plot::Value;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl<NodeData, DataType, ValueType> Graph<NodeData, DataType, ValueType> {
|
impl<NodeData, DataType, ValueType> Graph<NodeData, DataType, ValueType> {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use crate::{color_hex_utils::*, Graph, Node, NodeId};
|
use crate::{color_hex_utils::*, Graph, NodeId};
|
||||||
|
|
||||||
use egui::*;
|
use egui::*;
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ pub struct PanZoom {
|
|||||||
pub zoom: f32,
|
pub zoom: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GraphEditorState<NodeData, DataType, ValueType, NodeKind> {
|
pub struct GraphEditorState<NodeData, DataType, ValueType, NodeKind, UserState> {
|
||||||
pub graph: Graph<NodeData, DataType, ValueType>,
|
pub graph: Graph<NodeData, DataType, ValueType>,
|
||||||
/// Nodes are drawn in this order. Draw order is important because nodes
|
/// Nodes are drawn in this order. Draw order is important because nodes
|
||||||
/// that are drawn last are on top.
|
/// that are drawn last are on top.
|
||||||
@ -18,9 +18,6 @@ pub struct GraphEditorState<NodeData, DataType, ValueType, NodeKind> {
|
|||||||
/// An ongoing connection interaction: The mouse has dragged away from a
|
/// An ongoing connection interaction: The mouse has dragged away from a
|
||||||
/// port and the user is holding the click
|
/// port and the user is holding the click
|
||||||
pub connection_in_progress: Option<(NodeId, AnyParameterId)>,
|
pub connection_in_progress: Option<(NodeId, AnyParameterId)>,
|
||||||
/// The currently active node. A program will be compiled to compute the
|
|
||||||
/// result of this node and constantly updated in real-time.
|
|
||||||
pub active_node: Option<NodeId>,
|
|
||||||
/// The currently selected node. Some interface actions depend on the
|
/// The currently selected node. Some interface actions depend on the
|
||||||
/// currently selected node.
|
/// currently selected node.
|
||||||
pub selected_node: Option<NodeId>,
|
pub selected_node: Option<NodeId>,
|
||||||
@ -28,30 +25,27 @@ pub struct GraphEditorState<NodeData, DataType, ValueType, NodeKind> {
|
|||||||
pub node_positions: SecondaryMap<NodeId, egui::Pos2>,
|
pub node_positions: SecondaryMap<NodeId, egui::Pos2>,
|
||||||
/// The node finder is used to create new nodes.
|
/// The node finder is used to create new nodes.
|
||||||
pub node_finder: Option<NodeFinder<NodeKind>>,
|
pub node_finder: Option<NodeFinder<NodeKind>>,
|
||||||
/// When this option is set by the UI, the side effect encoded by the node
|
|
||||||
/// will be executed at the start of the next frame.
|
|
||||||
pub run_side_effect: Option<NodeId>,
|
|
||||||
/// The panning of the graph viewport.
|
/// The panning of the graph viewport.
|
||||||
pub pan_zoom: PanZoom,
|
pub pan_zoom: PanZoom,
|
||||||
|
pub user_state: UserState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<NodeData, DataType, ValueType, NodeKind>
|
impl<NodeData, DataType, ValueType, NodeKind, UserState>
|
||||||
GraphEditorState<NodeData, DataType, ValueType, NodeKind>
|
GraphEditorState<NodeData, DataType, ValueType, NodeKind, UserState>
|
||||||
{
|
{
|
||||||
pub fn new(default_zoom: f32) -> Self {
|
pub fn new(default_zoom: f32, user_state: UserState) -> Self {
|
||||||
Self {
|
Self {
|
||||||
graph: Graph::new(),
|
graph: Graph::new(),
|
||||||
node_order: Vec::new(),
|
node_order: Vec::new(),
|
||||||
connection_in_progress: None,
|
connection_in_progress: None,
|
||||||
active_node: None,
|
|
||||||
selected_node: None,
|
selected_node: None,
|
||||||
run_side_effect: None,
|
|
||||||
node_positions: SecondaryMap::new(),
|
node_positions: SecondaryMap::new(),
|
||||||
node_finder: None,
|
node_finder: None,
|
||||||
pan_zoom: PanZoom {
|
pan_zoom: PanZoom {
|
||||||
pan: egui::Vec2::ZERO,
|
pan: egui::Vec2::ZERO,
|
||||||
zoom: default_zoom,
|
zoom: default_zoom,
|
||||||
},
|
},
|
||||||
|
user_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,24 @@ pub enum MyNodeKind {
|
|||||||
AddVector,
|
AddVector,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The response type is used to encode side-effects produced when drawing a
|
||||||
|
/// node in the graph. Most side-effects (creating new nodes, deleting existing
|
||||||
|
/// nodes, handling connections...) are already handled by the library, but this
|
||||||
|
/// mechanism allows creating additional side effects from user code.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum MyResponse {
|
||||||
|
SetActiveNode(NodeId),
|
||||||
|
ClearActiveNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The graph 'global' state. This state struct is passed around to the node and
|
||||||
|
/// parameter drawing callbacks. The contents of this struct are entirely up to
|
||||||
|
/// the user. For this example, we use it to keep track of the 'active' node.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MyGraphState {
|
||||||
|
pub active_node: Option<NodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
// =========== Then, you need to implement some traits ============
|
// =========== Then, you need to implement some traits ============
|
||||||
|
|
||||||
// A trait for the data types, to tell the library how to display them
|
// A trait for the data types, to tell the library how to display them
|
||||||
@ -220,26 +238,93 @@ impl InputParamWidget for MyValueType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UserResponseTrait for MyResponse {}
|
||||||
|
impl NodeDataTrait for MyNodeData {
|
||||||
|
type Response = MyResponse;
|
||||||
|
type UserState = MyGraphState;
|
||||||
|
|
||||||
|
// This method will be called when drawing each node. This allows adding
|
||||||
|
// extra ui elements inside the nodes. In this case, we create an "active"
|
||||||
|
// button which introduces the concept of having an active node in the
|
||||||
|
// graph. This is done entirely from user code with no modifications to the
|
||||||
|
// node graph library.
|
||||||
|
fn bottom_ui<DataType, ValueType>(
|
||||||
|
&self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
node_id: NodeId,
|
||||||
|
_graph: &Graph<MyNodeData, DataType, ValueType>,
|
||||||
|
user_state: &Self::UserState,
|
||||||
|
) -> Vec<NodeResponse<MyResponse>>
|
||||||
|
where
|
||||||
|
MyResponse: UserResponseTrait,
|
||||||
|
{
|
||||||
|
// This logic is entirely up to the user. In this case, we check if the
|
||||||
|
// current node we're drawing is the active one, by comparing against
|
||||||
|
// the value stored in the global user state, and draw different button
|
||||||
|
// UIs based on that.
|
||||||
|
|
||||||
|
let mut responses = vec![];
|
||||||
|
let is_active = user_state
|
||||||
|
.active_node
|
||||||
|
.map(|id| id == node_id)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// Pressing the button will emit a custom user response to either set,
|
||||||
|
// or clear the active node. These responses do nothing by themselves,
|
||||||
|
// the library only makes the responses available to you after the graph
|
||||||
|
// has been drawn. See below at the update method for an example.
|
||||||
|
if !is_active {
|
||||||
|
if ui.button("👁 Set active").clicked() {
|
||||||
|
responses.push(NodeResponse::User(MyResponse::SetActiveNode(node_id)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let button =
|
||||||
|
egui::Button::new(egui::RichText::new("👁 Active").color(egui::Color32::BLACK))
|
||||||
|
.fill(egui::Color32::GOLD);
|
||||||
|
if ui.add(button).clicked() {
|
||||||
|
responses.push(NodeResponse::User(MyResponse::ClearActiveNode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct NodeGraphExample {
|
pub struct NodeGraphExample {
|
||||||
state: GraphEditorState<MyNodeData, MyDataType, MyValueType, MyNodeKind>,
|
// The `GraphEditorState` is the top-level object. You "register" all your
|
||||||
|
// custom types by specifying it as its generic parameters.
|
||||||
|
state: GraphEditorState<MyNodeData, MyDataType, MyValueType, MyNodeKind, MyGraphState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NodeGraphExample {
|
impl Default for NodeGraphExample {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: GraphEditorState::new(1.0),
|
state: GraphEditorState::new(1.0, MyGraphState::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl epi::App for NodeGraphExample {
|
impl epi::App for NodeGraphExample {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"eframe template"
|
"Egui node graph example"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called each time the UI needs repainting, which may be many times per second.
|
/// Called each time the UI needs repainting, which may be many times per second.
|
||||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||||
fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) {
|
fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) {
|
||||||
self.state.draw_graph_editor(ctx, AllMyNodeKinds);
|
let graph_response = self.state.draw_graph_editor(ctx, AllMyNodeKinds);
|
||||||
|
for node_response in graph_response.node_responses {
|
||||||
|
// Here, we ignore all other graph events. But you may find
|
||||||
|
// some use for them. For example, by playing a sound when a new
|
||||||
|
// connection is created
|
||||||
|
if let NodeResponse::User(user_event) = node_response {
|
||||||
|
match user_event {
|
||||||
|
MyResponse::SetActiveNode(node) => {
|
||||||
|
self.state.user_state.active_node = Some(node)
|
||||||
|
}
|
||||||
|
MyResponse::ClearActiveNode => self.state.user_state.active_node = None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user