Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 60 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ pub struct GraphTempMemory {
/// Primarily used to check for node selection, as we don't know the size of the node until the
/// contents have been instantiated.
node_sizes: NodeSizes,
/// A mapping from node IDs to their most recent `Response` IDs.
///
/// Includes the response ID for the node's outer scope `Ui`, the `Frame`,
/// and content UI.
///
/// This is used to check whether or not the pointer is over either the
/// graph or node prior to calling `graph_interaction`.
node_response_ids: NodeResponseIds,
/// The currently selected nodes and edges.
selection: Selection,
/// Whether or not the primary button was pressed on the graph area and is still down.
Expand All @@ -45,6 +53,7 @@ pub struct GraphTempMemory {
}

type NodeSizes = HashMap<egui::Id, egui::Vec2>;
type NodeResponseIds = HashMap<egui::Id, [egui::Id; 3]>;

#[derive(Clone, Default)]
struct Selection {
Expand Down Expand Up @@ -88,7 +97,7 @@ enum PressAction {

#[derive(Clone, Debug)]
struct PressedNode {
/// Unique Id of th node.
/// Unique Id of the node.
id: egui::Id,
/// The position of the node over the graph at the origin of the press.
position_at_origin: egui::Pos2,
Expand Down Expand Up @@ -263,9 +272,19 @@ impl Graph {
let ptr_on_graph = scene_response.hovered();

// Check for selection rectangle and node dragging.
let gmem_arc = memory(ui, self.id);
let graph_id = self.id;
let gmem_arc = memory(ui, graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");

// The pointer location if its over the graph or one of its nodes.
let graph_or_node_pos = {
let graph_id = ui.unique_id(); // Ui's respond with `unique_id`.
let node_ids = gmem.node_response_ids.values().flat_map(|ids| ids.clone());
graph_or_node_response(ui.ctx(), graph_id, node_ids, |res| {
res.interact_pointer_pos().or_else(|| res.hover_pos())
})
};

// FIXME: Here we grab the global pointer and transform its position
// to the graph scene space in order to check for initialising node
// drag events. However, doing this means we run the risk of
Expand All @@ -274,17 +293,14 @@ impl Graph {
// We should change this to get the pointer only if it is hovered or
// interacting with the scene or any of its child nodes somehow.
let pointer = ui.input(|i| i.pointer.clone());
if let Some(ptr_global) = pointer.interact_pos().or(pointer.hover_pos()) {
let ptr_graph = ui
.ctx()
.layer_transform_from_global(ui.layer_id())
.unwrap_or_default()
.mul_pos(ptr_global);

if let Some(ptr_graph) = graph_or_node_pos {
// Check for the closest socket.
let closest_socket = ui.response().hover_pos().and_then(|pos| {
find_closest_socket(pos, layout, &gmem, ui).map(|(socket, _dist_sqrd)| socket)
});
// FIXME: Rather than using `response.hover_pos()` which won't
// give the pointer if the mouse is over a node, we instead want
// to get the pointer if it's anywhere over a graph *or node*.
let closest_socket =
find_closest_socket(ptr_graph, layout, &gmem, ui)
.map(|(socket, _dist_sqrd)| socket);

// Check for graph interactions.
let interaction = graph_interaction(
Expand Down Expand Up @@ -334,7 +350,7 @@ impl Graph {
let mut visited = HashSet::default();

let show = Show {
graph_id: self.id,
graph_id,
graph_rect,
selection_rect,
select,
Expand All @@ -348,7 +364,7 @@ impl Graph {

let output = content(ui, show);

prune_unused_nodes(self.id, &visited, ui);
prune_unused_nodes(graph_id, &visited, ui);
bounding_rect = Some(ui.min_rect());

output
Expand Down Expand Up @@ -472,6 +488,7 @@ fn prune_unused_nodes(graph_id: egui::Id, visited: &HashSet<egui::Id>, ui: &mut
let gmem_arc = memory(ui, graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.node_sizes.retain(|k, _| visited.contains(k));
gmem.node_response_ids.retain(|k, _| visited.contains(k));
gmem.selection.nodes.retain(|k| visited.contains(k));
if let Some(socket) = gmem.closest_socket.as_ref() {
if !visited.contains(&socket.node) {
Expand Down Expand Up @@ -717,7 +734,9 @@ fn graph_interaction(
let delta = ptr_graph - pressed.origin_pos;
let target = node.position_at_origin + delta;
if let Some(current) = layout.get(&node.id) {
drag_nodes_delta = target - *current;
if pointer.primary_down() {
drag_nodes_delta = target - *current;
}
}
}
PressAction::Select => {
Expand All @@ -743,10 +762,7 @@ fn graph_interaction(
})
}
// Check for the beginning of a socket press or rectangular selection.
} else if ptr_on_graph
&& pointer.button_down(egui::PointerButton::Primary)
&& pointer.button_pressed(egui::PointerButton::Primary)
{
} else if ptr_on_graph && pointer.primary_pressed() {
// Choose which press action based on whether or not a socket was pressed.
let action = match closest_socket {
Some(socket) => PressAction::Socket(socket),
Expand Down Expand Up @@ -843,3 +859,28 @@ fn memory(ui: &egui::Ui, graph_id: egui::Id) -> Arc<Mutex<GraphTempMemory>> {
.clone()
})
}

/// Checks the graph, then all the nodes to see if any of their responses
/// satisfy the given predicate.
///
/// Normally used to check if the pointer is hovering above either the graph or
/// any of its known nodes.
///
/// NOTE: Responses for `Ui`s are associated with the `Ui`'s `unique_id`, not
/// its `id`. This means for graph and nodes, we need to be careful about using
/// the right `egui::Id`.
fn graph_or_node_response<R>(
ctx: &egui::Context,
graph_id: egui::Id,
node_ids: impl IntoIterator<Item = egui::Id>,
predicate: impl Fn(egui::Response) -> Option<R>,
) -> Option<R> {
for id in Some(graph_id).into_iter().chain(node_ids) {
if let Some(res) = ctx.read_response(id) {
if let Some(ret) = predicate(res) {
return Some(ret);
}
}
}
None
}
43 changes: 15 additions & 28 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,13 @@ impl Node {
}

// A `Ui` scope for placing the `Frame`.
let builder = egui::UiBuilder::new()
let mut builder = egui::UiBuilder::new()
.max_rect(put_rect)
.layer_id(frame_layer)
.sense(egui::Sense::click_and_drag());
// NOTE: We set the `id_salt` manually here, as if we use the builder
// method it uses the *hash* of our id rather than our id directly.
builder.id_salt = Some(self.id);
let inner_response = ui.scope_builder(builder, |ui| {
// Show the frame.
let inner_response = frame.show(ui, |ui| {
Expand All @@ -322,22 +325,28 @@ impl Node {
});

// Merge the content area response with the frame response.
inner_response.response.union(inner_response.inner)
let content_response = inner_response.inner;
let content_id = content_response.id;
let response = inner_response.response.union(content_response);
(response, content_id)
});
let mut response = inner_response.response.union(inner_response.inner);
let (frame_response, content_id) = inner_response.inner;
let frame_id = frame_response.id;
let mut response = inner_response.response.union(frame_response);

// Update the stored data for this node and check for edge events.
let mut edge_event = None;
{
let gmem_arc = crate::memory(ui, ctx.graph_id);
let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
gmem.node_sizes.insert(self.id, response.rect.size());
gmem.node_response_ids.insert(self.id, [response.id, frame_id, content_id]);

let ctrl_down = ui.input(|i| i.modifiers.ctrl);

// If the window is pressed, select the node.
let pointer = &ui.input(|i| i.pointer.clone());
if response.is_pointer_button_down_on() && primary_pressed(pointer) {
if response.is_pointer_button_down_on() && pointer.primary_pressed() {
// If ctrl is down, check for deselection.
let was_selected = gmem.selection.nodes.contains(&self.id);
if ctrl_down && was_selected {
Expand Down Expand Up @@ -368,7 +377,7 @@ impl Node {
}

// If the primary button was pressed, check for edge events.
} else if !response.is_pointer_button_down_on() && primary_pressed(pointer) {
} else if !response.is_pointer_button_down_on() && pointer.primary_pressed() {
// If this node's socket was pressed, create a start event.
if let Some(ref pressed) = gmem.pressed {
if let crate::PressAction::Socket(socket) = pressed.action {
Expand Down Expand Up @@ -398,7 +407,7 @@ impl Node {
if let Some(c) = gmem.closest_socket {
if r.kind == c.kind && self.id == r.node {
edge_event = Some(EdgeEvent::Cancelled);
} else if self.id == c.node && primary_released(&ui.input(|i| i.clone())) {
} else if self.id == c.node && ui.input(|i| i.pointer.primary_released()) {
let kind = c.kind;
let index = c.index;
edge_event = Some(EdgeEvent::Ended { kind, index });
Expand Down Expand Up @@ -626,25 +635,3 @@ pub fn default_frame(style: &egui::Style) -> egui::Frame {
frame.stroke.width = 0.0;
frame
}

fn only_primary_down(pointer: &egui::PointerState) -> bool {
pointer.button_down(egui::PointerButton::Primary)
&& !pointer.button_down(egui::PointerButton::Middle)
&& !pointer.button_down(egui::PointerButton::Secondary)
}

fn primary_pressed(pointer: &egui::PointerState) -> bool {
pointer.any_pressed() && only_primary_down(pointer)
}

fn primary_released(input: &egui::InputState) -> bool {
input.pointer.any_released()
&& input.events.iter().any(|e| match e {
egui::Event::PointerButton {
button: egui::PointerButton::Primary,
pressed: false,
..
} => true,
_ => false,
})
}