Files
ReactionSystemsGUI/egui_node_graph2/src/editor_ui.rs
2025-10-17 21:42:41 +02:00

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(
&param_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(
&param_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,
&param_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
}
}