Merge pull request #72 from setzer22/feature/box_selection

Box selection and multi-node movement
This commit is contained in:
setzer22 2022-11-12 18:21:23 +01:00 committed by GitHub
commit 070ff85bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 83 additions and 18 deletions

View File

@ -8,6 +8,8 @@ use egui::epaint::{CubicBezierShape, RectShape};
use egui::*;
pub type PortLocations = std::collections::HashMap<AnyParameterId, Pos2>;
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
@ -38,6 +40,10 @@ pub enum NodeResponse<UserResponse: UserResponseTrait, NodeData: NodeDataTrait>
},
/// Emitted when a node is interacted with, and should be raised
RaiseNode(NodeId),
MoveNode {
node: NodeId,
drag_delta: Vec2,
},
User(UserResponse),
}
@ -60,6 +66,7 @@ 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 node_rects: &'a mut NodeRects,
pub node_id: NodeId,
pub ongoing_drag: Option<(NodeId, AnyParameterId)>,
pub selected: bool,
@ -103,16 +110,21 @@ where
let mut cursor_in_editor = editor_rect.contains(cursor_pos);
let mut cursor_in_finder = false;
// Gets filled with the port locations as nodes are drawn
// Gets filled with the node metrics as they are drawn
let mut port_locations = PortLocations::new();
let mut node_sizes = NodeRects::new();
// 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>> = vec![];
// Used to detect when the background was clicked, to dismiss certain selfs
// 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<_>>(),
@ -126,12 +138,13 @@ where
position: self.node_positions.get_mut(node_id).unwrap(),
graph: &mut self.graph,
port_locations: &mut port_locations,
node_rects: &mut node_sizes,
node_id,
ongoing_drag: self.connection_in_progress,
selected: self
.selected_node
.map(|selected| selected == node_id)
.unwrap_or(false),
.selected_nodes
.iter()
.any(|selected| *selected == node_id),
pan: self.pan_zoom.pan + editor_rect.min.to_vec2(),
}
.show(ui, user_state);
@ -143,6 +156,10 @@ where
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_released() {
drag_released_on_background = true;
}
/* Draw the node finder, if open */
@ -279,7 +296,7 @@ where
//Convenience NodeResponse for users
}
NodeResponse::SelectNode(node_id) => {
self.selected_node = Some(*node_id);
self.selected_nodes = Vec::from([*node_id]);
}
NodeResponse::DeleteNodeUi(node_id) => {
let (node, disc_events) = self.graph.remove_node(*node_id);
@ -298,9 +315,7 @@ where
});
self.node_positions.remove(*node_id);
// Make sure to not leave references to old nodes hanging
if self.selected_node.map(|x| x == *node_id).unwrap_or(false) {
self.selected_node = None;
}
self.selected_nodes.retain(|id| *id != *node_id);
self.node_order.retain(|id| *id != *node_id);
}
NodeResponse::DisconnectEvent { input, output } => {
@ -318,6 +333,17 @@ where
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.
}
@ -327,6 +353,30 @@ where
}
}
// 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),
);
self.selected_nodes = node_sizes
.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.
@ -355,10 +405,17 @@ where
// Deselect and deactivate finder if the editor backround is clicked,
// *or* if the the mouse clicks off the ui
if click_on_background || (mouse.any_click() && !cursor_in_editor) {
self.selected_node = None;
self.selected_nodes = Vec::new();
self.node_finder = None;
}
if drag_started_on_background {
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,
}
@ -690,12 +747,10 @@ where
stroke: Stroke::none(),
});
let node_rect = titlebar_rect.union(body_rect).union(bottom_body_rect);
let outline = if self.selected {
Shape::Rect(RectShape {
rect: titlebar_rect
.union(body_rect)
.union(bottom_body_rect)
.expand(1.0),
rect: node_rect.expand(1.0),
rounding,
fill: Color32::WHITE.lighten(0.8),
stroke: Stroke::none(),
@ -704,6 +759,9 @@ where
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)
};
@ -724,8 +782,12 @@ where
);
// Movement
*self.position += window_response.drag_delta();
if window_response.drag_delta().length_sq() > 0.0 {
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));
}

View File

@ -23,7 +23,9 @@ pub struct GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, UserSta
pub connection_in_progress: Option<(NodeId, AnyParameterId)>,
/// The currently selected node. Some interface actions depend on the
/// currently selected node.
pub selected_node: Option<NodeId>,
pub selected_nodes: Vec<NodeId>,
/// The mouse drag start position for an ongoing box selection.
pub ongoing_box_selection: Option<egui::Pos2>,
/// The position of each node.
pub node_positions: SecondaryMap<NodeId, egui::Pos2>,
/// The node finder is used to create new nodes.
@ -54,7 +56,8 @@ impl<NodeData, DataType, ValueType, NodeKind, UserState> Default
graph: Default::default(),
node_order: Default::default(),
connection_in_progress: Default::default(),
selected_node: Default::default(),
selected_nodes: Default::default(),
ongoing_box_selection: Default::default(),
node_positions: Default::default(),
node_finder: Default::default(),
pan_zoom: Default::default(),