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:
parent
54a02dedd5
commit
8a75c0c1e1
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,22 @@
|
|||
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
|
||||
flush={true}
|
||||
/>
|
||||
{#if $nodeGraph.contextMenuInformation.contextMenuData.data.hasSelectedLayers}
|
||||
{@const allLocked = $nodeGraph.contextMenuInformation.contextMenuData.data.allSelectedLayersLocked}
|
||||
{@const nodeId = $nodeGraph.contextMenuInformation.contextMenuData.data.nodeId}
|
||||
<TextButton
|
||||
label={allLocked ? "Unlock" : "Lock"}
|
||||
action={() => {
|
||||
if ($nodeGraph.selected.includes(nodeId)) {
|
||||
editor.handle.toggleSelectedLocked();
|
||||
} else {
|
||||
editor.handle.toggleLayerLock(nodeId);
|
||||
}
|
||||
nodeGraph.closeContextMenu();
|
||||
}}
|
||||
flush={true}
|
||||
/>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
</FloatingMenu>
|
||||
|
|
@ -493,6 +509,7 @@
|
|||
class:in-selected-network={$nodeGraph.inSelectedNetwork}
|
||||
class:previewed={node.previewed}
|
||||
class:disabled={!node.visible}
|
||||
class:locked={node.locked}
|
||||
style:--offset-left={node.position?.x || 0}
|
||||
style:--offset-top={node.position?.y || 0}
|
||||
style:--clip-path-id={`url(#${clipPathId})`}
|
||||
|
|
@ -582,6 +599,19 @@
|
|||
<TextLabel>{node.displayName}</TextLabel>
|
||||
</div>
|
||||
<div class="solo-drag-grip" data-tooltip-description="Drag only this layer without pushing others outside the stack"></div>
|
||||
{#if node.locked}
|
||||
<IconButton
|
||||
class="lock"
|
||||
data-lock-button
|
||||
size={24}
|
||||
icon="PadlockLocked"
|
||||
hoverIcon="PadlockUnlocked"
|
||||
action={() => {
|
||||
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
|
||||
}}
|
||||
tooltipLabel="Unlock"
|
||||
/>
|
||||
{/if}
|
||||
<IconButton
|
||||
class="visibility"
|
||||
data-visibility-button
|
||||
|
|
@ -1231,6 +1261,10 @@
|
|||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.locked .solo-drag-grip {
|
||||
right: calc(-12px + 24px + 24px);
|
||||
}
|
||||
|
||||
.solo-drag-grip:hover,
|
||||
&.selected .solo-drag-grip {
|
||||
background-image: var(--icon-drag-grip);
|
||||
|
|
@ -1241,15 +1275,19 @@
|
|||
}
|
||||
|
||||
.visibility {
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
.lock {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.input.connectors {
|
||||
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
|
||||
}
|
||||
|
||||
.solo-drag-grip,
|
||||
.lock,
|
||||
.visibility,
|
||||
.input.connectors,
|
||||
.input.connectors .connector {
|
||||
|
|
|
|||
|
|
@ -178,9 +178,25 @@ export type FrontendClickTargets = {
|
|||
readonly modifyImportExport: string[];
|
||||
};
|
||||
|
||||
type ContextMenuDataCreateNode = {
|
||||
type: "CreateNode";
|
||||
data: {
|
||||
compatibleType: string | undefined;
|
||||
};
|
||||
};
|
||||
type ContextMenuDataModifyNode = {
|
||||
type: "ModifyNode";
|
||||
data: {
|
||||
nodeId: bigint;
|
||||
canBeLayer: boolean;
|
||||
currentlyIsNode: boolean;
|
||||
hasSelectedLayers: boolean;
|
||||
allSelectedLayersLocked: boolean;
|
||||
};
|
||||
};
|
||||
export type ContextMenuInformation = {
|
||||
contextMenuCoordinates: XY;
|
||||
contextMenuData: { type: "CreateNode"; data: { compatibleType: string | undefined } } | { type: "ModifyNode"; data: { canBeLayer: boolean; currentlyIsNode: boolean; nodeId: bigint } };
|
||||
contextMenuData: ContextMenuDataCreateNode | ContextMenuDataModifyNode;
|
||||
};
|
||||
|
||||
export class UpdateContextMenuInformation extends JsMessage {
|
||||
|
|
|
|||
|
|
@ -761,6 +761,13 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Toggle lock state of all selected layers
|
||||
#[wasm_bindgen(js_name = toggleSelectedLocked)]
|
||||
pub fn toggle_selected_locked(&self) {
|
||||
let message = NodeGraphMessage::ToggleSelectedLocked;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Creates a new document node in the node graph
|
||||
#[wasm_bindgen(js_name = createNode)]
|
||||
pub fn create_node(&self, node_type: JsValue, x: i32, y: i32) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue