Skip to content

Add shaking input gesture to disconnect a node being dragged #2889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 19, 2025
Merged
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
1 change: 1 addition & 0 deletions editor/src/messages/input_mapper/input_mapper_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ pub enum InputMapperMessage {

// Messages
PointerMove,
PointerShake,
WheelScroll,
}
22 changes: 13 additions & 9 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop),
//
// NodeGraphMessage
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: false}),
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: false, alt_click: false, right_click: false}),
entry!(KeyDown(MouseLeft); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: true, alt_click: false, right_click: false}),
entry!(KeyDown(MouseLeft); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: true, alt_click: false, right_click: false}),
entry!(KeyDown(MouseLeft); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: true, right_click: false}),
entry!(KeyDown(MouseRight); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: true}),
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: false }),
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: false, alt_click: false, right_click: false }),
entry!(KeyDown(MouseLeft); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: true, alt_click: false, right_click: false }),
entry!(KeyDown(MouseLeft); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: true, alt_click: false, right_click: false }),
entry!(KeyDown(MouseLeft); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: true, right_click: false }),
entry!(KeyDown(MouseRight); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: true }),
entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork),
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove {shift: Shift}),
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove { shift: Shift }),
entry!(PointerShake; action_dispatch=NodeGraphMessage::ShakeNode),
entry!(KeyUp(MouseLeft); action_dispatch=NodeGraphMessage::PointerUp),
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
Expand Down Expand Up @@ -417,7 +418,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Tab); modifiers=[Control], action_dispatch=PortfolioMessage::NextDocument),
entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument),
entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
entry!(KeyDown(KeyW); modifiers=[Accel,Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::OpenDocument),
entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import),
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
Expand All @@ -440,7 +441,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview),
entry!(KeyDown(Home); modifiers=[Shift], action_dispatch=AnimationMessage::RestartAnimation),
];
let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move) = mappings;
let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move, mut pointer_shake) = mappings;

let sort = |list: &mut KeyMappingEntries| list.0.sort_by(|a, b| b.modifiers.count_ones().cmp(&a.modifiers.count_ones()));
// Sort the sublists of `key_up`, `key_down`, `key_up_no_repeat`, and `key_down_no_repeat`
Expand All @@ -457,6 +458,8 @@ pub fn input_mappings() -> Mapping {
sort(&mut wheel_scroll);
// Sort `pointer_move`
sort(&mut pointer_move);
// Sort `pointer_shake`
sort(&mut pointer_shake);

Mapping {
key_up,
Expand All @@ -466,6 +469,7 @@ pub fn input_mappings() -> Mapping {
double_click,
wheel_scroll,
pointer_move,
pointer_shake,
}
}

Expand Down
4 changes: 3 additions & 1 deletion editor/src/messages/input_mapper/utility_types/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ macro_rules! mapping {
let mut double_click = KeyMappingEntries::mouse_buttons_arrays();
let mut wheel_scroll = KeyMappingEntries::new();
let mut pointer_move = KeyMappingEntries::new();
let mut pointer_shake = KeyMappingEntries::new();

$(
// Each of the many entry slices, one specified per action
Expand All @@ -104,14 +105,15 @@ macro_rules! mapping {
InputMapperMessage::DoubleClick(key) => &mut double_click[key as usize],
InputMapperMessage::WheelScroll => &mut wheel_scroll,
InputMapperMessage::PointerMove => &mut pointer_move,
InputMapperMessage::PointerShake => &mut pointer_shake,
};
// Push each entry to the corresponding `KeyMappingEntries` list for its input type
corresponding_list.push(entry.clone());
}
}
)*

(key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, wheel_scroll, pointer_move)
(key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, wheel_scroll, pointer_move, pointer_shake)
}};
}

Expand Down
3 changes: 3 additions & 0 deletions editor/src/messages/input_mapper/utility_types/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct Mapping {
pub double_click: [KeyMappingEntries; NUMBER_OF_MOUSE_BUTTONS],
pub wheel_scroll: KeyMappingEntries,
pub pointer_move: KeyMappingEntries,
pub pointer_shake: KeyMappingEntries,
}

impl Default for Mapping {
Expand Down Expand Up @@ -47,6 +48,7 @@ impl Mapping {
InputMapperMessage::DoubleClick(key) => &self.double_click[*key as usize],
InputMapperMessage::WheelScroll => &self.wheel_scroll,
InputMapperMessage::PointerMove => &self.pointer_move,
InputMapperMessage::PointerShake => &self.pointer_shake,
}
}

Expand All @@ -59,6 +61,7 @@ impl Mapping {
InputMapperMessage::DoubleClick(key) => &mut self.double_click[*key as usize],
InputMapperMessage::WheelScroll => &mut self.wheel_scroll,
InputMapperMessage::PointerMove => &mut self.pointer_move,
InputMapperMessage::PointerShake => &mut self.pointer_shake,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub enum InputPreprocessorMessage {
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerShake { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
CurrentTime { timestamp: u64 },
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f

self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);

let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;

responses.add(InputMapperMessage::PointerShake);
}
InputPreprocessorMessage::CurrentTime { timestamp } => {
responses.add(AnimationMessage::SetTime { time: timestamp as f64 });
self.time = timestamp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ pub enum NodeGraphMessage {
node_id: NodeId,
parent: LayerNodeIdentifier,
},
SetChainPosition {
node_id: NodeId,
},
PasteNodes {
serialized_nodes: String,
},
Expand All @@ -98,6 +101,7 @@ pub enum NodeGraphMessage {
PointerOutsideViewport {
shift: Key,
},
ShakeNode,
RemoveImport {
import_index: usize,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::messages::portfolio::document::node_graph::utility_types::{ContextMen
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
use crate::messages::portfolio::document::utility_types::network_interface::{
self, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
};
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
Expand Down Expand Up @@ -56,6 +56,8 @@ pub struct NodeGraphMessageHandler {
/// If dragging the selected nodes, this stores the starting position both in viewport and node graph coordinates,
/// plus a flag indicating if it has been dragged since the mousedown began.
pub drag_start: Option<(DragStart, bool)>,
// Store the selected chain nodes on drag start so they can be reconnected if shaken
pub drag_start_chain_nodes: Vec<NodeId>,
/// If dragging the background to create a box selection, this stores its starting point in node graph coordinates,
/// plus a flag indicating if it has been dragged since the mousedown began.
box_selection_start: Option<(DVec2, bool)>,
Expand Down Expand Up @@ -601,6 +603,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
NodeGraphMessage::MoveNodeToChainStart { node_id, parent } => {
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path);
}
NodeGraphMessage::SetChainPosition { node_id } => {
network_interface.set_chain_position(&node_id, selection_network_path);
}
NodeGraphMessage::PasteNodes { serialized_nodes } => {
let data = match serde_json::from_str::<Vec<(NodeId, NodeTemplate)>>(&serialized_nodes) {
Ok(d) => d,
Expand Down Expand Up @@ -854,6 +859,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
};

self.drag_start = Some((drag_start, false));
let selected_chain_nodes = updated_selected
.iter()
.filter(|node_id| network_interface.is_chain(node_id, selection_network_path))
.copied()
.collect::<Vec<_>>();
self.drag_start_chain_nodes = selected_chain_nodes
.iter()
.flat_map(|selected| {
network_interface
.upstream_flow_back_from_nodes(vec![*selected], selection_network_path, FlowType::PrimaryFlow)
.skip(1)
.filter(|node_id| network_interface.is_chain(node_id, selection_network_path))
})
.collect::<Vec<_>>();
self.begin_dragging = true;
self.node_has_moved_in_drag = false;
self.update_node_graph_hints(responses);
Expand Down Expand Up @@ -1221,6 +1240,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
{
return None;
}
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
wire.rectangle_intersections_exist(bounding_box[0], bounding_box[1]).then_some((input, is_stack))
})
Expand Down Expand Up @@ -1303,6 +1323,135 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
self.auto_panning.stop(&messages, responses);
}
}
NodeGraphMessage::ShakeNode => {
let Some(drag_start) = &self.drag_start else {
log::error!("Drag start should be initialized when shaking a node");
return;
};

let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
return;
};

let viewport_location = ipp.mouse.position;
let point = network_metadata
.persistent_metadata
.navigation_metadata
.node_graph_to_viewport
.inverse()
.transform_point2(viewport_location);

// Collect the distance to move the shaken nodes after the undo
let graph_delta = IVec2::new(((point.x - drag_start.0.start_x) / 24.).round() as i32, ((point.y - drag_start.0.start_y) / 24.).round() as i32);

// Undo to the state of the graph before shaking
responses.add(DocumentMessage::AbortTransaction);

// Add a history step to abort to the state before shaking if right clicked
responses.add(DocumentMessage::StartTransaction);

let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
log::error!("Could not get selected nodes in ShakeNode");
return;
};

let mut all_selected_nodes = selected_nodes.0.iter().copied().collect::<HashSet<_>>();
for selected_layer in selected_nodes
.0
.iter()
.filter(|selected_node| network_interface.is_layer(selected_node, selection_network_path))
.copied()
.collect::<Vec<_>>()
{
for sole_dependent in network_interface.upstream_nodes_below_layer(&selected_layer, selection_network_path) {
all_selected_nodes.insert(sole_dependent);
}
}

for selected_node in &all_selected_nodes {
// Handle inputs of selected node
for input_index in 0..network_interface.number_of_inputs(selected_node, selection_network_path) {
let input_connector = InputConnector::node(*selected_node, input_index);
// Only disconnect inputs to non selected nodes
if network_interface
.upstream_output_connector(&input_connector, selection_network_path)
.and_then(|connector| connector.node_id())
.is_some_and(|node_id| !all_selected_nodes.contains(&node_id))
{
responses.add(NodeGraphMessage::DisconnectInput { input_connector });
}
}

let number_of_outputs = network_interface.number_of_outputs(selected_node, selection_network_path);
let first_deselected_upstream_node = network_interface
.upstream_flow_back_from_nodes(vec![*selected_node], selection_network_path, FlowType::PrimaryFlow)
.find(|upstream_node| !all_selected_nodes.contains(upstream_node));
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
log::error!("Could not get output wires in shake input");
continue;
};

// Disconnect output wires to non selected nodes
for output_index in 0..number_of_outputs {
let output_connector = OutputConnector::node(*selected_node, output_index);
if let Some(downstream_connections) = outward_wires.get(&output_connector) {
for &input_connector in downstream_connections {
if input_connector.node_id().is_some_and(|downstream_node| !all_selected_nodes.contains(&downstream_node)) {
responses.add(NodeGraphMessage::DisconnectInput { input_connector });
}
}
}
}

// Handle reconnection
// Find first non selected upstream node by primary flow
if let Some(first_deselected_upstream_node) = first_deselected_upstream_node {
let Some(downstream_connections_to_first_output) = outward_wires.get(&OutputConnector::node(*selected_node, 0)).cloned() else {
log::error!("Could not get downstream_connections_to_first_output in shake node");
return;
};
// Reconnect only if all downstream outputs are not selected
if !downstream_connections_to_first_output
.iter()
.any(|connector| connector.node_id().is_some_and(|node_id| all_selected_nodes.contains(&node_id)))
{
// Find what output on the deselected upstream node to reconnect to
for output_index in 0..network_interface.number_of_outputs(&first_deselected_upstream_node, selection_network_path) {
let output_connector = &OutputConnector::node(first_deselected_upstream_node, output_index);
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
log::error!("Could not get output wires in shake input");
continue;
};
if let Some(inputs) = outward_wires.get(output_connector) {
// This can only run once
if inputs.iter().any(|input_connector| {
input_connector
.node_id()
.is_some_and(|upstream_node| all_selected_nodes.contains(&upstream_node) && input_connector.input_index() == 0)
}) {
// Output index is the output of the deselected upstream node to reconnect to
for downstream_connections_to_first_output in &downstream_connections_to_first_output {
responses.add(NodeGraphMessage::CreateWire {
output_connector: OutputConnector::node(first_deselected_upstream_node, output_index),
input_connector: *downstream_connections_to_first_output,
});
}
}
}

// Set all chain nodes back to chain position
// TODO: Fix
// for chain_node_to_reset in std::mem::take(&mut self.drag_start_chain_nodes) {
// responses.add(NodeGraphMessage::SetChainPosition { node_id: chain_node_to_reset });
// }
}
}
}
}
responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: false });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::RemoveImport { import_index: usize } => {
network_interface.remove_import(usize, selection_network_path);
responses.add(NodeGraphMessage::SendGraph);
Expand Down Expand Up @@ -1823,6 +1972,12 @@ impl NodeGraphMessageHandler {
));
}

if self.drag_start.is_some() {
common.extend(actions!(NodeGraphMessageDiscriminant;
ShakeNode,
));
}

common
}

Expand Down Expand Up @@ -2597,6 +2752,7 @@ impl Default for NodeGraphMessageHandler {
node_has_moved_in_drag: false,
shift_without_push: false,
box_selection_start: None,
drag_start_chain_nodes: Vec::new(),
selection_before_pointer_down: Vec::new(),
disconnecting: None,
initial_disconnecting: false,
Expand Down
Loading
Loading