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:
0HyperCube
2021-12-30 17:18:18 +00:00
committed by Keavon Chambers
parent 451c9fcd46
commit 3de426b7cc
9 changed files with 264 additions and 36 deletions

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use super::document_message_handler::CopyBufferEntry;
pub use super::layer_panel::*;
use super::movement_handler::{MovementMessage, MovementMessageHandler};
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
@@ -105,6 +106,10 @@ pub enum DocumentMessage {
#[child]
TransformLayers(TransformLayerMessage),
DispatchOperation(Box<DocumentOperation>),
UpdateLayerData {
path: Vec<LayerId>,
layer_data_entry: LayerData,
},
SetSelectedLayers(Vec<Vec<LayerId>>),
AddSelectedLayers(Vec<Vec<LayerId>>),
SelectAllLayers,
@@ -148,6 +153,11 @@ pub enum DocumentMessage {
insert_index: isize,
},
ReorderSelectedLayers(i32), // relative_position,
MoveLayerInTree {
layer: Vec<LayerId>,
insert_above: bool,
neighbor: Vec<LayerId>,
},
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) => {
self.layer_data.iter_mut().filter(|(_, layer_data)| layer_data.selected).for_each(|(path, layer_data)| {
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()),
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) => {
self.snapping_enabled = new_status;
}
@@ -927,6 +976,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
ExportDocument,
SaveDocument,
SetSnapping,
MoveLayerInTree,
);
if self.layer_data.values().any(|data| data.selected) {

View File

@@ -1,14 +1,15 @@
use super::{DocumentMessageHandler, LayerData};
use crate::consts::DEFAULT_DOCUMENT_NAME;
use crate::frontend::frontend_message_handler::FrontendDocumentDetails;
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use graphene::layers::Layer;
use graphene::{LayerId, Operation as DocumentOperation};
use log::warn;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use super::DocumentMessageHandler;
use crate::consts::DEFAULT_DOCUMENT_NAME;
use std::collections::{HashMap, VecDeque};
#[repr(u8)]
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
@@ -17,7 +18,8 @@ pub enum Clipboard {
User,
_ClipboardCount,
}
static CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8;
const CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8;
#[impl_message(Message, Documents)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
@@ -60,7 +62,13 @@ pub struct DocumentsMessageHandler {
documents: HashMap<u64, DocumentMessageHandler>,
document_ids: Vec<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 {
@@ -156,10 +164,12 @@ impl Default for DocumentsMessageHandler {
let starting_key = generate_uuid();
documents_map.insert(starting_key, DocumentMessageHandler::default());
const EMPTY_VEC: Vec<CopyBufferEntry> = vec![];
Self {
documents: documents_map,
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,
}
}
@@ -345,11 +355,12 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
let paths = self.active_document().selected_layers_sorted();
self.copy_buffer[clipboard as usize].clear();
for path in paths {
match self.active_document().graphene_document.layer(&path).map(|t| t.clone()) {
Ok(layer) => {
self.copy_buffer[clipboard as usize].push(layer);
let document = self.active_document();
match (document.graphene_document.layer(&path).map(|t| t.clone()), document.layer_data(&path).clone()) {
(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 } => {
let paste = |layer: &Layer, responses: &mut VecDeque<_>| {
log::trace!("Pasting into folder {:?} as index: {}", path, insert_index);
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>| {
log::trace!("Pasting into folder {:?} as index: {}", &path, insert_index);
let destination_path = [path.to_vec(), vec![generate_uuid()]].concat();
responses.push_back(
DocumentOperation::PasteLayer {
layer: layer.clone(),
path: path.clone(),
DocumentOperation::InsertLayer {
layer: entry.layer.clone(),
destination_path: destination_path.clone(),
insert_index,
}
.into(),
)
);
responses.push_back(
DocumentMessage::UpdateLayerData {
path: destination_path,
layer_data_entry: entry.layer_data,
}
.into(),
);
};
if insert_index == -1 {
for layer in self.copy_buffer[clipboard as usize].iter() {
paste(layer, responses)
for entry in self.copy_buffer[clipboard as usize].iter() {
paste(entry, responses)
}
} else {
for layer in self.copy_buffer[clipboard as usize].iter().rev() {
paste(layer, responses)
for entry in self.copy_buffer[clipboard as usize].iter().rev() {
paste(entry, responses)
}
}
}