Skip to content

Commit 8a75c0c

Browse files
authored
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
1 parent 54a02de commit 8a75c0c

File tree

6 files changed

+130
-8
lines changed

6 files changed

+130
-8
lines changed

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -809,10 +809,27 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
809809
let context_menu_data = if let Some(node_id) = clicked_id {
810810
let currently_is_node = !network_interface.is_layer(&node_id, breadcrumb_network_path);
811811
let can_be_layer = network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path);
812+
813+
// Determine which layers the Lock/Unlock action would affect:
814+
// - If the right-clicked node is in the selection, it affects all selected layers
815+
// - If the right-clicked node is not in the selection, it affects just the right-clicked node
816+
let selected_nodes = network_interface.selected_nodes_in_nested_network(selection_network_path);
817+
let is_clicked_selected = selected_nodes.as_ref().is_some_and(|selected| selected.selected_nodes().any(|id| *id == node_id));
818+
let affected_layer_ids = if is_clicked_selected {
819+
selected_nodes.map(|selected| selected.selected_nodes().copied().filter(|id| network_interface.is_layer(id, selection_network_path)).collect())
820+
} else {
821+
network_interface.is_layer(&node_id, selection_network_path).then(|| vec![node_id])
822+
}
823+
.unwrap_or_default();
824+
let has_selected_layers = !affected_layer_ids.is_empty();
825+
let all_selected_layers_locked = has_selected_layers && affected_layer_ids.iter().all(|id| network_interface.is_locked(id, selection_network_path));
826+
812827
ContextMenuData::ModifyNode {
813828
can_be_layer,
814829
currently_is_node,
815830
node_id,
831+
has_selected_layers,
832+
all_selected_layers_locked,
816833
}
817834
} else {
818835
ContextMenuData::CreateNode { compatible_type: None }
@@ -896,6 +913,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
896913
return;
897914
}
898915

916+
// Toggle lock of clicked node and return
917+
if let Some(clicked_lock) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Lock, selection_network_path) {
918+
responses.add(NodeGraphMessage::ToggleLocked { node_id: clicked_lock });
919+
return;
920+
}
921+
899922
// Alt-click sets the clicked node as previewed
900923
if alt_click && let Some(clicked_node) = clicked_id {
901924
self.preview_on_mouse_up = Some(clicked_node);
@@ -1834,9 +1857,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
18341857
log::error!("Could not get selected nodes in NodeGraphMessage::ToggleSelectedLocked");
18351858
return;
18361859
};
1837-
let node_ids = selected_nodes.selected_nodes().cloned().collect::<Vec<_>>();
1860+
let node_ids = selected_nodes
1861+
.selected_nodes()
1862+
.filter(|node_id| network_interface.is_layer(node_id, selection_network_path))
1863+
.cloned()
1864+
.collect::<Vec<_>>();
1865+
1866+
if node_ids.is_empty() {
1867+
return;
1868+
}
18381869

1839-
// If any of the selected layers are locked, show them all. Otherwise, hide them all.
1870+
// If any of the selected layers are unlocked, lock them all. Otherwise, unlock them all.
18401871
let locked = !node_ids.iter().all(|node_id| network_interface.is_locked(node_id, selection_network_path));
18411872

18421873
responses.add(DocumentMessage::AddTransaction);

editor/src/messages/portfolio/document/node_graph/utility_types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ pub enum ContextMenuData {
147147
can_be_layer: bool,
148148
#[serde(rename = "currentlyIsNode")]
149149
currently_is_node: bool,
150+
#[serde(rename = "hasSelectedLayers")]
151+
has_selected_layers: bool,
152+
#[serde(rename = "allSelectedLayersLocked")]
153+
all_selected_layers_locked: bool,
150154
},
151155
CreateNode {
152156
#[serde(rename = "compatibleType")]

editor/src/messages/portfolio/document/utility_types/network_interface.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2108,9 +2108,10 @@ impl NodeNetworkInterface {
21082108

21092109
let grip_padding = 4.;
21102110
let grip_width = 8.;
2111+
let lock_icon_width = if self.is_locked(node_id, network_path) { GRID_SIZE as f64 } else { 0. };
21112112
let icon_overhang_width = GRID_SIZE as f64 / 2.;
21122113

2113-
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + GAP_WIDTH + text_width + grip_padding + grip_width + icon_overhang_width;
2114+
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + GAP_WIDTH + text_width + grip_padding + grip_width + lock_icon_width + icon_overhang_width;
21142115
let layer_width = ((layer_width_pixels / 24.).ceil() as u32).max(8);
21152116

21162117
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
@@ -2527,14 +2528,25 @@ impl NodeNetworkInterface {
25272528
});
25282529
let width = layer_width_cells * crate::consts::GRID_SIZE;
25292530
let height = 2 * crate::consts::GRID_SIZE;
2531+
let locked = self.is_locked(node_id, network_path);
25302532

25312533
// Update visibility button click target
25322534
let visibility_offset = node_top_left + DVec2::new(width as f64, 24.);
25332535
let subpath = Subpath::new_rounded_rectangle(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]);
25342536
let visibility_click_target = ClickTarget::new_with_subpath(subpath, 0.);
25352537

2536-
// Update grip button click target, which is positioned to the left of the left most icon
2537-
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2., 24.);
2538+
// Update lock button click target, positioned one grid unit to the left of the visibility button (only when locked)
2539+
let lock_click_target = if locked {
2540+
let lock_offset = node_top_left + DVec2::new(width as f64 - GRID_SIZE as f64, 24.);
2541+
let subpath = Subpath::new_rounded_rectangle(DVec2::new(-12., -12.) + lock_offset, DVec2::new(12., 12.) + lock_offset, [3.; 4]);
2542+
Some(ClickTarget::new_with_subpath(subpath, 0.))
2543+
} else {
2544+
None
2545+
};
2546+
2547+
// Update grip button click target, which is positioned to the left of the leftmost icon
2548+
let icons_width = if locked { GRID_SIZE as f64 } else { 0. };
2549+
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2. - icons_width, 24.);
25382550
let subpath = Subpath::new_rounded_rectangle(DVec2::new(-8., -12.) + grip_offset_right_edge, DVec2::new(0., 12.) + grip_offset_right_edge, [0.; 4]);
25392551
let grip_click_target = ClickTarget::new_with_subpath(subpath, 0.);
25402552

@@ -2552,6 +2564,7 @@ impl NodeNetworkInterface {
25522564
port_click_targets,
25532565
node_type_metadata: NodeTypeClickTargets::Layer(LayerClickTargets {
25542566
visibility_click_target,
2567+
lock_click_target,
25552568
grip_click_target,
25562569
}),
25572570
}
@@ -2749,10 +2762,17 @@ impl NodeNetworkInterface {
27492762
}
27502763
}
27512764
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
2765+
// Visibility button (eye icon)
27522766
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
27532767
icon_click_targets.push(subpath.to_bezpath().to_svg());
27542768
}
2755-
2769+
// Lock button (padlock icon), only when the layer is locked
2770+
if let Some(lock_click_target) = &layer_metadata.lock_click_target
2771+
&& let ClickTargetType::Subpath(subpath) = lock_click_target.target_type()
2772+
{
2773+
icon_click_targets.push(subpath.to_bezpath().to_svg());
2774+
}
2775+
// Drag grip (dotted symbol)
27562776
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
27572777
icon_click_targets.push(subpath.to_bezpath().to_svg());
27582778
}
@@ -2882,6 +2902,7 @@ impl NodeNetworkInterface {
28822902
if let NodeTypeClickTargets::Layer(layer) = &transient_node_metadata.node_type_metadata {
28832903
match click_target_type {
28842904
LayerClickTargetTypes::Visibility => layer.visibility_click_target.intersect_point_no_stroke(point).then_some(*node_id),
2905+
LayerClickTargetTypes::Lock => layer.lock_click_target.as_ref().and_then(|target| target.intersect_point_no_stroke(point).then_some(*node_id)),
28852906
LayerClickTargetTypes::Grip => layer.grip_click_target.intersect_point_no_stroke(point).then_some(*node_id),
28862907
}
28872908
} else {
@@ -4508,6 +4529,8 @@ impl NodeNetworkInterface {
45084529

45094530
node_metadata.persistent_metadata.locked = locked;
45104531
self.transaction_modified();
4532+
self.try_unload_layer_width(node_id, network_path);
4533+
self.unload_node_click_targets(node_id, network_path);
45114534
}
45124535

45134536
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 {
64196442
pub struct LayerClickTargets {
64206443
/// Cache for all visibility buttons. Should be automatically updated when update_click_target is called
64216444
pub visibility_click_target: ClickTarget,
6445+
/// Cache for the lock icon button, only present when the layer is locked.
6446+
pub lock_click_target: Option<ClickTarget>,
64226447
/// Cache for the grip icon, which is next to the visibility button.
64236448
pub grip_click_target: ClickTarget,
64246449
// 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 {
64276452

64286453
pub enum LayerClickTargetTypes {
64296454
Visibility,
6455+
Lock,
64306456
Grip,
64316457
// Preview,
64326458
}

frontend/src/components/views/Graph.svelte

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,22 @@
234234
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
235235
flush={true}
236236
/>
237+
{#if $nodeGraph.contextMenuInformation.contextMenuData.data.hasSelectedLayers}
238+
{@const allLocked = $nodeGraph.contextMenuInformation.contextMenuData.data.allSelectedLayersLocked}
239+
{@const nodeId = $nodeGraph.contextMenuInformation.contextMenuData.data.nodeId}
240+
<TextButton
241+
label={allLocked ? "Unlock" : "Lock"}
242+
action={() => {
243+
if ($nodeGraph.selected.includes(nodeId)) {
244+
editor.handle.toggleSelectedLocked();
245+
} else {
246+
editor.handle.toggleLayerLock(nodeId);
247+
}
248+
nodeGraph.closeContextMenu();
249+
}}
250+
flush={true}
251+
/>
252+
{/if}
237253
</LayoutCol>
238254
{/if}
239255
</FloatingMenu>
@@ -493,6 +509,7 @@
493509
class:in-selected-network={$nodeGraph.inSelectedNetwork}
494510
class:previewed={node.previewed}
495511
class:disabled={!node.visible}
512+
class:locked={node.locked}
496513
style:--offset-left={node.position?.x || 0}
497514
style:--offset-top={node.position?.y || 0}
498515
style:--clip-path-id={`url(#${clipPathId})`}
@@ -582,6 +599,19 @@
582599
<TextLabel>{node.displayName}</TextLabel>
583600
</div>
584601
<div class="solo-drag-grip" data-tooltip-description="Drag only this layer without pushing others outside the stack"></div>
602+
{#if node.locked}
603+
<IconButton
604+
class="lock"
605+
data-lock-button
606+
size={24}
607+
icon="PadlockLocked"
608+
hoverIcon="PadlockUnlocked"
609+
action={() => {
610+
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
611+
}}
612+
tooltipLabel="Unlock"
613+
/>
614+
{/if}
585615
<IconButton
586616
class="visibility"
587617
data-visibility-button
@@ -1231,6 +1261,10 @@
12311261
border-radius: 2px;
12321262
}
12331263
1264+
&.locked .solo-drag-grip {
1265+
right: calc(-12px + 24px + 24px);
1266+
}
1267+
12341268
.solo-drag-grip:hover,
12351269
&.selected .solo-drag-grip {
12361270
background-image: var(--icon-drag-grip);
@@ -1241,15 +1275,19 @@
12411275
}
12421276
12431277
.visibility {
1244-
position: absolute;
12451278
right: -12px;
12461279
}
12471280
1281+
.lock {
1282+
right: 12px;
1283+
}
1284+
12481285
.input.connectors {
12491286
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
12501287
}
12511288
12521289
.solo-drag-grip,
1290+
.lock,
12531291
.visibility,
12541292
.input.connectors,
12551293
.input.connectors .connector {

frontend/src/messages.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,25 @@ export type FrontendClickTargets = {
178178
readonly modifyImportExport: string[];
179179
};
180180

181+
type ContextMenuDataCreateNode = {
182+
type: "CreateNode";
183+
data: {
184+
compatibleType: string | undefined;
185+
};
186+
};
187+
type ContextMenuDataModifyNode = {
188+
type: "ModifyNode";
189+
data: {
190+
nodeId: bigint;
191+
canBeLayer: boolean;
192+
currentlyIsNode: boolean;
193+
hasSelectedLayers: boolean;
194+
allSelectedLayersLocked: boolean;
195+
};
196+
};
181197
export type ContextMenuInformation = {
182198
contextMenuCoordinates: XY;
183-
contextMenuData: { type: "CreateNode"; data: { compatibleType: string | undefined } } | { type: "ModifyNode"; data: { canBeLayer: boolean; currentlyIsNode: boolean; nodeId: bigint } };
199+
contextMenuData: ContextMenuDataCreateNode | ContextMenuDataModifyNode;
184200
};
185201

186202
export class UpdateContextMenuInformation extends JsMessage {

frontend/wasm/src/editor_api.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,13 @@ impl EditorHandle {
761761
self.dispatch(message);
762762
}
763763

764+
/// Toggle lock state of all selected layers
765+
#[wasm_bindgen(js_name = toggleSelectedLocked)]
766+
pub fn toggle_selected_locked(&self) {
767+
let message = NodeGraphMessage::ToggleSelectedLocked;
768+
self.dispatch(message);
769+
}
770+
764771
/// Creates a new document node in the node graph
765772
#[wasm_bindgen(js_name = createNode)]
766773
pub fn create_node(&self, node_type: JsValue, x: i32, y: i32) {

0 commit comments

Comments
 (0)