mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Copy and paste layers MVP (#220)
* Initial implementation of copy and paste for layers * Sort layers on copy and add tests * Fix logger init for test * Fix `copy_paste_deleted_layers` test * Readd erroneously removed svg * Make Layer serializable and cleanup * Add test for copy and pasting folders * Cleanup * Rename left_mouseup * Cleanup * Add length check to test * Fix typo * Make mouseup, mousedown more consistent
This commit is contained in:
committed by
Keavon Chambers
parent
1d2bb6fb2b
commit
94c42ff5d7
@@ -10,6 +10,7 @@ license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
kurbo = "0.8"
|
||||
|
||||
kurbo = {version="0.8", features = ["serde"]}
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
glam = "0.16"
|
||||
glam = { version = "0.16", features = ["serde"] }
|
||||
|
||||
@@ -133,6 +133,30 @@ impl Document {
|
||||
self.folder(path)?.layer(id).ok_or(DocumentError::LayerNotFound)
|
||||
}
|
||||
|
||||
/// Given a path to a layer, returns a vector of the indices in the layer tree
|
||||
/// These indices can be used to order a list of layers
|
||||
pub fn indices_for_path(&self, mut path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
|
||||
let mut root = if self.is_mounted(self.work_mount_path.as_slice(), path) {
|
||||
path = &path[self.work_mount_path.len()..];
|
||||
&self.work
|
||||
} else {
|
||||
&self.root
|
||||
}
|
||||
.as_folder()?;
|
||||
let mut indices = vec![];
|
||||
let (path, layer_id) = split_path(path)?;
|
||||
|
||||
for id in path {
|
||||
let pos = root.layer_ids.iter().position(|x| *x == *id).ok_or(DocumentError::LayerNotFound)?;
|
||||
indices.push(pos);
|
||||
root = root.folder(*id).ok_or(DocumentError::LayerNotFound)?;
|
||||
}
|
||||
|
||||
indices.push(root.layer_ids.iter().position(|x| *x == layer_id).ok_or(DocumentError::LayerNotFound)?);
|
||||
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the layer struct at the specified `path`.
|
||||
/// If you manually edit the layer you have to set the cache_dirty flag yourself.
|
||||
pub fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
@@ -227,6 +251,13 @@ impl Document {
|
||||
let (path, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.to_vec() }])
|
||||
}
|
||||
Operation::PasteLayer { path, layer } => {
|
||||
let folder = self.folder_mut(path)?;
|
||||
//FIXME: This clone of layer should be avoided somehow
|
||||
folder.add_layer(layer.clone(), -1).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::DuplicateLayer { path } => {
|
||||
let layer = self.layer(&path)?.clone();
|
||||
let (folder_path, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
|
||||
|
||||
@@ -3,9 +3,10 @@ use kurbo::Shape;
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Deserialize, Serialize)]
|
||||
pub struct Ellipse {}
|
||||
|
||||
impl Ellipse {
|
||||
|
||||
@@ -2,9 +2,10 @@ use crate::{DocumentError, LayerId};
|
||||
|
||||
use super::{style, Layer, LayerData, LayerDataTypes};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Folder {
|
||||
next_assignment_id: LayerId,
|
||||
pub layer_ids: Vec<LayerId>,
|
||||
|
||||
@@ -4,9 +4,10 @@ use kurbo::Point;
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Line {}
|
||||
|
||||
impl Line {
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod ellipse;
|
||||
pub use ellipse::Ellipse;
|
||||
|
||||
pub mod line;
|
||||
use glam::{DMat2, DVec2};
|
||||
use kurbo::BezPath;
|
||||
pub use line::Line;
|
||||
|
||||
@@ -17,16 +18,16 @@ pub mod shape;
|
||||
pub use shape::Shape;
|
||||
|
||||
pub mod folder;
|
||||
pub use folder::Folder;
|
||||
|
||||
use crate::DocumentError;
|
||||
pub use folder::Folder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub trait LayerData {
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle);
|
||||
fn to_kurbo_path(&mut self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum LayerDataTypes {
|
||||
Folder(Folder),
|
||||
Ellipse(Ellipse),
|
||||
@@ -77,11 +78,19 @@ impl LayerDataTypes {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "glam::DAffine2")]
|
||||
struct DAffine2Ref {
|
||||
pub matrix2: DMat2,
|
||||
pub translation: DVec2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Layer {
|
||||
pub visible: bool,
|
||||
pub name: Option<String>,
|
||||
pub data: LayerDataTypes,
|
||||
#[serde(with = "DAffine2Ref")]
|
||||
pub transform: glam::DAffine2,
|
||||
pub style: style::PathStyle,
|
||||
pub cache: String,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::{style, LayerData};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct PolyLine {
|
||||
points: Vec<glam::DVec2>,
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ use kurbo::Point;
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Rect {}
|
||||
|
||||
impl Rect {
|
||||
|
||||
@@ -4,9 +4,10 @@ use kurbo::Vec2;
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Shape {
|
||||
equal_sides: bool,
|
||||
sides: u8,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{layers::style, LayerId};
|
||||
use crate::{
|
||||
layers::{style, Layer},
|
||||
LayerId,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -44,6 +47,10 @@ pub enum Operation {
|
||||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
PasteLayer {
|
||||
layer: Layer,
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
AddFolder {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
|
||||
@@ -19,3 +19,6 @@ glam = "0.16"
|
||||
[dependencies.document-core]
|
||||
path = "../document"
|
||||
package = "graphite-document-core"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.8.4"
|
||||
|
||||
@@ -76,3 +76,228 @@ impl Dispatcher {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
message_prelude::{DocumentMessage, Message},
|
||||
misc::test_utils::EditorTestUtils,
|
||||
Editor,
|
||||
};
|
||||
use document_core::{color::Color, Operation};
|
||||
use log::info;
|
||||
|
||||
fn init_logger() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
}
|
||||
|
||||
/// Create an editor instance with three layers
|
||||
/// 1. A red rectangle
|
||||
/// 2. A blue shape
|
||||
/// 3. A green ellipse
|
||||
fn create_editor_with_three_layers() -> Editor {
|
||||
let mut editor = Editor::new(Box::new(|e| {
|
||||
info!("Got frontend message: {:?}", e);
|
||||
}));
|
||||
|
||||
editor.select_primary_color(Color::RED);
|
||||
editor.draw_rect(100, 200, 300, 400);
|
||||
editor.select_primary_color(Color::BLUE);
|
||||
editor.draw_shape(10, 1200, 1300, 400);
|
||||
editor.select_primary_color(Color::GREEN);
|
||||
editor.draw_ellipse(104, 1200, 1300, 400);
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// - create rect, shape and ellipse
|
||||
/// - copy
|
||||
/// - paste
|
||||
/// - assert that ellipse was copied
|
||||
fn copy_paste_single_layer() {
|
||||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 4);
|
||||
|
||||
// Existing layers are unaffected
|
||||
for i in 0..=2 {
|
||||
assert_eq!(layers_before_copy[i], layers_after_copy[i]);
|
||||
}
|
||||
|
||||
// The ellipse was copied
|
||||
assert_eq!(layers_before_copy[2], layers_after_copy[3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// - create rect, shape and ellipse
|
||||
/// - select shape
|
||||
/// - copy
|
||||
/// - paste
|
||||
/// - assert that shape was copied
|
||||
fn copy_paste_single_layer_from_middle() {
|
||||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![shape_id]]))).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 4);
|
||||
|
||||
// Existing layers are unaffected
|
||||
for i in 0..=2 {
|
||||
assert_eq!(layers_before_copy[i], layers_after_copy[i]);
|
||||
}
|
||||
|
||||
// The shape was copied
|
||||
assert_eq!(layers_before_copy[1], layers_after_copy[3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_paste_folder() {
|
||||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
const FOLDER_INDEX: usize = 3;
|
||||
const ELLIPSE_INDEX: usize = 2;
|
||||
const SHAPE_INDEX: usize = 1;
|
||||
const RECT_INDEX: usize = 0;
|
||||
|
||||
const LINE_INDEX: usize = 0;
|
||||
const PEN_INDEX: usize = 1;
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::AddFolder(vec![]))).unwrap();
|
||||
|
||||
let document_before_added_shapes = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
|
||||
|
||||
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
|
||||
// This has not been done yet as the line and pen tool are not yet able to add layers to the currently selected folder
|
||||
editor
|
||||
.handle_message(Message::Document(DocumentMessage::DispatchOperation(Operation::AddLine {
|
||||
path: vec![folder_id],
|
||||
insert_index: 0,
|
||||
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
style: Default::default(),
|
||||
})))
|
||||
.unwrap();
|
||||
|
||||
editor
|
||||
.handle_message(Message::Document(DocumentMessage::DispatchOperation(Operation::AddPen {
|
||||
path: vec![folder_id],
|
||||
insert_index: 0,
|
||||
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
style: Default::default(),
|
||||
points: vec![(10.0, 20.0), (30.0, 40.0)],
|
||||
})))
|
||||
.unwrap();
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![folder_id]]))).unwrap();
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 4);
|
||||
assert_eq!(layers_after_copy.len(), 5);
|
||||
|
||||
let rect_before_copy = &layers_before_copy[RECT_INDEX];
|
||||
let ellipse_before_copy = &layers_before_copy[ELLIPSE_INDEX];
|
||||
let shape_before_copy = &layers_before_copy[SHAPE_INDEX];
|
||||
let folder_before_copy = &layers_before_copy[FOLDER_INDEX];
|
||||
let line_before_copy = folder_before_copy.as_folder().unwrap().layers()[LINE_INDEX].clone();
|
||||
let pen_before_copy = folder_before_copy.as_folder().unwrap().layers()[PEN_INDEX].clone();
|
||||
|
||||
assert_eq!(&layers_after_copy[0], rect_before_copy);
|
||||
assert_eq!(&layers_after_copy[1], shape_before_copy);
|
||||
assert_eq!(&layers_after_copy[2], ellipse_before_copy);
|
||||
assert_eq!(&layers_after_copy[3], folder_before_copy);
|
||||
assert_eq!(&layers_after_copy[4], folder_before_copy);
|
||||
|
||||
// Check the layers inside the two folders
|
||||
let first_folder_layers_after_copy = layers_after_copy[3].as_folder().unwrap().layers();
|
||||
let second_folder_layers_after_copy = layers_after_copy[4].as_folder().unwrap().layers();
|
||||
|
||||
assert_eq!(first_folder_layers_after_copy.len(), 2);
|
||||
assert_eq!(second_folder_layers_after_copy.len(), 2);
|
||||
|
||||
assert_eq!(first_folder_layers_after_copy[0], line_before_copy);
|
||||
assert_eq!(first_folder_layers_after_copy[1], pen_before_copy);
|
||||
|
||||
assert_eq!(second_folder_layers_after_copy[0], line_before_copy);
|
||||
assert_eq!(second_folder_layers_after_copy[1], pen_before_copy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// - create rect, shape and ellipse
|
||||
/// - select ellipse and rect
|
||||
/// - copy
|
||||
/// - delete
|
||||
/// - create another rect
|
||||
/// - paste
|
||||
/// - paste
|
||||
fn copy_paste_deleted_layers() {
|
||||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
const ELLIPSE_INDEX: usize = 2;
|
||||
const SHAPE_INDEX: usize = 1;
|
||||
const RECT_INDEX: usize = 0;
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
|
||||
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![rect_id], vec![ellipse_id]]))).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
|
||||
editor.draw_rect(0, 800, 12, 200);
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 6);
|
||||
|
||||
let rect_before_copy = &layers_before_copy[RECT_INDEX];
|
||||
let ellipse_before_copy = &layers_before_copy[ELLIPSE_INDEX];
|
||||
|
||||
assert_eq!(layers_after_copy[0], layers_before_copy[SHAPE_INDEX]);
|
||||
assert_eq!(&layers_after_copy[2], rect_before_copy);
|
||||
assert_eq!(&layers_after_copy[3], ellipse_before_copy);
|
||||
assert_eq!(&layers_after_copy[4], rect_before_copy);
|
||||
assert_eq!(&layers_after_copy[5], ellipse_before_copy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::{
|
||||
input::{mouse::ViewportPosition, InputPreprocessor},
|
||||
message_prelude::*,
|
||||
};
|
||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::message_prelude::*;
|
||||
use document_core::layers::Layer;
|
||||
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use log::info;
|
||||
use log::warn;
|
||||
|
||||
use crate::document::Document;
|
||||
use std::collections::VecDeque;
|
||||
@@ -17,6 +16,8 @@ pub enum DocumentMessage {
|
||||
DeleteLayer(Vec<LayerId>),
|
||||
DeleteSelectedLayers,
|
||||
DuplicateSelectedLayers,
|
||||
CopySelectedLayers,
|
||||
PasteLayers,
|
||||
AddFolder(Vec<LayerId>),
|
||||
RenameLayer(Vec<LayerId>, String),
|
||||
ToggleLayerVisibility(Vec<LayerId>),
|
||||
@@ -52,6 +53,7 @@ pub struct DocumentMessageHandler {
|
||||
active_document: usize,
|
||||
mmb_down: bool,
|
||||
mouse_pos: ViewportPosition,
|
||||
copy_buffer: Vec<Layer>,
|
||||
}
|
||||
|
||||
impl DocumentMessageHandler {
|
||||
@@ -81,6 +83,32 @@ impl DocumentMessageHandler {
|
||||
// TODO: Add deduplication
|
||||
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
|
||||
}
|
||||
|
||||
/// Returns the paths to the selected layers in order
|
||||
fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
// Compute the indices for each layer to be able to sort them
|
||||
let mut layers_with_indices: Vec<(Vec<LayerId>, Vec<usize>)> = self
|
||||
.active_document()
|
||||
.layer_data
|
||||
.iter()
|
||||
.filter_map(|(path, data)| data.selected.then(|| path.clone()))
|
||||
.filter_map(|path| {
|
||||
// Currently it is possible that layer_data contains layers that are don't actually exist
|
||||
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
|
||||
// Once this problem is solved this code can be simplified
|
||||
match self.active_document().document.indices_for_path(&path) {
|
||||
Err(err) => {
|
||||
warn!("selected_layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
|
||||
None
|
||||
}
|
||||
Ok(indices) => Some((path, indices)),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
layers_with_indices.sort_by_key(|(_, indices)| indices.clone());
|
||||
return layers_with_indices.into_iter().map(|(path, _)| path).collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DocumentMessageHandler {
|
||||
@@ -90,6 +118,7 @@ impl Default for DocumentMessageHandler {
|
||||
active_document: 0,
|
||||
mmb_down: false,
|
||||
mouse_pos: ViewportPosition::default(),
|
||||
copy_buffer: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +257,25 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
|
||||
}
|
||||
}
|
||||
CopySelectedLayers => {
|
||||
let paths: Vec<Vec<LayerId>> = self.selected_layers_sorted();
|
||||
self.copy_buffer.clear();
|
||||
for path in paths {
|
||||
match self.active_document().document.layer(&path).map(|t| t.clone()) {
|
||||
Ok(layer) => {
|
||||
self.copy_buffer.push(layer);
|
||||
}
|
||||
Err(e) => warn!("Could not access selected layer {:?}: {:?}", path, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
PasteLayers => {
|
||||
for layer in self.copy_buffer.iter() {
|
||||
//TODO: Should be the path to the current folder instead of root
|
||||
responses.push_back(DocumentOperation::PasteLayer { layer: layer.clone(), path: vec![] }.into())
|
||||
}
|
||||
}
|
||||
|
||||
SelectLayers(paths) => {
|
||||
self.clear_selection();
|
||||
for path in paths {
|
||||
@@ -294,9 +342,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||
}
|
||||
fn actions(&self) -> ActionList {
|
||||
if self.active_document().layer_data.values().any(|data| data.selected) {
|
||||
actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, DuplicateSelectedLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown)
|
||||
actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, DuplicateSelectedLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown, CopySelectedLayers, PasteLayers, )
|
||||
} else {
|
||||
actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown)
|
||||
actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown, PasteLayers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ macro_rules! mapping {
|
||||
impl Default for Mapping {
|
||||
fn default() -> Self {
|
||||
let (up, down, pointer_move) = mapping![
|
||||
entry! {action=DocumentMessage::PasteLayers, key_down=KeyV, modifiers=[KeyControl]},
|
||||
// Rectangle
|
||||
entry! {action=RectangleMessage::Center, key_down=KeyAlt},
|
||||
entry! {action=RectangleMessage::UnCenter, key_up=KeyAlt},
|
||||
@@ -174,11 +175,12 @@ impl Default for Mapping {
|
||||
entry! {action=DocumentMessage::NewDocument, key_down=KeyN, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::NextDocument, key_down=KeyTab, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::CloseActiveDocument, key_down=KeyW, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]},
|
||||
// Global Actions
|
||||
entry! {action=GlobalMessage::LogInfo, key_down=Key1},
|
||||
entry! {action=GlobalMessage::LogDebug, key_down=Key2},
|
||||
entry! {action=GlobalMessage::LogTrace, key_down=Key3},
|
||||
entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]},
|
||||
];
|
||||
Self { up, down, pointer_move }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
pub mod macros;
|
||||
pub mod derivable_custom_traits;
|
||||
mod error;
|
||||
pub mod test_utils;
|
||||
|
||||
pub use error::EditorError;
|
||||
pub use macros::*;
|
||||
|
||||
83
core/editor/src/misc/test_utils.rs
Normal file
83
core/editor/src/misc/test_utils.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use crate::{
|
||||
input::{
|
||||
mouse::{MouseKeys, MouseState, ViewportPosition},
|
||||
InputPreprocessorMessage,
|
||||
},
|
||||
message_prelude::{Message, ToolMessage},
|
||||
tool::ToolType,
|
||||
Editor,
|
||||
};
|
||||
use document_core::color::Color;
|
||||
|
||||
/// A set of utility functions to make the writing of editor test more declarative
|
||||
pub trait EditorTestUtils {
|
||||
fn draw_rect(&mut self, x1: u32, y1: u32, x2: u32, y2: u32);
|
||||
fn draw_shape(&mut self, x1: u32, y1: u32, x2: u32, y2: u32);
|
||||
fn draw_ellipse(&mut self, x1: u32, y1: u32, x2: u32, y2: u32);
|
||||
|
||||
/// Select given tool and drag it from (x1, y1) to (x2, y2)
|
||||
fn drag_tool(&mut self, typ: ToolType, x1: u32, y1: u32, x2: u32, y2: u32);
|
||||
fn move_mouse(&mut self, x: u32, y: u32);
|
||||
fn mousedown(&mut self, state: MouseState);
|
||||
fn mouseup(&mut self, state: MouseState);
|
||||
fn lmb_mousedown(&mut self, x: u32, y: u32);
|
||||
fn input(&mut self, message: InputPreprocessorMessage);
|
||||
fn select_tool(&mut self, typ: ToolType);
|
||||
fn select_primary_color(&mut self, color: Color);
|
||||
}
|
||||
|
||||
impl EditorTestUtils for Editor {
|
||||
fn draw_rect(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
|
||||
self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
fn draw_shape(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
|
||||
self.drag_tool(ToolType::Shape, x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
fn draw_ellipse(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
|
||||
self.drag_tool(ToolType::Ellipse, x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
fn drag_tool(&mut self, typ: ToolType, x1: u32, y1: u32, x2: u32, y2: u32) {
|
||||
self.select_tool(typ);
|
||||
self.move_mouse(x1, y1);
|
||||
self.lmb_mousedown(x1, y1);
|
||||
self.move_mouse(x2, y2);
|
||||
self.mouseup(MouseState {
|
||||
position: ViewportPosition { x: x2, y: y2 },
|
||||
mouse_keys: MouseKeys::empty(),
|
||||
});
|
||||
}
|
||||
|
||||
fn move_mouse(&mut self, x: u32, y: u32) {
|
||||
self.input(InputPreprocessorMessage::MouseMove(ViewportPosition { x, y }));
|
||||
}
|
||||
|
||||
fn mousedown(&mut self, state: MouseState) {
|
||||
self.input(InputPreprocessorMessage::MouseDown(state));
|
||||
}
|
||||
|
||||
fn mouseup(&mut self, state: MouseState) {
|
||||
self.handle_message(InputPreprocessorMessage::MouseUp(state)).unwrap()
|
||||
}
|
||||
|
||||
fn lmb_mousedown(&mut self, x: u32, y: u32) {
|
||||
self.mousedown(MouseState {
|
||||
position: ViewportPosition { x, y },
|
||||
mouse_keys: MouseKeys::LEFT,
|
||||
})
|
||||
}
|
||||
|
||||
fn input(&mut self, message: InputPreprocessorMessage) {
|
||||
self.handle_message(Message::InputPreprocessor(message)).unwrap();
|
||||
}
|
||||
|
||||
fn select_tool(&mut self, typ: ToolType) {
|
||||
self.handle_message(Message::Tool(ToolMessage::SelectTool(typ))).unwrap();
|
||||
}
|
||||
|
||||
fn select_primary_color(&mut self, color: Color) {
|
||||
self.handle_message(Message::Tool(ToolMessage::SelectPrimaryColor(color))).unwrap();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user