Skip to content

Implement duplication of Alt-dragged layers within the Layers panel #2847

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

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub enum DocumentMessage {
MoveSelectedLayersTo {
parent: LayerNodeIdentifier,
insert_index: usize,
as_duplicate: bool,
},
MoveSelectedLayersToGroup {
parent: LayerNodeIdentifier,
Expand Down
84 changes: 72 additions & 12 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,12 +616,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders });
}
}
DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
DocumentMessage::MoveSelectedLayersTo { parent, insert_index, as_duplicate } => {
// Exit early if we have been called with an empty selection.
if !self.selection_network_path.is_empty() {
log::error!("Moving selected layers is only supported for the Document Network");
return;
}

// Disallow trying to insert into self.
if self
.network_interface
Expand All @@ -640,22 +640,54 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
if any_artboards && parent != LayerNodeIdentifier::ROOT_PARENT {
return;
}

// Non-artboards cannot be put at the top level if artboards also exist there
// Non-artboards cannot be put at the top level if artboards also exist there.
let selected_any_non_artboards = self
.network_interface
.selected_nodes()
.selected_layers(self.metadata())
.any(|layer| !self.network_interface.is_artboard(&layer.to_node(), &self.selection_network_path));

let top_level_artboards = LayerNodeIdentifier::ROOT_PARENT
.children(self.metadata())
.any(|layer| self.network_interface.is_artboard(&layer.to_node(), &self.selection_network_path));

if selected_any_non_artboards && parent == LayerNodeIdentifier::ROOT_PARENT && top_level_artboards {
return;
}

if as_duplicate {
let mut all_new_ids = Vec::new();
let selected_layers = self.network_interface.selected_nodes().selected_layers(self.metadata()).collect::<Vec<_>>();

responses.add(DocumentMessage::DeselectAllLayers);
responses.add(DocumentMessage::AddTransaction);

for selected_layer in selected_layers {
let layer_node_id = selected_layer.to_node();

let mut copy_ids = HashMap::new();
copy_ids.insert(layer_node_id, NodeId(0));

self.network_interface
.upstream_flow_back_from_nodes(vec![layer_node_id], &[], network_interface::FlowType::LayerChildrenUpstreamFlow)
.enumerate()
.for_each(|(index, node_id)| {
copy_ids.insert(node_id, NodeId((index + 1) as u64));
});

let nodes: Vec<_> = self.network_interface.copy_nodes(&copy_ids, &[]).collect();
let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
all_new_ids.extend(new_ids.values().cloned());

responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() });
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
}

responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });

return;
}
Comment on lines +656 to +689
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have consolidated your new logic into the existing layer move message. Please see if you can further consolidate the logic to share code paths wherever possible. Because currently this just takes your duplicate-ish implementation and pastes it into here (followed by an early return; to avoid running the subsequent original non-duplication code paths).


let layers_to_move = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path);
// Offset the index for layers to move that are below another layer to move. For example when moving 1 and 2 between 3 and 4, 2 should be inserted at the same index as 1 since 1 is moved first.
let layers_to_move_with_insert_offset = layers_to_move
Expand Down Expand Up @@ -2893,8 +2925,11 @@ impl DocumentMessageHandler {
};

// If moving down, insert below this layer. If moving up, insert above this layer.
let insert_index = if relative_index_offset < 0 { neighbor_index } else { neighbor_index + 1 };
responses.add(DocumentMessage::MoveSelectedLayersTo { parent, insert_index });
responses.add(DocumentMessage::MoveSelectedLayersTo {
parent,
insert_index: if relative_index_offset < 0 { neighbor_index } else { neighbor_index + 1 },
as_duplicate: false,
});
}

pub fn graph_view_overlay_open(&self) -> bool {
Expand Down Expand Up @@ -3260,6 +3295,7 @@ mod document_message_handler_tests {
.handle_message(DocumentMessage::MoveSelectedLayersTo {
parent: child_folder,
insert_index: 0,
as_duplicate: false,
})
.await;

Expand All @@ -3285,15 +3321,27 @@ mod document_message_handler_tests {

// First move rectangle into folder1
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rect_layer.to_node()] }).await;
editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder1, insert_index: 0 }).await;
editor
.handle_message(DocumentMessage::MoveSelectedLayersTo {
parent: folder1,
insert_index: 0,
as_duplicate: false,
})
.await;

// Verifying rectagle is now in folder1
let rect_parent = rect_layer.parent(editor.active_document().metadata()).unwrap();
assert_eq!(rect_parent, folder1, "Rectangle should be inside folder1");

// Moving folder1 into folder2
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![folder1.to_node()] }).await;
editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder2, insert_index: 0 }).await;
editor
.handle_message(DocumentMessage::MoveSelectedLayersTo {
parent: folder2,
insert_index: 0,
as_duplicate: false,
})
.await;

// Verifing hirarchy: folder2 > folder1 > rectangle
let document = editor.active_document();
Expand Down Expand Up @@ -3352,7 +3400,13 @@ mod document_message_handler_tests {

// Moving the rectangle to folder1 to ensure it's inside
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rect_layer.to_node()] }).await;
editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder1, insert_index: 0 }).await;
editor
.handle_message(DocumentMessage::MoveSelectedLayersTo {
parent: folder1,
insert_index: 0,
as_duplicate: false,
})
.await;

editor.handle_message(TransformLayerMessage::BeginGrab).await;
editor.move_mouse(50., 25., ModifierKeys::empty(), MouseKeys::NONE).await;
Expand All @@ -3369,7 +3423,13 @@ mod document_message_handler_tests {
let rect_bbox_before = document.metadata().bounding_box_viewport(rect_layer).unwrap();

// Moving rectangle from folder1 to folder2
editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder2, insert_index: 0 }).await;
editor
.handle_message(DocumentMessage::MoveSelectedLayersTo {
parent: folder2,
insert_index: 0,
as_duplicate: false,
})
.await;

// Rectangle's viewport position after moving
let document = editor.active_document();
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/components/panels/Layers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
let draggingData: undefined | DraggingData = undefined;
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
let dragInPanel = false;
let isDuplicating = false;

// Interactive clipping
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
Expand Down Expand Up @@ -382,8 +383,9 @@

// Set style of cursor for drag
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
event.dataTransfer.effectAllowed = "move";
isDuplicating = event.altKey;
event.dataTransfer.dropEffect = isDuplicating ? "copy" : "move";
event.dataTransfer.effectAllowed = isDuplicating ? "copy" : "move";
Comment on lines +386 to +388
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please get this to update the cursor icon while dragging so the user can add or remove Alt any number of times during a drag and watch as the cursor updates, and the final state when the mouse drag is released becomes whether or not to duplicate. So the user can decide at the time of mouseup, not mousedown, whether to duplicate.

}

if (list) draggingData = calculateDragIndex(list, event.clientY, select);
Expand All @@ -406,11 +408,11 @@
e.preventDefault();

if (e.dataTransfer) {
// Moving layers
// Moving or duplicating layers
if (e.dataTransfer.items.length === 0) {
if (draggable && dragInPanel) {
if (draggable && dragInPanel && insertIndex !== undefined) {
select?.();
editor.handle.moveLayerInTree(insertParentId, insertIndex);
editor.handle.moveSelectedLayersInTree(insertParentId, insertIndex, isDuplicating);
}
}
// Importing files
Expand Down Expand Up @@ -444,6 +446,7 @@
draggingData = undefined;
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
dragInPanel = false;
isDuplicating = false;
}

function rebuildLayerHierarchy(updateDocumentLayerStructure: DocumentLayerStructure) {
Expand Down
12 changes: 5 additions & 7 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,18 +633,16 @@ impl EditorHandle {
self.dispatch(message);
}

/// Move a layer to within a folder and placed down at the given index.
/// Move the selected layers to within a given folder and placed within it at a given index.
/// If the folder is `None`, it is inserted into the document root.
/// If the insert index is `None`, it is inserted at the start of the folder.
#[wasm_bindgen(js_name = moveLayerInTree)]
pub fn move_layer_in_tree(&self, insert_parent_id: Option<u64>, insert_index: Option<usize>) {
#[wasm_bindgen(js_name = moveSelectedLayersInTree)]
pub fn move_selected_layers_in_tree(&self, insert_parent_id: Option<u64>, insert_index: Option<usize>, as_duplicate: bool) {
let insert_parent_id = insert_parent_id.map(NodeId);
let parent = insert_parent_id.map(LayerNodeIdentifier::new_unchecked).unwrap_or_default();
let insert_index = insert_index.unwrap_or_default();

let message = DocumentMessage::MoveSelectedLayersTo {
parent,
insert_index: insert_index.unwrap_or_default(),
};
let message = DocumentMessage::MoveSelectedLayersTo { parent, insert_index, as_duplicate };
self.dispatch(message);
}

Expand Down