Cleanup blackjack-specific responses. Add custom user responses

This commit is contained in:
Setzer22 2022-02-19 18:00:10 +01:00
parent a40d2343f3
commit db4c4f0f9f
5 changed files with 184 additions and 105 deletions

View File

@ -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

View File

@ -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> {

View File

@ -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::*;

View File

@ -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,
} }
} }
} }

View File

@ -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,
}
}
}
} }
} }