1357 lines
50 KiB
Rust
1357 lines
50 KiB
Rust
|
|
use std::collections::HashSet;
|
||
|
|
use std::num::NonZeroU32;
|
||
|
|
|
||
|
|
use egui::epaint::{CornerRadiusF32, CubicBezierShape, RectShape};
|
||
|
|
use egui::*;
|
||
|
|
|
||
|
|
use super::*;
|
||
|
|
use crate::color_hex_utils::*;
|
||
|
|
use crate::utils::ColorUtils;
|
||
|
|
|
||
|
|
/// Mapping from parameter id to positions of hooks it contains.
|
||
|
|
///
|
||
|
|
/// Outputs and short inputs always only have one hook, so the value is
|
||
|
|
/// just `vec![port_position]`. Wide inputs may have multiple hooks.
|
||
|
|
pub type PortLocations = std::collections::HashMap<AnyParameterId, Vec<Pos2>>;
|
||
|
|
|
||
|
|
/// Destination positions of connections made to a given input.
|
||
|
|
///
|
||
|
|
/// This is not equivalent to [`PortLocations`] because connections may be moved
|
||
|
|
/// around (e.g. while an in-progress connection is hovered over a wide port),
|
||
|
|
/// while hooks within a port are strictly a function of the port.
|
||
|
|
pub type ConnLocations = std::collections::HashMap<InputId, Vec<Pos2>>;
|
||
|
|
|
||
|
|
/// Rectangle containing each node.
|
||
|
|
pub type NodeRects = std::collections::HashMap<NodeId, Rect>;
|
||
|
|
|
||
|
|
const DISTANCE_TO_CONNECT: f32 = 10.0;
|
||
|
|
|
||
|
|
/// 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, Debug)]
|
||
|
|
pub enum NodeResponse<UserResponse: UserResponseTrait, NodeData: NodeDataTrait>
|
||
|
|
{
|
||
|
|
ConnectEventStarted(NodeId, AnyParameterId),
|
||
|
|
ConnectEventEnded {
|
||
|
|
output: OutputId,
|
||
|
|
input: InputId,
|
||
|
|
/// Index of the connection in wide input ports.
|
||
|
|
///
|
||
|
|
/// If the input isn't a wide port this is always 0 and may be ignored.
|
||
|
|
input_hook: usize,
|
||
|
|
},
|
||
|
|
CreatedNode(NodeId),
|
||
|
|
SelectNode(NodeId),
|
||
|
|
/// As a user of this library, prefer listening for `DeleteNodeFull` which
|
||
|
|
/// will also contain the user data for the deleted node.
|
||
|
|
DeleteNodeUi(NodeId),
|
||
|
|
/// Emitted when a node is deleted. The node will no longer exist in the
|
||
|
|
/// graph after this response is returned from the draw function, but its
|
||
|
|
/// contents are passed along with the event.
|
||
|
|
DeleteNodeFull {
|
||
|
|
node_id: NodeId,
|
||
|
|
node: Node<NodeData>,
|
||
|
|
},
|
||
|
|
DisconnectEvent {
|
||
|
|
output: OutputId,
|
||
|
|
input: InputId,
|
||
|
|
},
|
||
|
|
/// Emitted when a node is interacted with, and should be raised
|
||
|
|
RaiseNode(NodeId),
|
||
|
|
MoveNode {
|
||
|
|
node: NodeId,
|
||
|
|
drag_delta: Vec2,
|
||
|
|
},
|
||
|
|
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,
|
||
|
|
NodeData: NodeDataTrait,
|
||
|
|
> {
|
||
|
|
/// Events that occurred during this frame of rendering the graph. Check
|
||
|
|
/// the [`UserResponse`] type for a description of each event.
|
||
|
|
pub node_responses: Vec<NodeResponse<UserResponse, NodeData>>,
|
||
|
|
/// Is the mouse currently hovering the graph editor? Note that the node
|
||
|
|
/// finder is considered part of the graph editor, even when it floats
|
||
|
|
/// outside the graph editor rect.
|
||
|
|
pub cursor_in_editor: bool,
|
||
|
|
/// Is the mouse currently hovering the node finder?
|
||
|
|
pub cursor_in_finder: bool,
|
||
|
|
}
|
||
|
|
impl<UserResponse: UserResponseTrait, NodeData: NodeDataTrait> Default
|
||
|
|
for GraphResponse<UserResponse, NodeData>
|
||
|
|
{
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
node_responses: Default::default(),
|
||
|
|
cursor_in_editor: false,
|
||
|
|
cursor_in_finder: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> {
|
||
|
|
pub position: &'a mut Pos2,
|
||
|
|
pub graph: &'a mut Graph<NodeData, DataType, ValueType>,
|
||
|
|
pub port_locations: &'a mut PortLocations,
|
||
|
|
pub conn_locations: &'a mut ConnLocations,
|
||
|
|
pub node_rects: &'a mut NodeRects,
|
||
|
|
pub node_id: NodeId,
|
||
|
|
pub ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||
|
|
pub selected: bool,
|
||
|
|
pub pan: egui::Vec2,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<
|
||
|
|
NodeData,
|
||
|
|
DataType,
|
||
|
|
ValueType,
|
||
|
|
NodeTemplate,
|
||
|
|
UserResponse,
|
||
|
|
UserState,
|
||
|
|
CategoryType,
|
||
|
|
> GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, UserState>
|
||
|
|
where
|
||
|
|
NodeData: NodeDataTrait<
|
||
|
|
Response = UserResponse,
|
||
|
|
UserState = UserState,
|
||
|
|
DataType = DataType,
|
||
|
|
ValueType = ValueType,
|
||
|
|
>,
|
||
|
|
UserResponse: UserResponseTrait,
|
||
|
|
ValueType: WidgetValueTrait<
|
||
|
|
Response = UserResponse,
|
||
|
|
UserState = UserState,
|
||
|
|
NodeData = NodeData,
|
||
|
|
>,
|
||
|
|
NodeTemplate: NodeTemplateTrait<
|
||
|
|
NodeData = NodeData,
|
||
|
|
DataType = DataType,
|
||
|
|
ValueType = ValueType,
|
||
|
|
UserState = UserState,
|
||
|
|
CategoryType = CategoryType,
|
||
|
|
>,
|
||
|
|
DataType: DataTypeTrait<UserState>,
|
||
|
|
CategoryType: CategoryTrait,
|
||
|
|
{
|
||
|
|
#[must_use]
|
||
|
|
pub fn draw_graph_editor(
|
||
|
|
&mut self,
|
||
|
|
ui: &mut Ui,
|
||
|
|
all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
|
||
|
|
user_state: &mut UserState,
|
||
|
|
prepend_responses: Vec<NodeResponse<UserResponse, NodeData>>,
|
||
|
|
) -> GraphResponse<UserResponse, NodeData> {
|
||
|
|
ui.set_clip_rect(ui.max_rect());
|
||
|
|
let clip_rect = ui.clip_rect();
|
||
|
|
// Zoom may have never taken place, so ensure we use parent style
|
||
|
|
if !self.pan_zoom.started {
|
||
|
|
self.zoom(ui, 1.0);
|
||
|
|
self.pan_zoom.started = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Zoom only within area where graph is shown
|
||
|
|
if ui.rect_contains_pointer(clip_rect) {
|
||
|
|
let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
|
||
|
|
if scroll_delta != 0.0 {
|
||
|
|
let zoom_delta = (scroll_delta * 0.002).exp();
|
||
|
|
self.zoom(ui, zoom_delta);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render graph zoomed
|
||
|
|
let zoomed_style = self.pan_zoom.zoomed_style.clone();
|
||
|
|
let graph_response =
|
||
|
|
show_zoomed(ui.style().clone(), zoomed_style, ui, |ui| {
|
||
|
|
self.draw_graph_editor_inside_zoom(
|
||
|
|
ui,
|
||
|
|
all_kinds,
|
||
|
|
user_state,
|
||
|
|
prepend_responses,
|
||
|
|
)
|
||
|
|
});
|
||
|
|
|
||
|
|
graph_response
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Reset zoom to 1.0
|
||
|
|
pub fn reset_zoom(&mut self, ui: &Ui) {
|
||
|
|
let new_zoom = 1.0 / self.pan_zoom.zoom;
|
||
|
|
self.zoom(ui, new_zoom);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Zoom within the where you call `draw_graph_editor`. Use values like
|
||
|
|
/// 1.01, or 0.99 to zoom. For example: `let zoom_delta = (scroll_delta
|
||
|
|
/// * 0.002).exp();`
|
||
|
|
pub fn zoom(&mut self, ui: &Ui, zoom_delta: f32) {
|
||
|
|
// Update zoom, and styles
|
||
|
|
let zoom_before = self.pan_zoom.zoom;
|
||
|
|
self.pan_zoom.zoom(ui.clip_rect(), ui.style(), zoom_delta);
|
||
|
|
if zoom_before != self.pan_zoom.zoom {
|
||
|
|
let actual_delta = self.pan_zoom.zoom / zoom_before;
|
||
|
|
self.update_node_positions_after_zoom(actual_delta);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn update_node_positions_after_zoom(&mut self, zoom_delta: f32) {
|
||
|
|
// Update node positions, zoom towards center
|
||
|
|
let half_size = self.pan_zoom.clip_rect.size() / 2.0;
|
||
|
|
for (_id, node_pos) in self.node_positions.iter_mut() {
|
||
|
|
// 1. Get node local position (relative to origo)
|
||
|
|
let local_pos = node_pos.to_vec2() - half_size + self.pan_zoom.pan;
|
||
|
|
// 2. Scale local position by zoom delta
|
||
|
|
let scaled_local_pos = (local_pos * zoom_delta).to_pos2();
|
||
|
|
// 3. Transform back to global position
|
||
|
|
*node_pos = scaled_local_pos + half_size - self.pan_zoom.pan;
|
||
|
|
// This way we can retain pan untouched when zooming :)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn draw_graph_editor_inside_zoom(
|
||
|
|
&mut self,
|
||
|
|
ui: &mut Ui,
|
||
|
|
all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
|
||
|
|
user_state: &mut UserState,
|
||
|
|
prepend_responses: Vec<NodeResponse<UserResponse, NodeData>>,
|
||
|
|
) -> GraphResponse<UserResponse, NodeData> {
|
||
|
|
// This causes the graph editor to use as much free space as it can.
|
||
|
|
// (so for windows it will use up to the resizeably set limit
|
||
|
|
// and for a Panel it will fill it completely)
|
||
|
|
let editor_rect = ui.max_rect();
|
||
|
|
let resp = ui.allocate_rect(editor_rect, Sense::hover());
|
||
|
|
|
||
|
|
let cursor_pos = ui
|
||
|
|
.ctx()
|
||
|
|
.input(|i| i.pointer.hover_pos().unwrap_or(Pos2::ZERO));
|
||
|
|
let mut cursor_in_editor = resp.contains_pointer();
|
||
|
|
let mut cursor_in_finder = false;
|
||
|
|
|
||
|
|
// Gets filled with the node metrics as they are drawn
|
||
|
|
let mut port_locations = PortLocations::new();
|
||
|
|
let mut node_rects = NodeRects::new();
|
||
|
|
|
||
|
|
// actual dest location of each connection
|
||
|
|
let mut conn_locations = ConnLocations::default();
|
||
|
|
|
||
|
|
// The responses returned from node drawing have side effects that are
|
||
|
|
// best executed at the end of this function.
|
||
|
|
let mut delayed_responses: Vec<NodeResponse<UserResponse, NodeData>> =
|
||
|
|
prepend_responses;
|
||
|
|
|
||
|
|
// Used to detect when the background was clicked
|
||
|
|
let mut click_on_background = false;
|
||
|
|
|
||
|
|
// Used to detect drag events in the background
|
||
|
|
let mut drag_started_on_background = false;
|
||
|
|
let mut drag_released_on_background = false;
|
||
|
|
|
||
|
|
debug_assert_eq!(
|
||
|
|
self.node_order.iter().copied().collect::<HashSet<_>>(),
|
||
|
|
self.graph.iter_nodes().collect::<HashSet<_>>(),
|
||
|
|
"The node_order field of the GraphEditorself was left in an \
|
||
|
|
inconsistent self. It has either more or less values than the graph."
|
||
|
|
);
|
||
|
|
|
||
|
|
// Allocate rect before the nodes, otherwise this will block the
|
||
|
|
// interaction with the nodes.
|
||
|
|
let r = ui
|
||
|
|
.allocate_rect(ui.min_rect(), Sense::click().union(Sense::drag()));
|
||
|
|
if r.clicked() {
|
||
|
|
click_on_background = true;
|
||
|
|
} else if r.drag_started() {
|
||
|
|
drag_started_on_background = true;
|
||
|
|
} else if r.drag_stopped() {
|
||
|
|
drag_released_on_background = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Draw nodes */
|
||
|
|
for node_id in self.node_order.iter().copied() {
|
||
|
|
let responses = GraphNodeWidget {
|
||
|
|
position: self.node_positions.get_mut(node_id).unwrap(),
|
||
|
|
graph: &mut self.graph,
|
||
|
|
port_locations: &mut port_locations,
|
||
|
|
conn_locations: &mut conn_locations,
|
||
|
|
node_rects: &mut node_rects,
|
||
|
|
node_id,
|
||
|
|
ongoing_drag: self.connection_in_progress,
|
||
|
|
selected: self.selected_nodes.contains(&node_id),
|
||
|
|
pan: self.pan_zoom.pan + editor_rect.min.to_vec2(),
|
||
|
|
}
|
||
|
|
.show(&self.pan_zoom, ui, user_state);
|
||
|
|
|
||
|
|
// Actions executed later
|
||
|
|
delayed_responses.extend(responses);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Draw the node finder, if open */
|
||
|
|
let mut should_close_node_finder = false;
|
||
|
|
if let Some(ref mut node_finder) = self.node_finder {
|
||
|
|
let mut node_finder_area =
|
||
|
|
Area::new(Id::new("node_finder")).order(Order::Foreground);
|
||
|
|
if let Some(pos) = node_finder.position {
|
||
|
|
node_finder_area = node_finder_area.current_pos(pos);
|
||
|
|
}
|
||
|
|
node_finder_area.show(ui.ctx(), |ui| {
|
||
|
|
if let Some(node_kind) =
|
||
|
|
node_finder.show(ui, all_kinds, user_state)
|
||
|
|
{
|
||
|
|
let new_node = self.graph.add_node(
|
||
|
|
node_kind.node_graph_label(user_state),
|
||
|
|
node_kind.user_data(user_state),
|
||
|
|
|graph, node_id| {
|
||
|
|
node_kind.build_node(graph, user_state, node_id)
|
||
|
|
},
|
||
|
|
);
|
||
|
|
self.node_positions.insert(
|
||
|
|
new_node,
|
||
|
|
node_finder.position.unwrap_or(cursor_pos)
|
||
|
|
- self.pan_zoom.pan
|
||
|
|
- editor_rect.min.to_vec2(),
|
||
|
|
);
|
||
|
|
self.node_order.push(new_node);
|
||
|
|
|
||
|
|
should_close_node_finder = true;
|
||
|
|
delayed_responses.push(NodeResponse::CreatedNode(new_node));
|
||
|
|
}
|
||
|
|
let finder_rect = ui.min_rect();
|
||
|
|
// If the cursor is not in the main editor, check if the cursor
|
||
|
|
// is in the finder if the cursor is in the
|
||
|
|
// finder, then we can consider that also in the editor.
|
||
|
|
if finder_rect.contains(cursor_pos) {
|
||
|
|
cursor_in_editor = true;
|
||
|
|
cursor_in_finder = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if should_close_node_finder {
|
||
|
|
self.node_finder = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
// draw in-progress connections
|
||
|
|
if let Some((_, ref locator)) = self.connection_in_progress {
|
||
|
|
let port_type = self.graph.any_param_type(*locator).unwrap();
|
||
|
|
let connection_color = port_type.data_type_color(user_state);
|
||
|
|
|
||
|
|
// outputs can't be wide yet so this is fine.
|
||
|
|
let start_pos = *port_locations[locator].last().unwrap();
|
||
|
|
|
||
|
|
// Find a port to connect to
|
||
|
|
fn snap_to_ports<
|
||
|
|
NodeData,
|
||
|
|
UserState,
|
||
|
|
DataType: DataTypeTrait<UserState>,
|
||
|
|
ValueType,
|
||
|
|
Key: slotmap::Key + Into<AnyParameterId>,
|
||
|
|
Value,
|
||
|
|
>(
|
||
|
|
graph: &Graph<NodeData, DataType, ValueType>,
|
||
|
|
port_type: &DataType,
|
||
|
|
ports: &SlotMap<Key, Value>,
|
||
|
|
port_locations: &PortLocations,
|
||
|
|
cursor_pos: Pos2,
|
||
|
|
) -> Pos2 {
|
||
|
|
ports
|
||
|
|
.iter()
|
||
|
|
.find_map(|(port_id, _)| {
|
||
|
|
let compatible_ports = graph
|
||
|
|
.any_param_type(port_id.into())
|
||
|
|
.map(|other| other == port_type)
|
||
|
|
.unwrap_or(false);
|
||
|
|
|
||
|
|
if compatible_ports {
|
||
|
|
port_locations.get(&port_id.into()).and_then(
|
||
|
|
|hooks| {
|
||
|
|
hooks
|
||
|
|
.iter()
|
||
|
|
.min_by(|hook1, hook2| {
|
||
|
|
hook1
|
||
|
|
.distance(cursor_pos)
|
||
|
|
.partial_cmp(
|
||
|
|
&hook2.distance(cursor_pos),
|
||
|
|
)
|
||
|
|
.unwrap()
|
||
|
|
})
|
||
|
|
.filter(|nearest_hook| {
|
||
|
|
nearest_hook.distance(cursor_pos)
|
||
|
|
< DISTANCE_TO_CONNECT
|
||
|
|
})
|
||
|
|
.copied()
|
||
|
|
},
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.unwrap_or(cursor_pos)
|
||
|
|
}
|
||
|
|
|
||
|
|
let (src_pos, dst_pos) = match locator {
|
||
|
|
| AnyParameterId::Output(_) => (
|
||
|
|
start_pos,
|
||
|
|
snap_to_ports(
|
||
|
|
&self.graph,
|
||
|
|
port_type,
|
||
|
|
&self.graph.inputs,
|
||
|
|
&port_locations,
|
||
|
|
cursor_pos,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
| AnyParameterId::Input(_) => (
|
||
|
|
snap_to_ports(
|
||
|
|
&self.graph,
|
||
|
|
port_type,
|
||
|
|
&self.graph.outputs,
|
||
|
|
&port_locations,
|
||
|
|
cursor_pos,
|
||
|
|
),
|
||
|
|
start_pos,
|
||
|
|
),
|
||
|
|
};
|
||
|
|
draw_connection(
|
||
|
|
&self.pan_zoom,
|
||
|
|
ui.painter(),
|
||
|
|
src_pos,
|
||
|
|
dst_pos,
|
||
|
|
connection_color,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// draw existing connections
|
||
|
|
for (input, outputs) in self.graph.iter_connection_groups() {
|
||
|
|
for (hook_n, &output) in outputs.iter().enumerate() {
|
||
|
|
let port_type = self
|
||
|
|
.graph
|
||
|
|
.any_param_type(AnyParameterId::Output(output))
|
||
|
|
.unwrap();
|
||
|
|
let connection_color = port_type.data_type_color(user_state);
|
||
|
|
// outputs can't be wide yet so this is fine.
|
||
|
|
let src_pos =
|
||
|
|
port_locations[&AnyParameterId::Output(output)][0];
|
||
|
|
let dst_pos = conn_locations[&input][hook_n];
|
||
|
|
draw_connection(
|
||
|
|
&self.pan_zoom,
|
||
|
|
ui.painter(),
|
||
|
|
src_pos,
|
||
|
|
dst_pos,
|
||
|
|
connection_color,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Handle responses from drawing nodes */
|
||
|
|
|
||
|
|
// Some responses generate additional responses when processed. These
|
||
|
|
// are stored here to report them back to the user.
|
||
|
|
let mut extra_responses: Vec<NodeResponse<UserResponse, NodeData>> =
|
||
|
|
Vec::new();
|
||
|
|
|
||
|
|
for response in delayed_responses.iter() {
|
||
|
|
match response {
|
||
|
|
| NodeResponse::ConnectEventStarted(node_id, port) => {
|
||
|
|
self.connection_in_progress = Some((*node_id, *port));
|
||
|
|
},
|
||
|
|
| NodeResponse::ConnectEventEnded {
|
||
|
|
output,
|
||
|
|
input,
|
||
|
|
input_hook,
|
||
|
|
} => self.graph.add_connection(*output, *input, *input_hook),
|
||
|
|
| NodeResponse::CreatedNode(_) => {
|
||
|
|
//Convenience NodeResponse for users
|
||
|
|
},
|
||
|
|
| NodeResponse::SelectNode(node_id) => {
|
||
|
|
self.selected_nodes = Vec::from([*node_id]);
|
||
|
|
},
|
||
|
|
| NodeResponse::DeleteNodeUi(node_id) => {
|
||
|
|
let (node, disc_events) = self.graph.remove_node(*node_id);
|
||
|
|
|
||
|
|
// Pass the disconnection responses first so user code can
|
||
|
|
// perform cleanup before node removal
|
||
|
|
// response.
|
||
|
|
extra_responses.extend(disc_events.into_iter().map(
|
||
|
|
|(input, output)| NodeResponse::DisconnectEvent {
|
||
|
|
input,
|
||
|
|
output,
|
||
|
|
},
|
||
|
|
));
|
||
|
|
// Pass the full node as a response so library users can
|
||
|
|
// listen for it and get their user data.
|
||
|
|
extra_responses.push(NodeResponse::DeleteNodeFull {
|
||
|
|
node_id: *node_id,
|
||
|
|
node,
|
||
|
|
});
|
||
|
|
self.node_positions.remove(*node_id);
|
||
|
|
// Make sure to not leave references to old nodes hanging
|
||
|
|
self.selected_nodes.retain(|id| *id != *node_id);
|
||
|
|
self.node_order.retain(|id| *id != *node_id);
|
||
|
|
},
|
||
|
|
| NodeResponse::DisconnectEvent { input, output } => {
|
||
|
|
let other_node = self.graph.get_output(*output).node;
|
||
|
|
self.graph.remove_connection(*input, *output);
|
||
|
|
self.connection_in_progress =
|
||
|
|
Some((other_node, AnyParameterId::Output(*output)));
|
||
|
|
},
|
||
|
|
| NodeResponse::RaiseNode(node_id) => {
|
||
|
|
let old_pos = self
|
||
|
|
.node_order
|
||
|
|
.iter()
|
||
|
|
.position(|id| *id == *node_id)
|
||
|
|
.expect("Node to be raised should be in `node_order`");
|
||
|
|
self.node_order.remove(old_pos);
|
||
|
|
self.node_order.push(*node_id);
|
||
|
|
},
|
||
|
|
| NodeResponse::MoveNode { node, drag_delta } => {
|
||
|
|
self.node_positions[*node] += *drag_delta;
|
||
|
|
// Handle multi-node selection movement
|
||
|
|
if self.selected_nodes.contains(node)
|
||
|
|
&& self.selected_nodes.len() > 1
|
||
|
|
{
|
||
|
|
for n in self.selected_nodes.iter().copied() {
|
||
|
|
if n != *node {
|
||
|
|
self.node_positions[n] += *drag_delta;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
| NodeResponse::User(_) => {
|
||
|
|
// These are handled by the user code.
|
||
|
|
},
|
||
|
|
| NodeResponse::DeleteNodeFull { .. } => {
|
||
|
|
unreachable!(
|
||
|
|
"The UI should never produce a DeleteNodeFull event."
|
||
|
|
)
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle box selection
|
||
|
|
if let Some(box_start) = self.ongoing_box_selection {
|
||
|
|
let selection_rect = Rect::from_two_pos(cursor_pos, box_start);
|
||
|
|
let bg_color = Color32::from_rgba_unmultiplied(200, 200, 200, 20);
|
||
|
|
let stroke_color =
|
||
|
|
Color32::from_rgba_unmultiplied(200, 200, 200, 180);
|
||
|
|
ui.painter().rect(
|
||
|
|
selection_rect,
|
||
|
|
2.0,
|
||
|
|
bg_color,
|
||
|
|
Stroke::new(3.0, stroke_color),
|
||
|
|
StrokeKind::Middle,
|
||
|
|
);
|
||
|
|
|
||
|
|
self.selected_nodes = node_rects
|
||
|
|
.into_iter()
|
||
|
|
.filter_map(|(node_id, rect)| {
|
||
|
|
if selection_rect.intersects(rect) {
|
||
|
|
Some(node_id)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Push any responses that were generated during response handling.
|
||
|
|
// These are only informative for the end-user and need no special
|
||
|
|
// treatment here.
|
||
|
|
delayed_responses.extend(extra_responses);
|
||
|
|
|
||
|
|
/* Mouse input handling */
|
||
|
|
|
||
|
|
// This locks the context, so don't hold on to it for too long.
|
||
|
|
let mouse = &ui.ctx().input(|i| i.pointer.clone());
|
||
|
|
|
||
|
|
if mouse.any_released() && self.connection_in_progress.is_some() {
|
||
|
|
self.connection_in_progress = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
if mouse.secondary_released() && cursor_in_editor && !cursor_in_finder {
|
||
|
|
self.node_finder = Some(NodeFinder::new_at(cursor_pos));
|
||
|
|
}
|
||
|
|
if ui.ctx().input(|i| i.key_pressed(Key::Escape)) {
|
||
|
|
self.node_finder = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
if r.dragged()
|
||
|
|
&& ui.ctx().input(|i| {
|
||
|
|
i.pointer.middle_down() || i.modifiers.command_only()
|
||
|
|
})
|
||
|
|
{
|
||
|
|
self.pan_zoom.pan += ui.ctx().input(|i| i.pointer.delta());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deselect and deactivate finder if the editor background is clicked,
|
||
|
|
// *or* if the the mouse clicks off the ui
|
||
|
|
if click_on_background || (mouse.any_click() && !cursor_in_editor) {
|
||
|
|
self.selected_nodes = Vec::new();
|
||
|
|
self.node_finder = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
if drag_started_on_background
|
||
|
|
&& mouse.primary_down()
|
||
|
|
&& !ui.ctx().input(|i| i.modifiers.command_only())
|
||
|
|
{
|
||
|
|
self.ongoing_box_selection = Some(cursor_pos);
|
||
|
|
}
|
||
|
|
if mouse.primary_released() || drag_released_on_background {
|
||
|
|
self.ongoing_box_selection = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
GraphResponse {
|
||
|
|
node_responses: delayed_responses,
|
||
|
|
cursor_in_editor,
|
||
|
|
cursor_in_finder,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn draw_connection(
|
||
|
|
pan_zoom: &PanZoom,
|
||
|
|
painter: &Painter,
|
||
|
|
src_pos: Pos2,
|
||
|
|
dst_pos: Pos2,
|
||
|
|
color: Color32,
|
||
|
|
) {
|
||
|
|
let connection_stroke = egui::Stroke {
|
||
|
|
width: 5.0 * pan_zoom.zoom,
|
||
|
|
color,
|
||
|
|
};
|
||
|
|
|
||
|
|
let control_scale = ((dst_pos.x - src_pos.x) * pan_zoom.zoom / 2.0)
|
||
|
|
.max(30.0 * pan_zoom.zoom);
|
||
|
|
let src_control = src_pos + Vec2::X * control_scale;
|
||
|
|
let dst_control = dst_pos - Vec2::X * control_scale;
|
||
|
|
|
||
|
|
let bezier = CubicBezierShape::from_points_stroke(
|
||
|
|
[src_pos, src_control, dst_control, dst_pos],
|
||
|
|
false,
|
||
|
|
Color32::TRANSPARENT,
|
||
|
|
connection_stroke,
|
||
|
|
);
|
||
|
|
|
||
|
|
painter.add(bezier);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, Copy, Debug)]
|
||
|
|
struct OuterRectMemory(Rect);
|
||
|
|
|
||
|
|
impl<'a, NodeData, DataType, ValueType, UserResponse, UserState>
|
||
|
|
GraphNodeWidget<'a, NodeData, DataType, ValueType>
|
||
|
|
where
|
||
|
|
NodeData: NodeDataTrait<
|
||
|
|
Response = UserResponse,
|
||
|
|
UserState = UserState,
|
||
|
|
DataType = DataType,
|
||
|
|
ValueType = ValueType,
|
||
|
|
>,
|
||
|
|
UserResponse: UserResponseTrait,
|
||
|
|
ValueType: WidgetValueTrait<
|
||
|
|
Response = UserResponse,
|
||
|
|
UserState = UserState,
|
||
|
|
NodeData = NodeData,
|
||
|
|
>,
|
||
|
|
DataType: DataTypeTrait<UserState>,
|
||
|
|
{
|
||
|
|
pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0];
|
||
|
|
|
||
|
|
pub fn show(
|
||
|
|
self,
|
||
|
|
pan_zoom: &PanZoom,
|
||
|
|
ui: &mut Ui,
|
||
|
|
user_state: &mut UserState,
|
||
|
|
) -> Vec<NodeResponse<UserResponse, NodeData>> {
|
||
|
|
let mut child_ui = ui.new_child(
|
||
|
|
UiBuilder::new()
|
||
|
|
.id_salt(self.node_id)
|
||
|
|
.max_rect(Rect::from_min_size(
|
||
|
|
*self.position + self.pan,
|
||
|
|
Self::MAX_NODE_SIZE.into(),
|
||
|
|
))
|
||
|
|
.layout(Layout::default())
|
||
|
|
.ui_stack_info(UiStackInfo::default()),
|
||
|
|
);
|
||
|
|
|
||
|
|
Self::show_graph_node(self, pan_zoom, &mut child_ui, user_state)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Draws this node. Also fills in the list of port locations with all of
|
||
|
|
/// its ports. Returns responses indicating multiple events.
|
||
|
|
fn show_graph_node(
|
||
|
|
self,
|
||
|
|
pan_zoom: &PanZoom,
|
||
|
|
ui: &mut Ui,
|
||
|
|
user_state: &mut UserState,
|
||
|
|
) -> Vec<NodeResponse<UserResponse, NodeData>> {
|
||
|
|
let margin = egui::vec2(15.0, 5.0) * pan_zoom.zoom;
|
||
|
|
let mut responses = Vec::<NodeResponse<UserResponse, NodeData>>::new();
|
||
|
|
|
||
|
|
let background_color;
|
||
|
|
let text_color;
|
||
|
|
if ui.visuals().dark_mode {
|
||
|
|
background_color = color_from_hex("#3f3f3f").unwrap();
|
||
|
|
text_color = color_from_hex("#fefefe").unwrap();
|
||
|
|
} else {
|
||
|
|
background_color = color_from_hex("#ffffff").unwrap();
|
||
|
|
text_color = color_from_hex("#505050").unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
ui.visuals_mut().widgets.noninteractive.fg_stroke =
|
||
|
|
Stroke::new(2.0 * pan_zoom.zoom, text_color);
|
||
|
|
|
||
|
|
// Preallocate shapes to paint below contents
|
||
|
|
let outline_shape = ui.painter().add(Shape::Noop);
|
||
|
|
let background_shape = ui.painter().add(Shape::Noop);
|
||
|
|
|
||
|
|
let mut outer_rect_bounds = ui.available_rect_before_wrap();
|
||
|
|
// Scale hack, otherwise some (larger) rects expand too much when zoomed
|
||
|
|
// out
|
||
|
|
outer_rect_bounds.max.x =
|
||
|
|
outer_rect_bounds.min.x + outer_rect_bounds.width() * pan_zoom.zoom;
|
||
|
|
|
||
|
|
let mut inner_rect = outer_rect_bounds.shrink2(margin);
|
||
|
|
|
||
|
|
// Make sure we don't shrink to the negative:
|
||
|
|
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
|
||
|
|
inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y);
|
||
|
|
|
||
|
|
let mut child_ui = ui.new_child(
|
||
|
|
UiBuilder::new()
|
||
|
|
.max_rect(inner_rect)
|
||
|
|
.layout(*ui.layout())
|
||
|
|
.ui_stack_info(UiStackInfo::default()),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get interaction rect from memory, it may expand after the window
|
||
|
|
// response on resize.
|
||
|
|
let interaction_rect = ui
|
||
|
|
.ctx()
|
||
|
|
.memory_mut(|mem| {
|
||
|
|
mem.data
|
||
|
|
.get_temp::<OuterRectMemory>(child_ui.id())
|
||
|
|
.map(|stored| stored.0)
|
||
|
|
})
|
||
|
|
.unwrap_or(outer_rect_bounds);
|
||
|
|
// After 0.20, layers added over others can block hover interaction.
|
||
|
|
// Call this first before creating the node content.
|
||
|
|
let window_response = ui.interact(
|
||
|
|
interaction_rect,
|
||
|
|
Id::new((self.node_id, "window")),
|
||
|
|
Sense::click_and_drag(),
|
||
|
|
);
|
||
|
|
|
||
|
|
let mut title_height = 0.0;
|
||
|
|
|
||
|
|
let mut input_port_heights = vec![];
|
||
|
|
let mut output_port_heights = vec![];
|
||
|
|
|
||
|
|
child_ui.vertical(|ui| {
|
||
|
|
ui.horizontal(|ui| {
|
||
|
|
ui.add(Label::new(
|
||
|
|
RichText::new(&self.graph[self.node_id].label)
|
||
|
|
.text_style(TextStyle::Button)
|
||
|
|
.color(text_color),
|
||
|
|
));
|
||
|
|
responses.extend(
|
||
|
|
self.graph[self.node_id].user_data.top_bar_ui(
|
||
|
|
ui,
|
||
|
|
self.node_id,
|
||
|
|
self.graph,
|
||
|
|
user_state,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
ui.add_space(8.0 * pan_zoom.zoom); // The size of the little
|
||
|
|
// cross icon
|
||
|
|
});
|
||
|
|
ui.add_space(margin.y);
|
||
|
|
title_height = ui.min_size().y;
|
||
|
|
|
||
|
|
// First pass: Draw the inner fields. Compute port heights
|
||
|
|
let inputs = self.graph[self.node_id].inputs.clone();
|
||
|
|
for (param_name, param_id) in inputs {
|
||
|
|
if self.graph[param_id].shown_inline {
|
||
|
|
let height_before = ui.min_rect().bottom();
|
||
|
|
|
||
|
|
if self.graph[param_id].max_connections
|
||
|
|
== NonZeroU32::new(1)
|
||
|
|
{
|
||
|
|
// NOTE: We want to pass the `user_data` to
|
||
|
|
// `value_widget`, but we can't since that would require
|
||
|
|
// borrowing the graph twice. Here, we make the
|
||
|
|
// assumption that the value is cheaply replaced, and
|
||
|
|
// use `std::mem::take` to temporarily replace it with a
|
||
|
|
// dummy value. This requires `ValueType` to implement
|
||
|
|
// Default, but results in a totally safe alternative.
|
||
|
|
let mut value =
|
||
|
|
std::mem::take(&mut self.graph[param_id].value);
|
||
|
|
|
||
|
|
if !self.graph.connections(param_id).is_empty() {
|
||
|
|
let node_responses = value.value_widget_connected(
|
||
|
|
¶m_name,
|
||
|
|
self.node_id,
|
||
|
|
ui,
|
||
|
|
user_state,
|
||
|
|
&self.graph[self.node_id].user_data,
|
||
|
|
);
|
||
|
|
|
||
|
|
responses.extend(
|
||
|
|
node_responses
|
||
|
|
.into_iter()
|
||
|
|
.map(NodeResponse::User),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
let node_responses = value.value_widget(
|
||
|
|
¶m_name,
|
||
|
|
self.node_id,
|
||
|
|
ui,
|
||
|
|
user_state,
|
||
|
|
&self.graph[self.node_id].user_data,
|
||
|
|
);
|
||
|
|
|
||
|
|
responses.extend(
|
||
|
|
node_responses
|
||
|
|
.into_iter()
|
||
|
|
.map(NodeResponse::User),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
self.graph[param_id].value = value;
|
||
|
|
} else {
|
||
|
|
ui.label(param_name);
|
||
|
|
}
|
||
|
|
|
||
|
|
let height_intermediate = ui.min_rect().bottom();
|
||
|
|
|
||
|
|
let max_connections = self.graph[param_id]
|
||
|
|
.max_connections
|
||
|
|
.map(NonZeroU32::get)
|
||
|
|
.unwrap_or(u32::MAX)
|
||
|
|
as usize;
|
||
|
|
let port_height = port_height(
|
||
|
|
max_connections != 1,
|
||
|
|
self.graph.connections(param_id).len(),
|
||
|
|
max_connections,
|
||
|
|
);
|
||
|
|
let margin = 5.0;
|
||
|
|
let missing_space = port_height
|
||
|
|
- (height_intermediate - height_before)
|
||
|
|
+ margin;
|
||
|
|
if missing_space > 0.0 {
|
||
|
|
ui.add_space(missing_space);
|
||
|
|
}
|
||
|
|
|
||
|
|
self.graph[self.node_id].user_data.separator(
|
||
|
|
ui,
|
||
|
|
self.node_id,
|
||
|
|
AnyParameterId::Input(param_id),
|
||
|
|
self.graph,
|
||
|
|
user_state,
|
||
|
|
);
|
||
|
|
|
||
|
|
let height_after = ui.min_rect().bottom();
|
||
|
|
|
||
|
|
input_port_heights
|
||
|
|
.push((height_before + height_after) / 2.0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let outputs = self.graph[self.node_id].outputs.clone();
|
||
|
|
for (param_name, param_id) in outputs {
|
||
|
|
let height_before = ui.min_rect().bottom();
|
||
|
|
responses.extend(
|
||
|
|
self.graph[self.node_id]
|
||
|
|
.user_data
|
||
|
|
.output_ui(
|
||
|
|
ui,
|
||
|
|
self.node_id,
|
||
|
|
self.graph,
|
||
|
|
user_state,
|
||
|
|
¶m_name,
|
||
|
|
)
|
||
|
|
.into_iter(),
|
||
|
|
);
|
||
|
|
|
||
|
|
self.graph[self.node_id].user_data.separator(
|
||
|
|
ui,
|
||
|
|
self.node_id,
|
||
|
|
AnyParameterId::Output(param_id),
|
||
|
|
self.graph,
|
||
|
|
user_state,
|
||
|
|
);
|
||
|
|
|
||
|
|
let height_after = ui.min_rect().bottom();
|
||
|
|
output_port_heights.push((height_before + height_after) / 2.0);
|
||
|
|
}
|
||
|
|
|
||
|
|
responses.extend(self.graph[self.node_id].user_data.bottom_ui(
|
||
|
|
ui,
|
||
|
|
self.node_id,
|
||
|
|
self.graph,
|
||
|
|
user_state,
|
||
|
|
));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Second pass, iterate again to draw the ports. This happens outside
|
||
|
|
// the child_ui because we want ports to overflow the node background.
|
||
|
|
|
||
|
|
let outer_rect = child_ui.min_rect().expand2(margin);
|
||
|
|
let port_left = outer_rect.left();
|
||
|
|
let port_right = outer_rect.right();
|
||
|
|
|
||
|
|
// Save expanded rect to memory.
|
||
|
|
ui.ctx().memory_mut(|mem| {
|
||
|
|
mem.data
|
||
|
|
.insert_temp(child_ui.id(), OuterRectMemory(outer_rect))
|
||
|
|
});
|
||
|
|
|
||
|
|
fn port_height(
|
||
|
|
wide_port: bool,
|
||
|
|
connections: usize,
|
||
|
|
max_connections: usize,
|
||
|
|
) -> f32 {
|
||
|
|
let port_full = connections == max_connections;
|
||
|
|
if wide_port {
|
||
|
|
let hooks = connections + if port_full { 0 } else { 1 };
|
||
|
|
|
||
|
|
5.0 + (10.0 * hooks as f32).max(10.0)
|
||
|
|
} else {
|
||
|
|
10.0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[allow(clippy::too_many_arguments)]
|
||
|
|
fn draw_port<NodeData, DataType, ValueType, UserResponse, UserState>(
|
||
|
|
pan_zoom: &PanZoom,
|
||
|
|
ui: &mut Ui,
|
||
|
|
graph: &Graph<NodeData, DataType, ValueType>,
|
||
|
|
node_id: NodeId,
|
||
|
|
user_state: &mut UserState,
|
||
|
|
port_pos: Pos2,
|
||
|
|
responses: &mut Vec<NodeResponse<UserResponse, NodeData>>,
|
||
|
|
param_id: AnyParameterId,
|
||
|
|
port_locations: &mut PortLocations,
|
||
|
|
conn_locations: &mut ConnLocations,
|
||
|
|
ongoing_drag: Option<(NodeId, AnyParameterId)>,
|
||
|
|
wide_port: bool,
|
||
|
|
connections: usize,
|
||
|
|
max_connections: usize,
|
||
|
|
) where
|
||
|
|
DataType: DataTypeTrait<UserState>,
|
||
|
|
UserResponse: UserResponseTrait,
|
||
|
|
NodeData: NodeDataTrait,
|
||
|
|
{
|
||
|
|
let port_type = graph.any_param_type(param_id).unwrap();
|
||
|
|
|
||
|
|
let port_rect = Rect::from_center_size(
|
||
|
|
port_pos,
|
||
|
|
egui::vec2(
|
||
|
|
10.0,
|
||
|
|
port_height(wide_port, connections, max_connections),
|
||
|
|
) * pan_zoom.zoom,
|
||
|
|
);
|
||
|
|
|
||
|
|
let port_full = connections == max_connections;
|
||
|
|
|
||
|
|
let inner_ports = if wide_port {
|
||
|
|
connections + if port_full { 0 } else { 1 }
|
||
|
|
} else {
|
||
|
|
1
|
||
|
|
};
|
||
|
|
|
||
|
|
port_locations.insert(
|
||
|
|
param_id,
|
||
|
|
(0..inner_ports)
|
||
|
|
.map(|k| {
|
||
|
|
port_rect.center_top()
|
||
|
|
+ Vec2::new(0.0, 5.0 * pan_zoom.zoom)
|
||
|
|
+ Vec2::new(0.0, 10.0 * pan_zoom.zoom) * k as f32
|
||
|
|
})
|
||
|
|
.collect(),
|
||
|
|
);
|
||
|
|
|
||
|
|
let sense = if ongoing_drag.is_some() {
|
||
|
|
Sense::hover()
|
||
|
|
} else {
|
||
|
|
Sense::click_and_drag()
|
||
|
|
};
|
||
|
|
|
||
|
|
let resp = ui.allocate_rect(port_rect, sense);
|
||
|
|
|
||
|
|
// Check if the distance between the port and the mouse is the
|
||
|
|
// distance to connect
|
||
|
|
let close_enough =
|
||
|
|
if let Some(pointer_pos) = ui.ctx().pointer_hover_pos() {
|
||
|
|
port_rect.center().distance(pointer_pos)
|
||
|
|
< DISTANCE_TO_CONNECT * pan_zoom.zoom
|
||
|
|
} else {
|
||
|
|
false
|
||
|
|
};
|
||
|
|
|
||
|
|
let port_color = if close_enough {
|
||
|
|
Color32::WHITE
|
||
|
|
} else {
|
||
|
|
port_type.data_type_color(user_state)
|
||
|
|
};
|
||
|
|
|
||
|
|
if wide_port {
|
||
|
|
ui.painter().rect_filled(
|
||
|
|
port_rect,
|
||
|
|
5.0 * pan_zoom.zoom,
|
||
|
|
port_color,
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
ui.painter().circle(
|
||
|
|
port_rect.center(),
|
||
|
|
5.0 * pan_zoom.zoom,
|
||
|
|
port_color,
|
||
|
|
Stroke::NONE,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if connections > 0 {
|
||
|
|
if let AnyParameterId::Input(input) = param_id {
|
||
|
|
for (k, dst_pos) in port_locations
|
||
|
|
[&AnyParameterId::Input(input)]
|
||
|
|
.iter()
|
||
|
|
.enumerate()
|
||
|
|
{
|
||
|
|
conn_locations
|
||
|
|
.entry(input)
|
||
|
|
.or_default()
|
||
|
|
.insert(k, *dst_pos);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let nearest_hook = ui
|
||
|
|
.input(|in_state| in_state.pointer.hover_pos())
|
||
|
|
.and_then(|mouse_pos| match param_id {
|
||
|
|
| AnyParameterId::Input(input) => Some((mouse_pos, input)),
|
||
|
|
| AnyParameterId::Output(_) => None,
|
||
|
|
})
|
||
|
|
.and_then(|(mouse_pos, input)| {
|
||
|
|
let hooks = 0..inner_ports;
|
||
|
|
hooks.min_by(|&hook1, &hook2| {
|
||
|
|
let out1_dist =
|
||
|
|
conn_locations[&input][hook1].distance(mouse_pos);
|
||
|
|
let out2_dist =
|
||
|
|
conn_locations[&input][hook2].distance(mouse_pos);
|
||
|
|
|
||
|
|
out1_dist.partial_cmp(&out2_dist).unwrap()
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
if resp.drag_started() {
|
||
|
|
match param_id {
|
||
|
|
| AnyParameterId::Input(input) => {
|
||
|
|
match nearest_hook.and_then(|hook| {
|
||
|
|
graph.connections(input).get(hook).copied()
|
||
|
|
}) {
|
||
|
|
| Some(output) => {
|
||
|
|
responses.push(NodeResponse::DisconnectEvent {
|
||
|
|
input,
|
||
|
|
output,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
| None => {
|
||
|
|
responses.push(
|
||
|
|
NodeResponse::ConnectEventStarted(
|
||
|
|
node_id, param_id,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
}
|
||
|
|
},
|
||
|
|
| AnyParameterId::Output(_) => {
|
||
|
|
responses.push(NodeResponse::ConnectEventStarted(
|
||
|
|
node_id, param_id,
|
||
|
|
));
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some((origin_node, origin_param)) = ongoing_drag {
|
||
|
|
if origin_node != node_id {
|
||
|
|
// Don't allow self-loops
|
||
|
|
if graph.any_param_type(origin_param).unwrap() == port_type
|
||
|
|
&& close_enough
|
||
|
|
{
|
||
|
|
match (param_id, origin_param) {
|
||
|
|
| (
|
||
|
|
AnyParameterId::Input(input),
|
||
|
|
AnyParameterId::Output(output),
|
||
|
|
)
|
||
|
|
| (
|
||
|
|
AnyParameterId::Output(output),
|
||
|
|
AnyParameterId::Input(input),
|
||
|
|
) => {
|
||
|
|
let input_hook = nearest_hook
|
||
|
|
.unwrap_or(graph.connections(input).len());
|
||
|
|
|
||
|
|
if ui.input(|i| i.pointer.any_released()) {
|
||
|
|
responses.push(
|
||
|
|
NodeResponse::ConnectEventEnded {
|
||
|
|
output,
|
||
|
|
input,
|
||
|
|
input_hook,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
} else if wide_port && !port_full {
|
||
|
|
// move connections below the in-progress
|
||
|
|
// one to a lower position
|
||
|
|
for k in input_hook
|
||
|
|
..graph.connections(input).len()
|
||
|
|
{
|
||
|
|
conn_locations
|
||
|
|
.get_mut(&input)
|
||
|
|
.unwrap()[k]
|
||
|
|
.y += 7.5;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
| _ => { /* Ignore in-in or out-out connections */
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Input ports
|
||
|
|
for ((_, param), port_height) in self.graph[self.node_id]
|
||
|
|
.inputs
|
||
|
|
.iter()
|
||
|
|
.zip(input_port_heights.into_iter())
|
||
|
|
{
|
||
|
|
let should_draw = match self.graph[*param].kind() {
|
||
|
|
| InputParamKind::ConnectionOnly => true,
|
||
|
|
| InputParamKind::ConstantOnly => false,
|
||
|
|
| InputParamKind::ConnectionOrConstant => true,
|
||
|
|
};
|
||
|
|
|
||
|
|
if should_draw {
|
||
|
|
let pos_left = pos2(port_left, port_height);
|
||
|
|
let max_connections = self.graph[*param]
|
||
|
|
.max_connections
|
||
|
|
.map(NonZeroU32::get)
|
||
|
|
.unwrap_or(u32::MAX)
|
||
|
|
as usize;
|
||
|
|
draw_port(
|
||
|
|
pan_zoom,
|
||
|
|
ui,
|
||
|
|
self.graph,
|
||
|
|
self.node_id,
|
||
|
|
user_state,
|
||
|
|
pos_left,
|
||
|
|
&mut responses,
|
||
|
|
AnyParameterId::Input(*param),
|
||
|
|
self.port_locations,
|
||
|
|
self.conn_locations,
|
||
|
|
self.ongoing_drag,
|
||
|
|
max_connections > 1,
|
||
|
|
self.graph.connections(*param).len(),
|
||
|
|
max_connections,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Output ports
|
||
|
|
for ((_, param), port_height) in self.graph[self.node_id]
|
||
|
|
.outputs
|
||
|
|
.iter()
|
||
|
|
.zip(output_port_heights.into_iter())
|
||
|
|
{
|
||
|
|
let pos_right = pos2(port_right, port_height);
|
||
|
|
draw_port(
|
||
|
|
pan_zoom,
|
||
|
|
ui,
|
||
|
|
self.graph,
|
||
|
|
self.node_id,
|
||
|
|
user_state,
|
||
|
|
pos_right,
|
||
|
|
&mut responses,
|
||
|
|
AnyParameterId::Output(*param),
|
||
|
|
self.port_locations,
|
||
|
|
self.conn_locations,
|
||
|
|
self.ongoing_drag,
|
||
|
|
false,
|
||
|
|
0,
|
||
|
|
1,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw the background shape.
|
||
|
|
// NOTE: This code is a bit more involved than it needs to be because
|
||
|
|
// egui does not support drawing rectangles with asymmetrical
|
||
|
|
// round corners.
|
||
|
|
|
||
|
|
let (shape, outline) = {
|
||
|
|
let rounding_radius = 4.0 * pan_zoom.zoom;
|
||
|
|
let rounding = CornerRadiusF32::same(rounding_radius);
|
||
|
|
|
||
|
|
let titlebar_height = title_height + margin.y;
|
||
|
|
let titlebar_rect = Rect::from_min_size(
|
||
|
|
outer_rect.min,
|
||
|
|
vec2(outer_rect.width(), titlebar_height),
|
||
|
|
);
|
||
|
|
let titlebar = Shape::Rect(RectShape {
|
||
|
|
rect: titlebar_rect,
|
||
|
|
corner_radius: rounding.into(),
|
||
|
|
fill: self.graph[self.node_id]
|
||
|
|
.user_data
|
||
|
|
.titlebar_color(ui, self.node_id, self.graph, user_state)
|
||
|
|
.unwrap_or_else(|| background_color.lighten(0.8)),
|
||
|
|
stroke: Stroke::NONE,
|
||
|
|
blur_width: 0.0,
|
||
|
|
stroke_kind: StrokeKind::Middle,
|
||
|
|
brush: None,
|
||
|
|
round_to_pixels: None,
|
||
|
|
});
|
||
|
|
|
||
|
|
let body_rect = Rect::from_min_size(
|
||
|
|
outer_rect.min + vec2(0.0, titlebar_height - rounding_radius),
|
||
|
|
vec2(outer_rect.width(), outer_rect.height() - titlebar_height),
|
||
|
|
);
|
||
|
|
let body = Shape::Rect(RectShape {
|
||
|
|
rect: body_rect,
|
||
|
|
corner_radius: CornerRadius::ZERO,
|
||
|
|
fill: background_color,
|
||
|
|
stroke: Stroke::NONE,
|
||
|
|
blur_width: 0.0,
|
||
|
|
stroke_kind: StrokeKind::Middle,
|
||
|
|
brush: None,
|
||
|
|
round_to_pixels: None,
|
||
|
|
});
|
||
|
|
|
||
|
|
let bottom_body_rect = Rect::from_min_size(
|
||
|
|
body_rect.min
|
||
|
|
+ vec2(0.0, body_rect.height() - titlebar_height * 0.5),
|
||
|
|
vec2(outer_rect.width(), titlebar_height),
|
||
|
|
);
|
||
|
|
let bottom_body = Shape::Rect(RectShape {
|
||
|
|
rect: bottom_body_rect,
|
||
|
|
corner_radius: rounding.into(),
|
||
|
|
fill: background_color,
|
||
|
|
stroke: Stroke::NONE,
|
||
|
|
blur_width: 0.0,
|
||
|
|
stroke_kind: StrokeKind::Middle,
|
||
|
|
brush: None,
|
||
|
|
round_to_pixels: None,
|
||
|
|
});
|
||
|
|
|
||
|
|
let node_rect =
|
||
|
|
titlebar_rect.union(body_rect).union(bottom_body_rect);
|
||
|
|
let outline = if self.selected {
|
||
|
|
Shape::Rect(RectShape {
|
||
|
|
rect: node_rect.expand(1.0 * pan_zoom.zoom),
|
||
|
|
corner_radius: rounding.into(),
|
||
|
|
fill: Color32::WHITE.lighten(0.8),
|
||
|
|
stroke: Stroke::NONE,
|
||
|
|
blur_width: 0.0,
|
||
|
|
stroke_kind: StrokeKind::Middle,
|
||
|
|
brush: None,
|
||
|
|
round_to_pixels: None,
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
Shape::Noop
|
||
|
|
};
|
||
|
|
|
||
|
|
// Take note of the node rect, so the editor can use it later to
|
||
|
|
// compute intersections.
|
||
|
|
self.node_rects.insert(self.node_id, node_rect);
|
||
|
|
|
||
|
|
(Shape::Vec(vec![titlebar, body, bottom_body]), outline)
|
||
|
|
};
|
||
|
|
|
||
|
|
ui.painter().set(background_shape, shape);
|
||
|
|
ui.painter().set(outline_shape, outline);
|
||
|
|
|
||
|
|
// --- Interaction ---
|
||
|
|
|
||
|
|
// Titlebar buttons
|
||
|
|
let can_delete = self.graph.nodes[self.node_id].user_data.can_delete(
|
||
|
|
self.node_id,
|
||
|
|
self.graph,
|
||
|
|
user_state,
|
||
|
|
);
|
||
|
|
|
||
|
|
if can_delete && Self::close_button(pan_zoom, ui, outer_rect).clicked()
|
||
|
|
{
|
||
|
|
responses.push(NodeResponse::DeleteNodeUi(self.node_id));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Movement
|
||
|
|
let drag_delta = window_response.drag_delta();
|
||
|
|
if drag_delta.length_sq() > 0.0 {
|
||
|
|
responses.push(NodeResponse::MoveNode {
|
||
|
|
node: self.node_id,
|
||
|
|
drag_delta,
|
||
|
|
});
|
||
|
|
responses.push(NodeResponse::RaiseNode(self.node_id));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Node selection
|
||
|
|
//
|
||
|
|
// 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(NodeResponse::SelectNode(self.node_id));
|
||
|
|
responses.push(NodeResponse::RaiseNode(self.node_id));
|
||
|
|
}
|
||
|
|
|
||
|
|
responses
|
||
|
|
}
|
||
|
|
|
||
|
|
fn close_button(
|
||
|
|
pan_zoom: &PanZoom,
|
||
|
|
ui: &mut Ui,
|
||
|
|
node_rect: Rect,
|
||
|
|
) -> Response {
|
||
|
|
// Measurements
|
||
|
|
let margin = 8.0 * pan_zoom.zoom;
|
||
|
|
let size = 10.0 * pan_zoom.zoom;
|
||
|
|
let stroke_width = 2.0;
|
||
|
|
let offs = margin + size / 2.0;
|
||
|
|
|
||
|
|
let position = pos2(node_rect.right() - offs, node_rect.top() + offs);
|
||
|
|
let rect = Rect::from_center_size(position, vec2(size, size));
|
||
|
|
let resp = ui.allocate_rect(rect, Sense::click());
|
||
|
|
|
||
|
|
let dark_mode = ui.visuals().dark_mode;
|
||
|
|
let color = if resp.clicked() {
|
||
|
|
if dark_mode {
|
||
|
|
color_from_hex("#ffffff").unwrap()
|
||
|
|
} else {
|
||
|
|
color_from_hex("#000000").unwrap()
|
||
|
|
}
|
||
|
|
} else if resp.hovered() {
|
||
|
|
if dark_mode {
|
||
|
|
color_from_hex("#dddddd").unwrap()
|
||
|
|
} else {
|
||
|
|
color_from_hex("#222222").unwrap()
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
#[allow(clippy::collapsible_else_if)]
|
||
|
|
if dark_mode {
|
||
|
|
color_from_hex("#aaaaaa").unwrap()
|
||
|
|
} else {
|
||
|
|
color_from_hex("#555555").unwrap()
|
||
|
|
}
|
||
|
|
};
|
||
|
|
let stroke = Stroke {
|
||
|
|
width: stroke_width,
|
||
|
|
color,
|
||
|
|
};
|
||
|
|
|
||
|
|
ui.painter()
|
||
|
|
.line_segment([rect.left_top(), rect.right_bottom()], stroke);
|
||
|
|
ui.painter()
|
||
|
|
.line_segment([rect.right_top(), rect.left_bottom()], stroke);
|
||
|
|
|
||
|
|
resp
|
||
|
|
}
|
||
|
|
}
|