From db4c4f0f9ff7790bc0526873509a01dde9707741 Mon Sep 17 00:00:00 2001 From: Setzer22 Date: Sat, 19 Feb 2022 18:00:10 +0100 Subject: [PATCH] Cleanup blackjack-specific responses. Add custom user responses --- egui_node_graph/src/editor_ui.rs | 172 +++++++++++++++-------------- egui_node_graph/src/graph_impls.rs | 2 - egui_node_graph/src/node_finder.rs | 2 +- egui_node_graph/src/ui_state.rs | 18 +-- egui_node_graph_example/src/app.rs | 95 +++++++++++++++- 5 files changed, 184 insertions(+), 105 deletions(-) diff --git a/egui_node_graph/src/editor_ui.rs b/egui_node_graph/src/editor_ui.rs index d36ac29..7bc418f 100644 --- a/egui_node_graph/src/editor_ui.rs +++ b/egui_node_graph/src/editor_ui.rs @@ -9,25 +9,36 @@ use egui::*; pub type PortLocations = std::collections::HashMap; -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 { ConnectEventStarted(NodeId, AnyParameterId), ConnectEventEnded(AnyParameterId), - SetActiveNode(NodeId), SelectNode(NodeId), - RunNodeSideEffect(NodeId), - ClearActiveNode, DeleteNode(NodeId), DisconnectEvent(InputId), /// Emitted when a node is interacted with, and should be raised 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 { + pub node_responses: Vec>, +} + pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> { pub position: &'a mut Pos2, pub graph: &'a mut Graph, pub port_locations: &'a mut PortLocations, pub node_id: NodeId, pub ongoing_drag: Option<(NodeId, AnyParameterId)>, - pub active: bool, pub selected: bool, pub pan: egui::Vec2, } @@ -44,18 +55,41 @@ pub trait DataTypeTrait: PartialEq + Eq { fn name(&self) -> &str; } -impl - GraphEditorState +pub trait NodeDataTrait where + Self: Sized, +{ + type Response; + type UserState; + + /// Additional UI elements to draw in the nodes, after the parameters. + fn bottom_ui( + &self, + ui: &mut Ui, + node_id: NodeId, + graph: &Graph, + user_state: &Self::UserState, + + ) -> Vec> + where + Self::Response: UserResponseTrait; +} + +impl + GraphEditorState +where + NodeData: NodeDataTrait, + UserResponse: UserResponseTrait, ValueType: InputParamWidget, NodeKind: NodeKindTrait, DataType: DataTypeTrait, { + #[must_use] pub fn draw_graph_editor( &mut self, ctx: &CtxRef, all_kinds: impl NodeKindIter, - ) { + ) -> GraphResponse { let mouse = &ctx.input().pointer; 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 // executed at the end of this function. - let mut delayed_responses: Vec = vec![]; + let mut delayed_responses: Vec> = vec![]; // Used to detect when the background was clicked, to dismiss certain selfs let mut click_on_background = false; @@ -85,17 +119,13 @@ where port_locations: &mut port_locations, node_id, ongoing_drag: self.connection_in_progress, - active: self - .active_node - .map(|active| active == node_id) - .unwrap_or(false), selected: self .selected_node .map(|selected| selected == node_id) .unwrap_or(false), pan: self.pan_zoom.pan, } - .show(ui); + .show(ui, &self.user_state); // Actions executed later delayed_responses.extend(responses); @@ -153,12 +183,12 @@ where /* Handle responses from drawing nodes */ - for response in delayed_responses { + for response in delayed_responses.iter().copied() { match response { - DrawGraphNodeResponse::ConnectEventStarted(node_id, port) => { + NodeResponse::ConnectEventStarted(node_id, port) => { self.connection_in_progress = Some((node_id, port)); } - DrawGraphNodeResponse::ConnectEventEnded(locator) => { + NodeResponse::ConnectEventEnded(locator) => { let in_out = match ( self.connection_in_progress .map(|(_node, param)| param) @@ -177,34 +207,19 @@ where self.graph.add_connection(output, input) } } - DrawGraphNodeResponse::SetActiveNode(node_id) => { - self.active_node = Some(node_id); - } - DrawGraphNodeResponse::SelectNode(node_id) => { + NodeResponse::SelectNode(node_id) => { self.selected_node = Some(node_id); } - DrawGraphNodeResponse::ClearActiveNode => { - self.active_node = None; - } - DrawGraphNodeResponse::RunNodeSideEffect(node_id) => { - self.run_side_effect = Some(node_id); - } - DrawGraphNodeResponse::DeleteNode(node_id) => { + NodeResponse::DeleteNode(node_id) => { self.graph.remove_node(node_id); self.node_positions.remove(node_id); // 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) { 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); } - DrawGraphNodeResponse::DisconnectEvent(input_id) => { + NodeResponse::DisconnectEvent(input_id) => { let corresp_output = self .graph .connection(input_id) @@ -214,7 +229,7 @@ where self.connection_in_progress = Some((other_node, AnyParameterId::Output(corresp_output))); } - DrawGraphNodeResponse::RaiseNode(node_id) => { + NodeResponse::RaiseNode(node_id) => { let old_pos = self .node_order .iter() @@ -223,6 +238,9 @@ where self.node_order.remove(old_pos); 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.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 + NodeData: NodeDataTrait, + UserResponse: UserResponseTrait, ValueType: InputParamWidget, DataType: DataTypeTrait, { pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0]; - pub fn show(self, ui: &mut Ui) -> Vec { + pub fn show( + self, + ui: &mut Ui, + user_state: &UserState, + ) -> Vec> { let mut child_ui = ui.child_ui_with_id_source( Rect::from_min_size(*self.position + self.pan, Self::MAX_NODE_SIZE.into()), Layout::default(), 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. - /// Returns a response showing whether a drag event was started. - /// Parameters: - /// - **ongoing_drag**: Is there a port drag event currently going on? - fn show_graph_node(self, ui: &mut Ui) -> Vec { + /// Returns responses indicating multiple events. + fn show_graph_node(self, ui: &mut Ui, user_state: &UserState) -> Vec> { let margin = egui::vec2(15.0, 5.0); - let _field_separation = 5.0; - let mut responses = Vec::::new(); + let mut responses = Vec::new(); let background_color = color_from_hex("#3f3f3f").unwrap(); let titlebar_color = background_color.lighten(0.8); @@ -333,33 +359,12 @@ where output_port_heights.push((height_before + height_after) / 2.0); } - /* TODO: Restore button row - // Button row - ui.horizontal(|ui| { - // Show 'Enable' button for nodes that output a mesh - if self.graph[self.node_id].can_be_enabled(self.graph) { - 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)); - } - }) - */ + responses.extend( + self.graph[self.node_id] + .user_data + .bottom_ui(ui, self.node_id, self.graph, user_state) + .into_iter(), + ); }); // Second pass, iterate again to draw the ports. This happens outside @@ -370,18 +375,19 @@ where let port_right = outer_rect.right(); #[allow(clippy::too_many_arguments)] - fn draw_port( + fn draw_port( ui: &mut Ui, graph: &Graph, node_id: NodeId, port_pos: Pos2, - responses: &mut Vec, + responses: &mut Vec>, param_id: AnyParameterId, port_locations: &mut PortLocations, ongoing_drag: Option<(NodeId, AnyParameterId)>, is_connected_input: bool, ) where DataType: DataTypeTrait, + UserResponse: UserResponseTrait, { let port_type = graph.any_param_type(param_id).unwrap(); @@ -404,13 +410,9 @@ where if resp.drag_started() { if is_connected_input { - responses.push(DrawGraphNodeResponse::DisconnectEvent( - param_id.assume_input(), - )); + responses.push(NodeResponse::DisconnectEvent(param_id.assume_input())); } else { - responses.push(DrawGraphNodeResponse::ConnectEventStarted( - node_id, param_id, - )); + responses.push(NodeResponse::ConnectEventStarted(node_id, param_id)); } } @@ -421,7 +423,7 @@ where && resp.hovered() && ui.input().pointer.any_released() { - responses.push(DrawGraphNodeResponse::ConnectEventEnded(param_id)); + responses.push(NodeResponse::ConnectEventEnded(param_id)); } } } @@ -540,7 +542,7 @@ where // Titlebar buttons 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( @@ -552,7 +554,7 @@ where // Movement *self.position += window_response.drag_delta(); 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 @@ -560,8 +562,8 @@ where // HACK: Only set the select response when no other response is active. // This prevents some issues. if responses.is_empty() && window_response.clicked_by(PointerButton::Primary) { - responses.push(DrawGraphNodeResponse::SelectNode(self.node_id)); - responses.push(DrawGraphNodeResponse::RaiseNode(self.node_id)); + responses.push(NodeResponse::SelectNode(self.node_id)); + responses.push(NodeResponse::RaiseNode(self.node_id)); } responses diff --git a/egui_node_graph/src/graph_impls.rs b/egui_node_graph/src/graph_impls.rs index 667fae5..7f898a6 100644 --- a/egui_node_graph/src/graph_impls.rs +++ b/egui_node_graph/src/graph_impls.rs @@ -1,5 +1,3 @@ -use egui::plot::Value; - use super::*; impl Graph { diff --git a/egui_node_graph/src/node_finder.rs b/egui_node_graph/src/node_finder.rs index d6fb35f..24cb478 100644 --- a/egui_node_graph/src/node_finder.rs +++ b/egui_node_graph/src/node_finder.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use crate::{color_hex_utils::*, Graph, Node, NodeId}; +use crate::{color_hex_utils::*, Graph, NodeId}; use egui::*; diff --git a/egui_node_graph/src/ui_state.rs b/egui_node_graph/src/ui_state.rs index e6fe8b6..2905ffc 100644 --- a/egui_node_graph/src/ui_state.rs +++ b/egui_node_graph/src/ui_state.rs @@ -10,7 +10,7 @@ pub struct PanZoom { pub zoom: f32, } -pub struct GraphEditorState { +pub struct GraphEditorState { pub graph: Graph, /// Nodes are drawn in this order. Draw order is important because nodes /// that are drawn last are on top. @@ -18,9 +18,6 @@ pub struct GraphEditorState { /// 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 active node. A program will be compiled to compute the - /// result of this node and constantly updated in real-time. - pub active_node: Option, /// The currently selected node. Some interface actions depend on the /// currently selected node. pub selected_node: Option, @@ -28,30 +25,27 @@ pub struct GraphEditorState { pub node_positions: SecondaryMap, /// The node finder is used to create new nodes. pub node_finder: Option>, - /// 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, /// The panning of the graph viewport. pub pan_zoom: PanZoom, + pub user_state: UserState, } -impl - GraphEditorState +impl + GraphEditorState { - pub fn new(default_zoom: f32) -> Self { + pub fn new(default_zoom: f32, user_state: UserState) -> Self { Self { graph: Graph::new(), node_order: Vec::new(), connection_in_progress: None, - active_node: None, selected_node: None, - run_side_effect: None, node_positions: SecondaryMap::new(), node_finder: None, pan_zoom: PanZoom { pan: egui::Vec2::ZERO, zoom: default_zoom, }, + user_state, } } } diff --git a/egui_node_graph_example/src/app.rs b/egui_node_graph_example/src/app.rs index cc5f995..b8ac887 100644 --- a/egui_node_graph_example/src/app.rs +++ b/egui_node_graph_example/src/app.rs @@ -43,6 +43,24 @@ pub enum MyNodeKind { 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, +} + // =========== Then, you need to implement some traits ============ // 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( + &self, + ui: &mut egui::Ui, + node_id: NodeId, + _graph: &Graph, + user_state: &Self::UserState, + ) -> Vec> + 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 { - state: GraphEditorState, + // The `GraphEditorState` is the top-level object. You "register" all your + // custom types by specifying it as its generic parameters. + state: GraphEditorState, } impl Default for NodeGraphExample { fn default() -> Self { Self { - state: GraphEditorState::new(1.0), + state: GraphEditorState::new(1.0, MyGraphState::default()), } } } impl epi::App for NodeGraphExample { fn name(&self) -> &str { - "eframe template" + "Egui node graph example" } /// 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`. - fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) { - self.state.draw_graph_editor(ctx, AllMyNodeKinds); + fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) { + 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, + } + } + } } }