diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index 9ba9a46a49..fb3c9b869e 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -174,11 +174,6 @@ pub enum DocumentMessage { insert_index: isize, }, ReorderSelectedLayers(i32), // relative_position, - MoveLayerInTree { - layer: Vec, - insert_above: bool, - neighbor: Vec, - }, SetSnapping(bool), ZoomCanvasToFitAll, } @@ -537,6 +532,16 @@ impl DocumentMessageHandler { Some(layer_panel_entry(layer_metadata, transform, layer, path.to_vec())) } + /// When working with an insert index, deleting the layers may cause the insert index to point to a different location (if the layer being deleted was located before the insert index). + /// + /// This function updates the insert index so that it points to the same place after the specified `layers` are deleted. + fn update_insert_index<'a>(&self, layers: &[&'a [LayerId]], path: &[LayerId], insert_index: isize) -> Result { + let folder = self.graphene_document.folder(path)?; + let layer_ids_above = if insert_index < 0 { &folder.layer_ids } else { &folder.layer_ids[..(insert_index as usize)] }; + + Ok(insert_index - layer_ids_above.iter().filter(|layer_id| layers.iter().any(|x| *x == [path, &[**layer_id]].concat())).count() as isize) + } + pub fn document_bounds(&self) -> Option<[DVec2; 2]> { if self.artboard_message_handler.is_infinite_canvas() { self.graphene_document.viewport_bounding_box(&[]).ok().flatten() @@ -842,10 +847,14 @@ impl MessageHandler for DocumentMessageHand } DocumentResponse::LayerChanged { path } => responses.push_back(LayerChanged(path.clone()).into()), DocumentResponse::CreatedLayer { path } => { + if self.layer_metadata.contains_key(path) { + log::warn!("CreatedLayer overrides existing layer metadata."); + } self.layer_metadata.insert(path.clone(), LayerMetadata::new(false)); + responses.push_back(LayerChanged(path.clone()).into()); self.layer_range_selection_reference = path.clone(); - responses.push_back(SetSelectedLayers(vec![path.clone()]).into()); + responses.push_back(AddSelectedLayers(vec![path.clone()]).into()); } DocumentResponse::DocumentChanged => responses.push_back(RenderDocument.into()), }; @@ -923,6 +932,13 @@ impl MessageHandler for DocumentMessageHand responses.push_back(ToolMessage::DocumentIsDirty.into()); } MoveSelectedLayersTo { path, insert_index } => { + let layers = self.selected_layers().collect::>(); + + // Trying to insert into self. + if layers.iter().any(|layer| path.starts_with(layer)) { + return; + } + let insert_index = self.update_insert_index(&layers, &path, insert_index).unwrap(); responses.push_back(DocumentsMessage::Copy(Clipboard::System).into()); responses.push_back(DocumentMessage::DeleteSelectedLayers.into()); responses.push_back( @@ -954,15 +970,11 @@ impl MessageHandler for DocumentMessageHand if let Some(insert_path) = insert { let (id, path) = insert_path.split_last().expect("Can't move the root folder"); if let Some(folder) = self.graphene_document.layer(path).ok().and_then(|layer| layer.as_folder().ok()) { - let selected: Vec<_> = selected_layers - .iter() - .filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1) - .map(|x| x.last().unwrap()) - .collect(); - let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect(); - let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 }; - let fallback = offset * (non_selected.len()); - let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize; + let layer_index = folder.layer_ids.iter().position(|comparison_id| comparison_id == id).unwrap() as isize; + + // If moving down, insert below this layer, if moving up, insert above this layer + let insert_index = if relative_position < 0 { layer_index } else { layer_index + 1 }; + responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into()); } } @@ -1029,42 +1041,6 @@ impl MessageHandler for DocumentMessageHand } } RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()), - MoveLayerInTree { - layer: target_layer, - insert_above, - neighbor, - } => { - let neighbor_id = neighbor.last().expect("Tried to move next to root"); - let neighbor_path = &neighbor[..neighbor.len() - 1]; - - if !neighbor.starts_with(&target_layer) { - let containing_folder = self.graphene_document.folder(neighbor_path).expect("Neighbor does not exist"); - let neighbor_index = containing_folder.position_of_layer(*neighbor_id).expect("Neighbor layer does not exist"); - - let layer = self.graphene_document.layer(&target_layer).expect("Layer moving does not exist.").to_owned(); - let destination_path = [neighbor_path.to_vec(), vec![generate_uuid()]].concat(); - let insert_index = if insert_above { neighbor_index } else { neighbor_index + 1 } as isize; - - responses.push_back(DocumentMessage::StartTransaction.into()); - responses.push_back( - DocumentOperation::InsertLayer { - layer, - destination_path: destination_path.clone(), - insert_index, - } - .into(), - ); - responses.push_back( - DocumentMessage::UpdateLayerMetadata { - layer_path: destination_path, - layer_metadata: *self.layer_metadata(&target_layer), - } - .into(), - ); - responses.push_back(DocumentOperation::DeleteLayer { path: target_layer }.into()); - responses.push_back(DocumentMessage::CommitTransaction.into()); - } - } SetSnapping(new_status) => { self.snapping_enabled = new_status; } @@ -1094,7 +1070,6 @@ impl MessageHandler for DocumentMessageHand SaveDocument, SetSnapping, DebugPrintDocument, - MoveLayerInTree, ZoomCanvasToFitAll, ); diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 4b409d525f..7a29cb745e 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -379,6 +379,7 @@ impl MessageHandler for DocumentsMessageHa .graphene_document .shallowest_common_folder(document.selected_layers()) .expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion"); + responses.push_back(DeselectAllLayers.into()); responses.push_back(StartTransaction.into()); responses.push_back( PasteIntoFolder { diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index c9aa7ca1a1..98167042ae 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -29,8 +29,8 @@ - -
+ +
, - layers: [] as LayerPanelEntry[], + layers: [] as { folderIndex: number; entry: LayerPanelEntry }[], layerDepths: [] as number[], selectionRangeStartLayer: undefined as undefined | LayerPanelEntry, selectionRangeEndLayer: undefined as undefined | LayerPanelEntry, opacity: 100, - draggingData: undefined as undefined | { path: BigUint64Array; above: boolean; nearestPath: BigUint64Array; insertLine: HTMLDivElement }, + draggingData: undefined as undefined | { insertFolder: BigUint64Array; insertIndex: number; insertLine: HTMLDivElement }, }; }, methods: { @@ -343,32 +343,32 @@ export default defineComponent({ }, async clearSelection() { this.layers.forEach((layer) => { - layer.layer_metadata.selected = false; + layer.entry.layer_metadata.selected = false; }); }, - closest(tree: HTMLElement, clientY: number): [BigUint64Array, boolean, Node] { + closest(tree: HTMLElement, clientY: number): { insertFolder: BigUint64Array; insertIndex: number; insertAboveNode: Node } { const treeChildren = tree.children; // Closest distance to the middle of the row along the Y axis let closest = Infinity; // The nearest row parent (element of the tree) - let nearestElement = tree.lastChild as Node; + let insertAboveNode = tree.lastChild as Node; - // The nearest element in the path to the mouse - let nearestPath = new BigUint64Array(); + // Folder to insert into + let insertFolder = new BigUint64Array(); - // Item goes above or below the mouse - let above = false; + // Insert index + let insertIndex = -1; Array.from(treeChildren).forEach((treeChild) => { - if (treeChild.childElementCount <= 2) return; - - const child = treeChild.children[2] as HTMLElement; + const layerComponents = treeChild.getElementsByClassName("layer"); + if (layerComponents.length !== 1) return; + const child = layerComponents[0]; const indexAttribute = child.getAttribute("data-index"); if (!indexAttribute) return; - const layer = this.layers[parseInt(indexAttribute, 10)]; + const { folderIndex, entry: layer } = this.layers[parseInt(indexAttribute, 10)]; const rect = child.getBoundingClientRect(); const position = rect.top + rect.height / 2; @@ -376,30 +376,31 @@ export default defineComponent({ // Inserting above current row if (distance > 0 && distance < closest) { + insertAboveNode = treeChild; + insertFolder = layer.path.slice(0, layer.path.length - 1); + insertIndex = folderIndex; closest = distance; - nearestPath = layer.path; - above = true; - if (child.parentNode) { - nearestElement = child.parentNode; - } } // Inserting below current row - else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0 && layer.layer_type !== "Folder") { - closest = -distance; - nearestPath = layer.path; + else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) { if (child.parentNode && child.parentNode.nextSibling) { - nearestElement = child.parentNode.nextSibling; + insertAboveNode = child.parentNode.nextSibling; } + insertFolder = layer.layer_type === "Folder" ? layer.path : layer.path.slice(0, layer.path.length - 1); + insertIndex = layer.layer_type === "Folder" ? 0 : folderIndex + 1; + closest = -distance; } // Inserting with no nesting at the end of the panel - else if (closest === Infinity) { - nearestPath = layer.path.slice(0, 1); + else if (closest === Infinity && layer.path.length === 1) { + insertIndex = folderIndex + 1; } }); - return [nearestPath, above, nearestElement]; + return { insertFolder, insertIndex, insertAboveNode }; }, async dragStart(event: DragEvent, layer: LayerPanelEntry) { + if (!layer.layer_metadata.selected) this.selectLayer(layer, event.ctrlKey, event.shiftKey); + // Set style of cursor for drag if (event.dataTransfer) { event.dataTransfer.dropEffect = "move"; @@ -413,31 +414,30 @@ export default defineComponent({ insertLine.classList.add("insert-mark"); tree.appendChild(insertLine); - const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY); + const { insertFolder, insertIndex, insertAboveNode } = this.closest(tree, event.clientY); // Set the initial state of the insert line - if (nearestElement.parentNode) { - insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`; // TODO: use layerIndent function to calculate this - tree.insertBefore(insertLine, nearestElement); + if (insertAboveNode.parentNode) { + insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * (insertFolder.length + 1)}px`; // TODO: use layerIndent function to calculate this + tree.insertBefore(insertLine, insertAboveNode); } - this.draggingData = { path: layer.path, above, nearestPath, insertLine }; + this.draggingData = { insertFolder, insertIndex, insertLine }; }, updateInsertLine(event: DragEvent) { // Stop the drag from being shown as cancelled event.preventDefault(); const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement; - - const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY); + const { insertFolder, insertIndex, insertAboveNode } = this.closest(tree, event.clientY); if (this.draggingData) { - this.draggingData.nearestPath = nearestPath; - this.draggingData.above = above; + this.draggingData.insertFolder = insertFolder; + this.draggingData.insertIndex = insertIndex; - if (nearestElement.parentNode) { - this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`; - tree.insertBefore(this.draggingData.insertLine, nearestElement); + if (insertAboveNode.parentNode) { + this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * (insertFolder.length + 1)}px`; + tree.insertBefore(this.draggingData.insertLine, insertAboveNode); } } }, @@ -448,12 +448,15 @@ export default defineComponent({ }, async drop() { this.removeLine(); + if (this.draggingData) { - this.editor.instance.move_layer_in_tree(this.draggingData.path, this.draggingData.above, this.draggingData.nearestPath); + const { insertFolder, insertIndex } = this.draggingData; + + this.editor.instance.move_layer_in_tree(insertFolder, insertIndex); } }, setBlendModeForSelectedLayers() { - const selected = this.layers.filter((layer) => layer.layer_metadata.selected); + const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected); if (selected.length < 1) { this.blendModeSelectedIndex = 0; @@ -462,8 +465,8 @@ export default defineComponent({ } this.blendModeDropdownDisabled = false; - const firstEncounteredBlendMode = selected[0].blend_mode; - const allBlendModesAlike = !selected.find((layer) => layer.blend_mode !== firstEncounteredBlendMode); + const firstEncounteredBlendMode = selected[0].entry.blend_mode; + const allBlendModesAlike = !selected.find((layer) => layer.entry.blend_mode !== firstEncounteredBlendMode); if (allBlendModesAlike) { this.blendModeSelectedIndex = this.blendModeEntries.flat().findIndex((entry) => entry.value === firstEncounteredBlendMode); @@ -474,7 +477,7 @@ export default defineComponent({ }, setOpacityForSelectedLayers() { // todo figure out why this is here - const selected = this.layers.filter((layer) => layer.layer_metadata.selected); + const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected); if (selected.length < 1) { this.opacity = 100; @@ -483,8 +486,8 @@ export default defineComponent({ } this.opacityNumberInputDisabled = false; - const firstEncounteredOpacity = selected[0].opacity; - const allOpacitiesAlike = !selected.find((layer) => layer.opacity !== firstEncounteredOpacity); + const firstEncounteredOpacity = selected[0].entry.opacity; + const allOpacitiesAlike = !selected.find((layer) => layer.entry.opacity !== firstEncounteredOpacity); if (allOpacitiesAlike) { this.opacity = firstEncounteredOpacity; @@ -497,14 +500,14 @@ export default defineComponent({ mounted() { this.editor.dispatcher.subscribeJsMessage(DisplayFolderTreeStructure, (displayFolderTreeStructure) => { const path = [] as bigint[]; - this.layers = [] as LayerPanelEntry[]; + this.layers = [] as { folderIndex: number; entry: LayerPanelEntry }[]; - const recurse = (folder: DisplayFolderTreeStructure, layers: LayerPanelEntry[], cache: Map): void => { - folder.children.forEach((item) => { + const recurse = (folder: DisplayFolderTreeStructure, layers: { folderIndex: number; entry: LayerPanelEntry }[], cache: Map): void => { + folder.children.forEach((item, index) => { // TODO: fix toString path.push(BigInt(item.layerId.toString())); const mapping = cache.get(path.toString()); - if (mapping) layers.push(mapping); + if (mapping) layers.push({ folderIndex: index, entry: mapping }); if (item.children.length >= 1) recurse(item, layers, cache); path.pop(); }); diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index 8f39f5aa0a..14426984db 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -383,8 +383,8 @@ impl JsEditorHandle { } /// Move a layer to be next to the specified neighbor - pub fn move_layer_in_tree(&self, layer: Vec, insert_above: bool, neighbor: Vec) { - let message = DocumentMessage::MoveLayerInTree { layer, insert_above, neighbor }; + pub fn move_layer_in_tree(&self, path: Vec, insert_index: isize) { + let message = DocumentMessage::MoveSelectedLayersTo { path, insert_index }; self.dispatch(message); }