Skip to content

Migrate to using MoveSelectedLayersTo #469

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

Merged
merged 10 commits into from
Jan 12, 2022
79 changes: 27 additions & 52 deletions editor/src/document/document_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,6 @@ pub enum DocumentMessage {
insert_index: isize,
},
ReorderSelectedLayers(i32), // relative_position,
MoveLayerInTree {
layer: Vec<LayerId>,
insert_above: bool,
neighbor: Vec<LayerId>,
},
SetSnapping(bool),
ZoomCanvasToFitAll,
}
Expand Down Expand Up @@ -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<isize, DocumentError> {
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()
Expand Down Expand Up @@ -842,10 +847,14 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> 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()),
};
Expand Down Expand Up @@ -923,6 +932,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
MoveSelectedLayersTo { path, insert_index } => {
let layers = self.selected_layers().collect::<Vec<_>>();

// 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(
Expand Down Expand Up @@ -954,15 +970,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> 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());
}
}
Expand Down Expand Up @@ -1029,42 +1041,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> 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;
}
Expand Down Expand Up @@ -1094,7 +1070,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
SaveDocument,
SetSnapping,
DebugPrintDocument,
MoveLayerInTree,
ZoomCanvasToFitAll,
);

Expand Down
1 change: 1 addition & 0 deletions editor/src/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> 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 {
Expand Down
103 changes: 53 additions & 50 deletions frontend/src/components/panels/LayerTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
</PopoverButton>
</LayoutRow>
<LayoutRow :class="'layer-tree scrollable-y'">
<LayoutCol :class="'list'" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="updateInsertLine($event)" @dragend="drop()">
<div class="layer-row" v-for="(layer, index) in layers" :key="String(layer.path.slice(-1))">
<LayoutCol :class="'list'" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="updateInsertLine($event)" @dragend="drop($event)">
<div class="layer-row" v-for="({ entry: layer }, index) in layers" :key="String(layer.path.slice(-1))">
<div class="visibility">
<IconButton
:action="(e) => (toggleLayerVisibility(layer.path), e && e.stopPropagation())"
Expand Down Expand Up @@ -307,12 +307,12 @@ export default defineComponent({
opacityNumberInputDisabled: true,
// TODO: replace with BigUint64Array as index
layerCache: new Map() as Map<string, LayerPanelEntry>,
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: {
Expand Down Expand Up @@ -343,63 +343,64 @@ 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;
const distance = position - clientY;

// 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";
Expand All @@ -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);
}
}
},
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<string, LayerPanelEntry>): void => {
folder.children.forEach((item) => {
const recurse = (folder: DisplayFolderTreeStructure, layers: { folderIndex: number; entry: LayerPanelEntry }[], cache: Map<string, LayerPanelEntry>): 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();
});
Expand Down
4 changes: 2 additions & 2 deletions frontend/wasm/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LayerId>, insert_above: bool, neighbor: Vec<LayerId>) {
let message = DocumentMessage::MoveLayerInTree { layer, insert_above, neighbor };
pub fn move_layer_in_tree(&self, path: Vec<LayerId>, insert_index: isize) {
let message = DocumentMessage::MoveSelectedLayersTo { path, insert_index };
self.dispatch(message);
}

Expand Down