diff --git a/Cargo.lock b/Cargo.lock index 6988fd1948..39120197f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,13 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] [[package]] name = "arrayvec" @@ -8,6 +15,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -16,9 +34,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bumpalo" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" [[package]] name = "cfg-if" @@ -42,11 +60,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "glam" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16" +dependencies = [ + "serde", +] [[package]] name = "graphite-cli" @@ -67,6 +101,7 @@ name = "graphite-editor-core" version = "0.1.0" dependencies = [ "bitflags", + "env_logger", "glam", "graphite-document-core", "graphite-proc-macros", @@ -101,6 +136,21 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "itoa" version = "0.4.7" @@ -123,6 +173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42" dependencies = [ "arrayvec", + "serde", ] [[package]] @@ -131,6 +182,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + [[package]] name = "log" version = "0.4.14" @@ -140,11 +197,17 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + [[package]] name = "proc-macro2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" dependencies = [ "unicode-xid", ] @@ -158,6 +221,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "ryu" version = "1.0.5" @@ -203,29 +283,38 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2", "quote", @@ -339,3 +428,34 @@ dependencies = [ "js-sys", "wasm-bindgen", ] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/core/document/Cargo.toml b/core/document/Cargo.toml index 9d8657640e..d54a5f2576 100644 --- a/core/document/Cargo.toml +++ b/core/document/Cargo.toml @@ -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"] } diff --git a/core/document/src/document.rs b/core/document/src/document.rs index a7aa3848c2..8307cf5dbe 100644 --- a/core/document/src/document.rs +++ b/core/document/src/document.rs @@ -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, 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)); diff --git a/core/document/src/layers/ellipse.rs b/core/document/src/layers/ellipse.rs index 56a07157c0..65e8b6ea87 100644 --- a/core/document/src/layers/ellipse.rs +++ b/core/document/src/layers/ellipse.rs @@ -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 { diff --git a/core/document/src/layers/folder.rs b/core/document/src/layers/folder.rs index bafff20fd2..dae9344c86 100644 --- a/core/document/src/layers/folder.rs +++ b/core/document/src/layers/folder.rs @@ -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, diff --git a/core/document/src/layers/line.rs b/core/document/src/layers/line.rs index 7595380fce..8b36a2e2cb 100644 --- a/core/document/src/layers/line.rs +++ b/core/document/src/layers/line.rs @@ -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 { diff --git a/core/document/src/layers/mod.rs b/core/document/src/layers/mod.rs index 2ee4ad8c10..48a8ebbdcd 100644 --- a/core/document/src/layers/mod.rs +++ b/core/document/src/layers/mod.rs @@ -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, pub data: LayerDataTypes, + #[serde(with = "DAffine2Ref")] pub transform: glam::DAffine2, pub style: style::PathStyle, pub cache: String, diff --git a/core/document/src/layers/polyline.rs b/core/document/src/layers/polyline.rs index 4892e0320f..c2558fae13 100644 --- a/core/document/src/layers/polyline.rs +++ b/core/document/src/layers/polyline.rs @@ -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, } diff --git a/core/document/src/layers/rect.rs b/core/document/src/layers/rect.rs index 503430ddad..0cd3e7f946 100644 --- a/core/document/src/layers/rect.rs +++ b/core/document/src/layers/rect.rs @@ -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 { diff --git a/core/document/src/layers/shape.rs b/core/document/src/layers/shape.rs index d00134287a..6ff9a71c6e 100644 --- a/core/document/src/layers/shape.rs +++ b/core/document/src/layers/shape.rs @@ -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, diff --git a/core/document/src/operation.rs b/core/document/src/operation.rs index 960ce35ce7..96902ee5c1 100644 --- a/core/document/src/operation.rs +++ b/core/document/src/operation.rs @@ -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, }, + PasteLayer { + layer: Layer, + path: Vec, + }, AddFolder { path: Vec, }, diff --git a/core/editor/Cargo.toml b/core/editor/Cargo.toml index aa701dc168..5f6c36e21b 100644 --- a/core/editor/Cargo.toml +++ b/core/editor/Cargo.toml @@ -19,3 +19,6 @@ glam = "0.16" [dependencies.document-core] path = "../document" package = "graphite-document-core" + +[dev-dependencies] +env_logger = "0.8.4" diff --git a/core/editor/src/communication/dispatcher.rs b/core/editor/src/communication/dispatcher.rs index 7081b17ca0..5e519d7083 100644 --- a/core/editor/src/communication/dispatcher.rs +++ b/core/editor/src/communication/dispatcher.rs @@ -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); + } +} diff --git a/core/editor/src/document/document_message_handler.rs b/core/editor/src/document/document_message_handler.rs index b449ca6509..a9d2e8872a 100644 --- a/core/editor/src/document/document_message_handler.rs +++ b/core/editor/src/document/document_message_handler.rs @@ -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), DeleteSelectedLayers, DuplicateSelectedLayers, + CopySelectedLayers, + PasteLayers, AddFolder(Vec), RenameLayer(Vec, String), ToggleLayerVisibility(Vec), @@ -52,6 +53,7 @@ pub struct DocumentMessageHandler { active_document: usize, mmb_down: bool, mouse_pos: ViewportPosition, + copy_buffer: Vec, } 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> { + // Compute the indices for each layer to be able to sort them + let mut layers_with_indices: Vec<(Vec, Vec)> = 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 for DocumentMessageHand responses.push_back(DocumentOperation::DuplicateLayer { path }.into()) } } + CopySelectedLayers => { + let paths: Vec> = 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 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) } } } diff --git a/core/editor/src/input/input_mapper.rs b/core/editor/src/input/input_mapper.rs index 91d06e307f..797e2d1b3f 100644 --- a/core/editor/src/input/input_mapper.rs +++ b/core/editor/src/input/input_mapper.rs @@ -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 } } diff --git a/core/editor/src/misc/mod.rs b/core/editor/src/misc/mod.rs index ad0b8560a0..963324ed36 100644 --- a/core/editor/src/misc/mod.rs +++ b/core/editor/src/misc/mod.rs @@ -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::*; diff --git a/core/editor/src/misc/test_utils.rs b/core/editor/src/misc/test_utils.rs new file mode 100644 index 0000000000..fe3b03f421 --- /dev/null +++ b/core/editor/src/misc/test_utils.rs @@ -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(); + } +}