Add layer locked toggle icon and context menu entry to node graph (#3855)

* Add layer locked toggle icon and context menu entry to node graph

* Simplify logic
This commit is contained in:
Keavon Chambers
2026-03-03 22:30:04 -08:00
committed by GitHub
parent 54a02dedd5
commit 8a75c0c1e1
6 changed files with 130 additions and 8 deletions

View File

@@ -809,10 +809,27 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let context_menu_data = if let Some(node_id) = clicked_id {
let currently_is_node = !network_interface.is_layer(&node_id, breadcrumb_network_path);
let can_be_layer = network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path);
// Determine which layers the Lock/Unlock action would affect:
// - If the right-clicked node is in the selection, it affects all selected layers
// - If the right-clicked node is not in the selection, it affects just the right-clicked node
let selected_nodes = network_interface.selected_nodes_in_nested_network(selection_network_path);
let is_clicked_selected = selected_nodes.as_ref().is_some_and(|selected| selected.selected_nodes().any(|id| *id == node_id));
let affected_layer_ids = if is_clicked_selected {
selected_nodes.map(|selected| selected.selected_nodes().copied().filter(|id| network_interface.is_layer(id, selection_network_path)).collect())
} else {
network_interface.is_layer(&node_id, selection_network_path).then(|| vec![node_id])
}
.unwrap_or_default();
let has_selected_layers = !affected_layer_ids.is_empty();
let all_selected_layers_locked = has_selected_layers && affected_layer_ids.iter().all(|id| network_interface.is_locked(id, selection_network_path));
ContextMenuData::ModifyNode {
can_be_layer,
currently_is_node,
node_id,
has_selected_layers,
all_selected_layers_locked,
}
} else {
ContextMenuData::CreateNode { compatible_type: None }
@@ -896,6 +913,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
}
// Toggle lock of clicked node and return
if let Some(clicked_lock) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Lock, selection_network_path) {
responses.add(NodeGraphMessage::ToggleLocked { node_id: clicked_lock });
return;
}
// Alt-click sets the clicked node as previewed
if alt_click && let Some(clicked_node) = clicked_id {
self.preview_on_mouse_up = Some(clicked_node);
@@ -1834,9 +1857,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
log::error!("Could not get selected nodes in NodeGraphMessage::ToggleSelectedLocked");
return;
};
let node_ids = selected_nodes.selected_nodes().cloned().collect::<Vec<_>>();
let node_ids = selected_nodes
.selected_nodes()
.filter(|node_id| network_interface.is_layer(node_id, selection_network_path))
.cloned()
.collect::<Vec<_>>();
// If any of the selected layers are locked, show them all. Otherwise, hide them all.
if node_ids.is_empty() {
return;
}
// If any of the selected layers are unlocked, lock them all. Otherwise, unlock them all.
let locked = !node_ids.iter().all(|node_id| network_interface.is_locked(node_id, selection_network_path));
responses.add(DocumentMessage::AddTransaction);

View File

@@ -147,6 +147,10 @@ pub enum ContextMenuData {
can_be_layer: bool,
#[serde(rename = "currentlyIsNode")]
currently_is_node: bool,
#[serde(rename = "hasSelectedLayers")]
has_selected_layers: bool,
#[serde(rename = "allSelectedLayersLocked")]
all_selected_layers_locked: bool,
},
CreateNode {
#[serde(rename = "compatibleType")]

View File

@@ -2108,9 +2108,10 @@ impl NodeNetworkInterface {
let grip_padding = 4.;
let grip_width = 8.;
let lock_icon_width = if self.is_locked(node_id, network_path) { GRID_SIZE as f64 } else { 0. };
let icon_overhang_width = GRID_SIZE as f64 / 2.;
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + GAP_WIDTH + text_width + grip_padding + grip_width + icon_overhang_width;
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + GAP_WIDTH + text_width + grip_padding + grip_width + lock_icon_width + icon_overhang_width;
let layer_width = ((layer_width_pixels / 24.).ceil() as u32).max(8);
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
@@ -2527,14 +2528,25 @@ impl NodeNetworkInterface {
});
let width = layer_width_cells * crate::consts::GRID_SIZE;
let height = 2 * crate::consts::GRID_SIZE;
let locked = self.is_locked(node_id, network_path);
// Update visibility button click target
let visibility_offset = node_top_left + DVec2::new(width as f64, 24.);
let subpath = Subpath::new_rounded_rectangle(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]);
let visibility_click_target = ClickTarget::new_with_subpath(subpath, 0.);
// Update grip button click target, which is positioned to the left of the left most icon
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2., 24.);
// Update lock button click target, positioned one grid unit to the left of the visibility button (only when locked)
let lock_click_target = if locked {
let lock_offset = node_top_left + DVec2::new(width as f64 - GRID_SIZE as f64, 24.);
let subpath = Subpath::new_rounded_rectangle(DVec2::new(-12., -12.) + lock_offset, DVec2::new(12., 12.) + lock_offset, [3.; 4]);
Some(ClickTarget::new_with_subpath(subpath, 0.))
} else {
None
};
// Update grip button click target, which is positioned to the left of the leftmost icon
let icons_width = if locked { GRID_SIZE as f64 } else { 0. };
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2. - icons_width, 24.);
let subpath = Subpath::new_rounded_rectangle(DVec2::new(-8., -12.) + grip_offset_right_edge, DVec2::new(0., 12.) + grip_offset_right_edge, [0.; 4]);
let grip_click_target = ClickTarget::new_with_subpath(subpath, 0.);
@@ -2552,6 +2564,7 @@ impl NodeNetworkInterface {
port_click_targets,
node_type_metadata: NodeTypeClickTargets::Layer(LayerClickTargets {
visibility_click_target,
lock_click_target,
grip_click_target,
}),
}
@@ -2749,10 +2762,17 @@ impl NodeNetworkInterface {
}
}
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
// Visibility button (eye icon)
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
icon_click_targets.push(subpath.to_bezpath().to_svg());
}
// Lock button (padlock icon), only when the layer is locked
if let Some(lock_click_target) = &layer_metadata.lock_click_target
&& let ClickTargetType::Subpath(subpath) = lock_click_target.target_type()
{
icon_click_targets.push(subpath.to_bezpath().to_svg());
}
// Drag grip (dotted symbol)
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
icon_click_targets.push(subpath.to_bezpath().to_svg());
}
@@ -2882,6 +2902,7 @@ impl NodeNetworkInterface {
if let NodeTypeClickTargets::Layer(layer) = &transient_node_metadata.node_type_metadata {
match click_target_type {
LayerClickTargetTypes::Visibility => layer.visibility_click_target.intersect_point_no_stroke(point).then_some(*node_id),
LayerClickTargetTypes::Lock => layer.lock_click_target.as_ref().and_then(|target| target.intersect_point_no_stroke(point).then_some(*node_id)),
LayerClickTargetTypes::Grip => layer.grip_click_target.intersect_point_no_stroke(point).then_some(*node_id),
}
} else {
@@ -4508,6 +4529,8 @@ impl NodeNetworkInterface {
node_metadata.persistent_metadata.locked = locked;
self.transaction_modified();
self.try_unload_layer_width(node_id, network_path);
self.unload_node_click_targets(node_id, network_path);
}
pub fn set_to_node_or_layer(&mut self, node_id: &NodeId, network_path: &[NodeId], is_layer: bool) {
@@ -6419,6 +6442,8 @@ pub enum NodeTypeClickTargets {
pub struct LayerClickTargets {
/// Cache for all visibility buttons. Should be automatically updated when update_click_target is called
pub visibility_click_target: ClickTarget,
/// Cache for the lock icon button, only present when the layer is locked.
pub lock_click_target: Option<ClickTarget>,
/// Cache for the grip icon, which is next to the visibility button.
pub grip_click_target: ClickTarget,
// TODO: Store click target for the preview button, which will appear when the node is a selected/(hovered?) layer node
@@ -6427,6 +6452,7 @@ pub struct LayerClickTargets {
pub enum LayerClickTargetTypes {
Visibility,
Lock,
Grip,
// Preview,
}