Drag to rearrange layers in the layer panel (#434)
* Add insert line * Implement dragging * Improve CSS and variable naming consistency * Resolved folder crash, added Undo/Redo support * Removed marker TODO leftover * JS cleanup * WIP preserving expanded state (via LayerData) when copy/pasting and moving layers in layer panel * Finish making folders copy/paste preserving expanded state, but not recursively yet * Add cut, copy, and paste to the Edit menu (#440) * Add cut * Hook up edit dropdown * Use copy message Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: otdavies <oliver@psyfer.io>
This commit is contained in:
parent
451c9fcd46
commit
3de426b7cc
|
|
@ -1,6 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use super::document_message_handler::CopyBufferEntry;
|
||||||
pub use super::layer_panel::*;
|
pub use super::layer_panel::*;
|
||||||
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
||||||
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
|
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
|
||||||
|
|
@ -105,6 +106,10 @@ pub enum DocumentMessage {
|
||||||
#[child]
|
#[child]
|
||||||
TransformLayers(TransformLayerMessage),
|
TransformLayers(TransformLayerMessage),
|
||||||
DispatchOperation(Box<DocumentOperation>),
|
DispatchOperation(Box<DocumentOperation>),
|
||||||
|
UpdateLayerData {
|
||||||
|
path: Vec<LayerId>,
|
||||||
|
layer_data_entry: LayerData,
|
||||||
|
},
|
||||||
SetSelectedLayers(Vec<Vec<LayerId>>),
|
SetSelectedLayers(Vec<Vec<LayerId>>),
|
||||||
AddSelectedLayers(Vec<Vec<LayerId>>),
|
AddSelectedLayers(Vec<Vec<LayerId>>),
|
||||||
SelectAllLayers,
|
SelectAllLayers,
|
||||||
|
|
@ -148,6 +153,11 @@ pub enum DocumentMessage {
|
||||||
insert_index: isize,
|
insert_index: isize,
|
||||||
},
|
},
|
||||||
ReorderSelectedLayers(i32), // relative_position,
|
ReorderSelectedLayers(i32), // relative_position,
|
||||||
|
MoveLayerInTree {
|
||||||
|
layer: Vec<LayerId>,
|
||||||
|
insert_above: bool,
|
||||||
|
neighbor: Vec<LayerId>,
|
||||||
|
},
|
||||||
SetSnapping(bool),
|
SetSnapping(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -653,6 +663,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UpdateLayerData { path, layer_data_entry } => {
|
||||||
|
self.layer_data.insert(path, layer_data_entry);
|
||||||
|
}
|
||||||
SetSelectedLayers(paths) => {
|
SetSelectedLayers(paths) => {
|
||||||
self.layer_data.iter_mut().filter(|(_, layer_data)| layer_data.selected).for_each(|(path, layer_data)| {
|
self.layer_data.iter_mut().filter(|(_, layer_data)| layer_data.selected).for_each(|(path, layer_data)| {
|
||||||
layer_data.selected = false;
|
layer_data.selected = false;
|
||||||
|
|
@ -911,6 +924,42 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()),
|
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::UpdateLayerData {
|
||||||
|
path: destination_path,
|
||||||
|
layer_data_entry: *self.layer_data(&target_layer),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
responses.push_back(DocumentOperation::DeleteLayer { path: target_layer }.into());
|
||||||
|
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
SetSnapping(new_status) => {
|
SetSnapping(new_status) => {
|
||||||
self.snapping_enabled = new_status;
|
self.snapping_enabled = new_status;
|
||||||
}
|
}
|
||||||
|
|
@ -927,6 +976,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
ExportDocument,
|
ExportDocument,
|
||||||
SaveDocument,
|
SaveDocument,
|
||||||
SetSnapping,
|
SetSnapping,
|
||||||
|
MoveLayerInTree,
|
||||||
);
|
);
|
||||||
|
|
||||||
if self.layer_data.values().any(|data| data.selected) {
|
if self.layer_data.values().any(|data| data.selected) {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
|
use super::{DocumentMessageHandler, LayerData};
|
||||||
|
use crate::consts::DEFAULT_DOCUMENT_NAME;
|
||||||
use crate::frontend::frontend_message_handler::FrontendDocumentDetails;
|
use crate::frontend::frontend_message_handler::FrontendDocumentDetails;
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::InputPreprocessor;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
use graphene::layers::Layer;
|
use graphene::layers::Layer;
|
||||||
use graphene::{LayerId, Operation as DocumentOperation};
|
use graphene::{LayerId, Operation as DocumentOperation};
|
||||||
|
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, VecDeque};
|
|
||||||
|
|
||||||
use super::DocumentMessageHandler;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use crate::consts::DEFAULT_DOCUMENT_NAME;
|
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
|
@ -17,7 +18,8 @@ pub enum Clipboard {
|
||||||
User,
|
User,
|
||||||
_ClipboardCount,
|
_ClipboardCount,
|
||||||
}
|
}
|
||||||
static CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8;
|
|
||||||
|
const CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8;
|
||||||
|
|
||||||
#[impl_message(Message, Documents)]
|
#[impl_message(Message, Documents)]
|
||||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -60,7 +62,13 @@ pub struct DocumentsMessageHandler {
|
||||||
documents: HashMap<u64, DocumentMessageHandler>,
|
documents: HashMap<u64, DocumentMessageHandler>,
|
||||||
document_ids: Vec<u64>,
|
document_ids: Vec<u64>,
|
||||||
active_document_id: u64,
|
active_document_id: u64,
|
||||||
copy_buffer: Vec<Vec<Layer>>,
|
copy_buffer: [Vec<CopyBufferEntry>; CLIPBOARD_COUNT as usize],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct CopyBufferEntry {
|
||||||
|
layer: Layer,
|
||||||
|
layer_data: LayerData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentsMessageHandler {
|
impl DocumentsMessageHandler {
|
||||||
|
|
@ -156,10 +164,12 @@ impl Default for DocumentsMessageHandler {
|
||||||
let starting_key = generate_uuid();
|
let starting_key = generate_uuid();
|
||||||
documents_map.insert(starting_key, DocumentMessageHandler::default());
|
documents_map.insert(starting_key, DocumentMessageHandler::default());
|
||||||
|
|
||||||
|
const EMPTY_VEC: Vec<CopyBufferEntry> = vec![];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
documents: documents_map,
|
documents: documents_map,
|
||||||
document_ids: vec![starting_key],
|
document_ids: vec![starting_key],
|
||||||
copy_buffer: vec![vec![]; CLIPBOARD_COUNT as usize],
|
copy_buffer: [EMPTY_VEC; CLIPBOARD_COUNT as usize],
|
||||||
active_document_id: starting_key,
|
active_document_id: starting_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -345,11 +355,12 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
||||||
let paths = self.active_document().selected_layers_sorted();
|
let paths = self.active_document().selected_layers_sorted();
|
||||||
self.copy_buffer[clipboard as usize].clear();
|
self.copy_buffer[clipboard as usize].clear();
|
||||||
for path in paths {
|
for path in paths {
|
||||||
match self.active_document().graphene_document.layer(&path).map(|t| t.clone()) {
|
let document = self.active_document();
|
||||||
Ok(layer) => {
|
match (document.graphene_document.layer(&path).map(|t| t.clone()), document.layer_data(&path).clone()) {
|
||||||
self.copy_buffer[clipboard as usize].push(layer);
|
(Ok(layer), layer_data) => {
|
||||||
|
self.copy_buffer[clipboard as usize].push(CopyBufferEntry { layer, layer_data });
|
||||||
}
|
}
|
||||||
Err(e) => warn!("Could not access selected layer {:?}: {:?}", path, e),
|
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", path, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -374,24 +385,35 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
PasteIntoFolder { clipboard, path, insert_index } => {
|
PasteIntoFolder { clipboard, path, insert_index } => {
|
||||||
let paste = |layer: &Layer, responses: &mut VecDeque<_>| {
|
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>| {
|
||||||
log::trace!("Pasting into folder {:?} as index: {}", path, insert_index);
|
log::trace!("Pasting into folder {:?} as index: {}", &path, insert_index);
|
||||||
|
|
||||||
|
let destination_path = [path.to_vec(), vec![generate_uuid()]].concat();
|
||||||
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
DocumentOperation::PasteLayer {
|
DocumentOperation::InsertLayer {
|
||||||
layer: layer.clone(),
|
layer: entry.layer.clone(),
|
||||||
path: path.clone(),
|
destination_path: destination_path.clone(),
|
||||||
insert_index,
|
insert_index,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
)
|
);
|
||||||
|
responses.push_back(
|
||||||
|
DocumentMessage::UpdateLayerData {
|
||||||
|
path: destination_path,
|
||||||
|
layer_data_entry: entry.layer_data,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if insert_index == -1 {
|
if insert_index == -1 {
|
||||||
for layer in self.copy_buffer[clipboard as usize].iter() {
|
for entry in self.copy_buffer[clipboard as usize].iter() {
|
||||||
paste(layer, responses)
|
paste(entry, responses)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for layer in self.copy_buffer[clipboard as usize].iter().rev() {
|
for entry in self.copy_buffer[clipboard as usize].iter().rev() {
|
||||||
paste(layer, responses)
|
paste(entry, responses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,9 @@ impl Fsm for FillToolFsmState {
|
||||||
RightMouseDown => tool_data.secondary_color,
|
RightMouseDown => tool_data.secondary_color,
|
||||||
Abort => unreachable!(),
|
Abort => unreachable!(),
|
||||||
};
|
};
|
||||||
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
responses.push_back(Operation::SetLayerFill { path: path.to_vec(), color }.into());
|
responses.push_back(Operation::SetLayerFill { path: path.to_vec(), color }.into());
|
||||||
|
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow :class="'layer-tree scrollable-y'">
|
<LayoutRow :class="'layer-tree scrollable-y'">
|
||||||
<LayoutCol :class="'list'" @click="deselectAllLayers">
|
<LayoutCol :class="'list'" ref="layerTreeList" @click="deselectAllLayers" @dragover="updateLine($event)" @dragend="drop()">
|
||||||
<div class="layer-row" v-for="layer in layers" :key="layer.path">
|
<div class="layer-row" v-for="(layer, index) in layers" :key="layer.path">
|
||||||
<div class="layer-visibility">
|
<div class="layer-visibility">
|
||||||
<IconButton
|
<IconButton
|
||||||
:action="(e) => (toggleLayerVisibility(layer.path), e.stopPropagation())"
|
:action="(e) => (toggleLayerVisibility(layer.path), e.stopPropagation())"
|
||||||
|
|
@ -40,6 +40,9 @@
|
||||||
@click.shift.ctrl.exact.stop="selectLayer(layer, true, true)"
|
@click.shift.ctrl.exact.stop="selectLayer(layer, true, true)"
|
||||||
@click.ctrl.exact.stop="selectLayer(layer, true, false)"
|
@click.ctrl.exact.stop="selectLayer(layer, true, false)"
|
||||||
@click.exact.stop="selectLayer(layer, false, false)"
|
@click.exact.stop="selectLayer(layer, false, false)"
|
||||||
|
:data-index="index"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="dragStart($event, layer)"
|
||||||
>
|
>
|
||||||
<div class="layer-thumbnail" v-html="layer.thumbnail"></div>
|
<div class="layer-thumbnail" v-html="layer.thumbnail"></div>
|
||||||
<div class="layer-type-icon">
|
<div class="layer-type-icon">
|
||||||
|
|
@ -85,7 +88,8 @@
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
& + .layer-row {
|
& + .layer-row,
|
||||||
|
& + .insert-mark + .layer-row {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,6 +194,33 @@
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insert-mark {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 16px;
|
||||||
|
height: 0;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background: var(--color-accent-hover);
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:first-child, :last-child) {
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child::after {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child::after {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -250,6 +281,10 @@ const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 40;
|
||||||
|
const LAYER_LEFT_MARGIN_OFFSET = 28;
|
||||||
|
const LAYER_LEFT_INDENT_OFFSET = 16;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
inject: ["editor"],
|
inject: ["editor"],
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -265,6 +300,7 @@ export default defineComponent({
|
||||||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
||||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
|
draggingData: undefined as undefined | { path: BigUint64Array; above: boolean; nearestPath: BigUint64Array; insertLine: HTMLDivElement },
|
||||||
MenuDirection,
|
MenuDirection,
|
||||||
SeparatorType,
|
SeparatorType,
|
||||||
LayerTypeOptions,
|
LayerTypeOptions,
|
||||||
|
|
@ -303,6 +339,112 @@ export default defineComponent({
|
||||||
layer.layer_data.selected = false;
|
layer.layer_data.selected = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
closest(tree: HTMLElement, clientY: number): [BigUint64Array, boolean, 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;
|
||||||
|
|
||||||
|
// The nearest element in the path to the mouse
|
||||||
|
let nearestPath = new BigUint64Array();
|
||||||
|
|
||||||
|
// Item goes above or below the mouse
|
||||||
|
let above = false;
|
||||||
|
|
||||||
|
Array.from(treeChildren).forEach((treeChild) => {
|
||||||
|
if (treeChild.childElementCount <= 2) return;
|
||||||
|
|
||||||
|
const child = treeChild.children[2] as HTMLElement;
|
||||||
|
|
||||||
|
const indexAttribute = child.getAttribute("data-index");
|
||||||
|
if (!indexAttribute) return;
|
||||||
|
const 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) {
|
||||||
|
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 !== LayerTypeOptions.Folder) {
|
||||||
|
closest = -distance;
|
||||||
|
nearestPath = layer.path;
|
||||||
|
if (child.parentNode && child.parentNode.nextSibling) {
|
||||||
|
nearestElement = child.parentNode.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Inserting with no nesting at the end of the panel
|
||||||
|
else if (closest === Infinity) {
|
||||||
|
nearestPath = layer.path.slice(0, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [nearestPath, above, nearestElement];
|
||||||
|
},
|
||||||
|
async dragStart(event: DragEvent, layer: LayerPanelEntry) {
|
||||||
|
// Set style of cursor for drag
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el;
|
||||||
|
|
||||||
|
// Create the insert line
|
||||||
|
const insertLine = document.createElement("div") as HTMLDivElement;
|
||||||
|
insertLine.classList.add("insert-mark");
|
||||||
|
tree.appendChild(insertLine);
|
||||||
|
|
||||||
|
const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY);
|
||||||
|
|
||||||
|
// Set the initial state of the line
|
||||||
|
if (nearestElement.parentNode) {
|
||||||
|
insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`;
|
||||||
|
tree.insertBefore(insertLine, nearestElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.draggingData = { path: layer.path, above, nearestPath, insertLine };
|
||||||
|
},
|
||||||
|
updateLine(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);
|
||||||
|
|
||||||
|
if (this.draggingData) {
|
||||||
|
this.draggingData.nearestPath = nearestPath;
|
||||||
|
this.draggingData.above = above;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeLine() {
|
||||||
|
if (this.draggingData) {
|
||||||
|
this.draggingData.insertLine.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async drop() {
|
||||||
|
this.removeLine();
|
||||||
|
if (this.draggingData) {
|
||||||
|
this.editor.instance.move_layer_in_tree(this.draggingData.path, this.draggingData.above, this.draggingData.nearestPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
setBlendModeForSelectedLayers() {
|
setBlendModeForSelectedLayers() {
|
||||||
const selected = this.layers.filter((layer) => layer.layer_data.selected);
|
const selected = this.layers.filter((layer) => layer.layer_data.selected);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,8 @@ export enum MenuType {
|
||||||
Dialog = "Dialog",
|
Dialog = "Dialog",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const POINTER_STRAY_DISTANCE = 100;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -313,7 +315,6 @@ export default defineComponent({
|
||||||
floatingMenuContent.style.minWidth = minWidth;
|
floatingMenuContent.style.minWidth = minWidth;
|
||||||
},
|
},
|
||||||
pointerMoveHandler(e: PointerEvent) {
|
pointerMoveHandler(e: PointerEvent) {
|
||||||
const POINTER_STRAY_DISTANCE = 100;
|
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const pointerOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement);
|
const pointerOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement);
|
||||||
const pointerOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement);
|
const pointerOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement);
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,12 @@ impl JsEditorHandle {
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the blend mode for the selected layers
|
/// Set the blend mode for the selected layers
|
||||||
pub fn set_blend_mode_for_selected_layers(&self, blend_mode_svg_style_name: String) -> Result<(), JsValue> {
|
pub fn set_blend_mode_for_selected_layers(&self, blend_mode_svg_style_name: String) -> Result<(), JsValue> {
|
||||||
let blend_mode = translate_blend_mode(blend_mode_svg_style_name.as_str());
|
let blend_mode = translate_blend_mode(blend_mode_svg_style_name.as_str());
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,5 @@ fn panic_hook(info: &panic::PanicInfo) {
|
||||||
let panic_info = info.to_string();
|
let panic_info = info.to_string();
|
||||||
let title = "The editor crashed — sorry about that".to_string();
|
let title = "The editor crashed — sorry about that".to_string();
|
||||||
let description = "An internal error occurred. Reload the editor to continue. Please report this by filing an issue on GitHub.".to_string();
|
let description = "An internal error occurred. Reload the editor to continue. Please report this by filing an issue on GitHub.".to_string();
|
||||||
|
|
||||||
EDITOR_HAS_CRASHED.with(|crash_status| crash_status.borrow_mut().replace(FrontendMessage::DisplayPanic { panic_info, title, description }));
|
EDITOR_HAS_CRASHED.with(|crash_status| crash_status.borrow_mut().replace(FrontendMessage::DisplayPanic { panic_info, title, description }));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -490,11 +490,16 @@ impl Document {
|
||||||
responses.extend(update_thumbnails_upstream(folder));
|
responses.extend(update_thumbnails_upstream(folder));
|
||||||
Some(responses)
|
Some(responses)
|
||||||
}
|
}
|
||||||
Operation::PasteLayer { path, layer, insert_index } => {
|
Operation::InsertLayer {
|
||||||
let folder = self.folder_mut(path)?;
|
destination_path,
|
||||||
let id = folder.add_layer(layer.clone(), None, *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
layer,
|
||||||
let full_path = [path.clone(), vec![id]].concat();
|
insert_index,
|
||||||
self.mark_as_dirty(&full_path)?;
|
} => {
|
||||||
|
let (folder_path, layer_id) = split_path(destination_path)?;
|
||||||
|
let folder = self.folder_mut(folder_path)?;
|
||||||
|
folder.add_layer(layer.clone(), Some(layer_id), *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||||
|
self.mark_as_dirty(destination_path)?;
|
||||||
|
|
||||||
fn aggregate_insertions(folder: &Folder, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
|
fn aggregate_insertions(folder: &Folder, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
|
||||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
||||||
path.push(*id);
|
path.push(*id);
|
||||||
|
|
@ -505,13 +510,14 @@ impl Document {
|
||||||
path.pop();
|
path.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut responses = Vec::new();
|
let mut responses = Vec::new();
|
||||||
if let Ok(folder) = self.folder(&full_path) {
|
if let Ok(folder) = self.folder(destination_path) {
|
||||||
aggregate_insertions(folder, &mut full_path.clone(), &mut responses)
|
aggregate_insertions(folder, &mut destination_path.clone(), &mut responses)
|
||||||
};
|
};
|
||||||
|
|
||||||
responses.extend([DocumentChanged, CreatedLayer { path: full_path }, FolderChanged { path: path.clone() }]);
|
responses.extend([DocumentChanged, CreatedLayer { path: destination_path.clone() }, FolderChanged { path: folder_path.to_vec() }]);
|
||||||
responses.extend(update_thumbnails_upstream(path));
|
responses.extend(update_thumbnails_upstream(destination_path));
|
||||||
Some(responses)
|
Some(responses)
|
||||||
}
|
}
|
||||||
Operation::DuplicateLayer { path } => {
|
Operation::DuplicateLayer { path } => {
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,9 @@ pub enum Operation {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
PasteLayer {
|
InsertLayer {
|
||||||
layer: Layer,
|
layer: Layer,
|
||||||
path: Vec<LayerId>,
|
destination_path: Vec<LayerId>,
|
||||||
insert_index: isize,
|
insert_index: isize,
|
||||||
},
|
},
|
||||||
CreateFolder {
|
CreateFolder {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue