diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 68bbd66a74..5efc67cd5d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -489,33 +489,36 @@ impl<'a> MessageHandler> for NodeGrap self.update_selection_action_buttons(document_network, document_metadata, selected_nodes, responses); } NodeGraphMessage::ToggleSelectedLocked => { - if let Some(network) = document_network.nested_network(&self.network) { - responses.add(DocumentMessage::StartTransaction); + responses.add(DocumentMessage::StartTransaction); - let new_locked = !selected_nodes.selected_nodes().any(|id| network.locked.contains(id)); - for &node_id in selected_nodes.selected_nodes() { - responses.add(NodeGraphMessage::SetLocked { node_id, locked: new_locked }); - } + let is_locked = !selected_nodes.selected_nodes().any(|&id| document_metadata.node_is_locked(id)); + + for &node_id in selected_nodes.selected_nodes() { + responses.add(NodeGraphMessage::SetLocked { node_id, locked: is_locked }); } } NodeGraphMessage::ToggleLocked { node_id } => { - if let Some(network) = document_network.nested_network(&self.network) { - let new_locked = !network.locked.contains(&node_id); - responses.add(NodeGraphMessage::SetLocked { node_id, locked: new_locked }); - } + responses.add(DocumentMessage::StartTransaction); + let is_locked = !document_metadata.node_is_locked(node_id); + responses.add(NodeGraphMessage::SetLocked { node_id, locked: is_locked }); } NodeGraphMessage::SetLocked { node_id, locked } => { if let Some(network) = document_network.nested_network_mut(&self.network) { - if !locked { - network.locked.retain(|&id| node_id != id); + let is_locked = if !locked { + false } else if !network.imports.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) { - network.locked.push(node_id); - } + true + } else { + return; + }; + let Some(node) = network.nodes.get_mut(&node_id) else { return }; + node.locked = is_locked; + if network.connected_to_output(node_id) { responses.add(NodeGraphMessage::RunDocumentGraph); } } - self.update_selection_action_buttons(document_network, selected_nodes, responses); + self.update_selection_action_buttons(document_network, document_metadata, selected_nodes, responses); } NodeGraphMessage::SetName { node_id, name } => { responses.add(DocumentMessage::StartTransaction); @@ -619,6 +622,8 @@ impl NodeGraphMessageHandler { .widget_holder(); widgets.push(hide_button); + // Check if any of the selected nodes are locked + let is_locked = selected_nodes.selected_nodes().all(|&id| document_metadata.node_is_locked(id)); let (lock_unlock_label, lock_unlock_icon) = if is_locked { ("Make Unlock", "Lock") } else { ("Make Lock", "Unlock") }; let lock_button = TextButton::new(lock_unlock_label) .icon(Some(lock_unlock_icon.to_string())) @@ -794,6 +799,7 @@ impl NodeGraphMessageHandler { position: node.metadata.position.into(), previewed: network.outputs_contain(node_id), visible: node.visible, + locked: node.locked, errors: errors.map(|e| format!("{e:?}")), }); } @@ -823,6 +829,7 @@ impl NodeGraphMessageHandler { name: network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(), tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() }, visible: node.visible, + locked: node.locked, }; responses.add(FrontendMessage::UpdateDocumentLayerDetails { data }); } diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 6bf6ff5383..0f71ce0d54 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -85,6 +85,7 @@ pub struct FrontendNode { pub exposed_outputs: Vec, pub position: (i32, i32), pub visible: bool, + pub locked: bool, pub previewed: bool, pub errors: Option, } diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 944675290a..1132bfae77 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -23,6 +23,7 @@ pub struct DocumentMetadata { artboards: HashSet, folders: HashSet, hidden: HashSet, + locked: HashSet, click_targets: HashMap>, /// Transform from document space to viewport space. pub document_to_viewport: DAffine2, @@ -36,6 +37,7 @@ impl Default for DocumentMetadata { artboards: HashSet::new(), folders: HashSet::new(), hidden: HashSet::new(), + locked: HashSet::new(), click_targets: HashMap::new(), document_to_viewport: DAffine2::IDENTITY, } @@ -126,6 +128,10 @@ impl DocumentMetadata { !self.hidden.contains(&layer) } + pub fn node_is_locked(&self, layer: NodeId) -> bool { + self.locked.contains(&layer) + } + /// Folders sorted from most nested to least nested pub fn folders_sorted_by_most_nested(&self, layers: impl Iterator) -> Vec { let mut folders: Vec<_> = layers.filter(|layer| self.folders.contains(layer)).collect(); @@ -149,6 +155,7 @@ impl DocumentMetadata { self.artboards = HashSet::new(); self.folders = HashSet::new(); self.hidden = HashSet::new(); + self.locked = HashSet::new(); let id = graph.exports[0].node_id; let Some(output_node) = graph.nodes.get(&id) else { @@ -180,6 +187,10 @@ impl DocumentMetadata { if !current_node.visible { self.hidden.insert(current_node_id); } + + if current_node.locked { + self.locked.insert(current_node_id); + } } // Get the sibling below diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index b47ac4c763..aaa879a2f5 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -47,6 +47,7 @@ pub struct LayerPanelEntry { pub layer_classification: LayerClassification, pub expanded: bool, pub visible: bool, + pub locked: bool, #[serde(rename = "parentId")] pub parent_id: Option, pub depth: usize, @@ -64,12 +65,12 @@ impl SelectedNodes { self.selected_layers(metadata).filter(move |&layer| self.layer_visible(layer, metadata)) } - pub fn layer_ulocked(&self, layer: LayerNodeIdentifier, network: &NodeNetwork, metadata: &DocumentMetadata) -> bool { - !layer.ancestors(metadata).any(|layer| network.locked.contains(&layer.to_node())) + pub fn layer_locked(&self, layer: LayerNodeIdentifier, metadata: &DocumentMetadata) -> bool { + layer.ancestors(metadata).any(|layer| metadata.node_is_locked(layer.to_node())) } - pub fn selected_ulocked_layers<'a>(&'a self, network: &'a NodeNetwork, metadata: &'a DocumentMetadata) -> impl Iterator + '_ { - self.selected_layers(metadata).filter(move |&layer| self.layer_ulocked(layer, network, metadata)) + pub fn selected_locked_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator + '_ { + self.selected_layers(metadata).filter(move |&layer| self.layer_locked(layer, metadata)) } pub fn selected_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator + '_ { diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 09c3a98b40..23f58c092a 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -162,6 +162,9 @@ pub struct DocumentNode { /// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step. #[serde(default = "return_true")] pub visible: bool, + /// Represents the lock icon for locking/unlocking the node in the graph UI. When locked, a node cannot be moved in the graph UI. + #[serde(default)] + pub locked: bool, /// Metadata about the node including its position in the graph UI. pub metadata: DocumentNodeMetadata, /// When two different proto nodes hash to the same value (e.g. two value nodes each containing `2_u32` or two multiply nodes that have the same node IDs as input), the duplicates are removed. @@ -210,6 +213,7 @@ impl Default for DocumentNode { has_primary_output: true, implementation: Default::default(), visible: true, + locked: Default::default(), metadata: Default::default(), skip_deduplication: Default::default(), world_state_hash: Default::default(),