Skip to content
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