diff --git a/Cargo.lock b/Cargo.lock index 49a170a87a..71170bedd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,21 +30,21 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] -name = "cfg-if" -version = "0.1.10" +name = "bytemuck" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" [[package]] name = "cfg-if" @@ -54,11 +54,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "console_error_panic_hook" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "wasm-bindgen", ] @@ -75,6 +75,18 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fonterator" +version = "0.9.0" +source = "git+https://github.com/0HyperCube/fonterator.git#f5b399b1f5328667bb200e7a8bc791b2cbd52f46" +dependencies = [ + "kurbo", + "lazy_static", + "rustybuzz", + "ttf-parser", + "unicode-script", +] + [[package]] name = "glam" version = "0.17.3" @@ -109,6 +121,7 @@ dependencies = [ name = "graphite-graphene" version = "0.1.0" dependencies = [ + "fonterator", "glam", "kurbo", "log", @@ -155,15 +168,15 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -171,7 +184,7 @@ dependencies = [ [[package]] name = "kurbo" version = "0.8.1" -source = "git+https://github.com/GraphiteEditor/kurbo.git#feb9ef74f841fcc5890bfe6ae763fbb986c80490" +source = "git+https://github.com/GraphiteEditor/kurbo.git#d3d469d91bd42d6f9665e48e5d56dce66a7e183c" dependencies = [ "arrayvec", "serde", @@ -185,15 +198,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -204,35 +217,35 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] name = "memchr" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -270,6 +283,22 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "rustybuzz" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44561062e583c4873162861261f16fd1d85fe927c4904d71329a4fe43dc355ef" +dependencies = [ + "bitflags", + "bytemuck", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-general-category", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.5" @@ -290,18 +319,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.126" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -310,15 +339,21 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + [[package]] name = "spin" version = "0.9.2" @@ -330,9 +365,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.73" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", @@ -350,24 +385,54 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "ttf-parser" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-general-category" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742" + +[[package]] +name = "unicode-script" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dd944fd05f2f0b5c674917aea8a4df6af84f2d8de3fe8d988b95d28fb8fb09" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -376,11 +441,11 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "serde", "serde_json", "wasm-bindgen-macro", @@ -388,9 +453,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", @@ -403,11 +468,11 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.24" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", @@ -415,9 +480,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -425,9 +490,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", "quote", @@ -438,15 +503,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "wasm-bindgen-test" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cab416a9b970464c2882ed92d55b0c33046b08e0bdc9d59b3b718acd4e1bae8" +checksum = "96f1aa7971fdf61ef0f353602102dbea75a56e225ed036c1e3740564b91e6b7e" dependencies = [ "console_error_panic_hook", "js-sys", @@ -458,9 +523,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4543fc6cf3541ef0d98bf720104cc6bd856d7eba449fd2aa365ef4fed0e782" +checksum = "6006f79628dfeb96a86d4db51fbf1344cd7fd8408f06fc9aa3c84913a4789688" dependencies = [ "proc-macro2", "quote", @@ -468,9 +533,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index f6e4c759e0..ad3b3cb634 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -161,11 +161,13 @@ impl DocumentMessageHandler { let shapes = self.selected_layers().filter_map(|path_to_shape| { let viewport_transform = self.graphene_document.generate_transform_relative_to_viewport(path_to_shape).ok()?; - let shape = match &self.graphene_document.layer(path_to_shape).ok()?.data { - LayerDataType::Shape(shape) => Some(shape), + let path = match &self.graphene_document.layer(path_to_shape).ok()?.data { + LayerDataType::Shape(shape) => Some(shape.path.clone()), LayerDataType::Folder(_) => None, + LayerDataType::Text(text) => Some(text.bezpath.clone()), }?; - let path = shape.path.clone(); + + log::info!("Bez {:?}", path); let segments = path .segments() @@ -210,6 +212,7 @@ impl DocumentMessageHandler { space += 1; match layer.data { LayerDataType::Shape(_) => (), + LayerDataType::Text(_) => (), LayerDataType::Folder(ref folder) => { path.push(*id); if self.layerdata(path).expanded { @@ -443,7 +446,7 @@ impl MessageHandler for DocumentMessageHand size.x, size.y, "\n", - self.graphene_document.render_root() + self.graphene_document.render_root(false) ), name, } @@ -577,7 +580,8 @@ impl MessageHandler for DocumentMessageHand responses.push_back(FolderChanged(vec![]).into()); } FolderChanged(path) => { - let _ = self.graphene_document.render_root(); + // ToDo: text editable only enabled if in text tool + let _ = self.graphene_document.render_root(true); responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]); } DocumentStructureChanged => { @@ -619,7 +623,7 @@ impl MessageHandler for DocumentMessageHand RenderDocument => { responses.push_back( FrontendMessage::UpdateCanvas { - document: self.graphene_document.render_root(), + document: self.graphene_document.render_root(true), // ToDo: text editable only enabled if in text tool } .into(), ); diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index ad1e5693a3..83f166d0d0 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -57,14 +57,14 @@ pub fn layer_data<'a>(layer_data: &'a mut HashMap, LayerData>, path layer_data.get_mut(path).expect(&format!("Layer data cannot be found because the path {:?} does not exist", path)) } -pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &Layer, path: Vec) -> LayerPanelEntry { +pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &Layer, mut path: Vec) -> LayerPanelEntry { let layer_type: LayerType = (&layer.data).into(); let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type)); let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]); let arr = arr.iter().map(|x| (*x).into()).collect::>(); let mut thumbnail = String::new(); - layer.data.clone().render(&mut thumbnail, &mut vec![transform]); + layer.data.clone().render(&mut thumbnail, &mut vec![transform], &mut path, false); let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::>().join(","); let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() { format!( @@ -100,6 +100,7 @@ impl From> for Path { Self(iter) } } + impl Serialize for Path { fn serialize(&self, serializer: S) -> Result where @@ -163,6 +164,7 @@ pub struct LayerPanelEntry { pub enum LayerType { Folder, Shape, + Text, } impl fmt::Display for LayerType { @@ -170,6 +172,7 @@ impl fmt::Display for LayerType { let name = match self { LayerType::Folder => "Folder", LayerType::Shape => "Shape", + LayerType::Text => "Text", }; formatter.write_str(name) @@ -182,6 +185,7 @@ impl From<&LayerDataType> for LayerType { match data { Folder(_) => LayerType::Folder, Shape(_) => LayerType::Shape, + Text(_) => LayerType::Text, } } } diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 8487667cfe..cadc3affd9 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -152,6 +152,8 @@ impl Default for Mapping { entry! {action=SelectMessage::DragStop, key_up=Lmb}, entry! {action=SelectMessage::Abort, key_down=Rmb}, entry! {action=SelectMessage::Abort, key_down=KeyEscape}, + // Text + entry! {action=TextMessage::PlaceText, key_down=Lmb}, // Eyedropper entry! {action=EyedropperMessage::LeftMouseDown, key_down=Lmb}, entry! {action=EyedropperMessage::RightMouseDown, key_down=Rmb}, @@ -191,6 +193,7 @@ impl Default for Mapping { // Tool Actions entry! {action=ToolMessage::ActivateTool(ToolType::Select), key_down=KeyV}, entry! {action=ToolMessage::ActivateTool(ToolType::Eyedropper), key_down=KeyI}, + entry! {action=ToolMessage::ActivateTool(ToolType::Text), key_down=KeyT}, entry! {action=ToolMessage::ActivateTool(ToolType::Fill), key_down=KeyF}, entry! {action=ToolMessage::ActivateTool(ToolType::Path), key_down=KeyA}, entry! {action=ToolMessage::ActivateTool(ToolType::Pen), key_down=KeyP}, diff --git a/editor/src/lib.rs b/editor/src/lib.rs index ff5a8339d5..2a5d00cd8b 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -74,6 +74,7 @@ pub mod message_prelude { pub use crate::tool::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant}; pub use crate::tool::tools::select::{SelectMessage, SelectMessageDiscriminant}; pub use crate::tool::tools::shape::{ShapeMessage, ShapeMessageDiscriminant}; + pub use crate::tool::tools::text::{TextMessage, TextMessageDiscriminant}; pub use crate::LayerId; pub use graphite_proc_macros::*; pub use std::collections::VecDeque; diff --git a/editor/src/tool/mod.rs b/editor/src/tool/mod.rs index 17c31c947f..78182431fb 100644 --- a/editor/src/tool/mod.rs +++ b/editor/src/tool/mod.rs @@ -92,6 +92,7 @@ impl Default for ToolFsmState { Line => line::Line, Shape => shape::Shape, Ellipse => ellipse::Ellipse, + Text => text::Text, Fill => fill::Fill, }, }, diff --git a/editor/src/tool/tool_message_handler.rs b/editor/src/tool/tool_message_handler.rs index a281b8f4fb..dfd16c1f09 100644 --- a/editor/src/tool/tool_message_handler.rs +++ b/editor/src/tool/tool_message_handler.rs @@ -9,6 +9,8 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::collections::VecDeque; +use super::tools::text::TextMessage; + #[impl_message(Message, Tool)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum ToolMessage { @@ -21,6 +23,8 @@ pub enum ToolMessage { NoOp, SetToolOptions(ToolType, ToolOptions), #[child] + Text(TextMessage), + #[child] Fill(FillMessage), #[child] Rectangle(RectangleMessage), @@ -167,6 +171,7 @@ impl MessageHandler fn message_to_tool_type(message: &ToolMessage) -> ToolType { use ToolMessage::*; let tool_type = match message { + Text(_) => ToolType::Text, Fill(_) => ToolType::Fill, Rectangle(_) => ToolType::Rectangle, Ellipse(_) => ToolType::Ellipse, diff --git a/editor/src/tool/tools/mod.rs b/editor/src/tool/tools/mod.rs index 36119873be..e4deaa46ef 100644 --- a/editor/src/tool/tools/mod.rs +++ b/editor/src/tool/tools/mod.rs @@ -6,6 +6,7 @@ pub mod pen; pub mod rectangle; pub mod resize; pub mod shape; +pub mod text; // not implemented yet pub mod crop; diff --git a/editor/src/tool/tools/text.rs b/editor/src/tool/tools/text.rs new file mode 100644 index 0000000000..8784acafeb --- /dev/null +++ b/editor/src/tool/tools/text.rs @@ -0,0 +1,48 @@ +use crate::message_prelude::*; +use crate::tool::ToolActionHandlerData; +use glam::{DAffine2, UVec2}; +use graphene::{layers::style, Operation}; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct Text; + +#[impl_message(Message, ToolMessage, Text)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub enum TextMessage { + PlaceText, + InputChanged { path: String, value: String, size: [f64; 2] }, +} + +impl<'a> MessageHandler> for Text { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + if let ToolMessage::Text(action) = action { + match action { + TextMessage::PlaceText => { + let path = vec![generate_uuid()]; + responses.extend([ + Operation::AddText { + path: path.clone(), + insert_index: -1, + style: style::PathStyle::new(None, Some(style::Fill::new(data.1.primary_color))), + } + .into(), + Operation::SetLayerTransformInViewport { + path, + transform: DAffine2::from_translation(data.2.mouse.position).to_cols_array(), + } + .into(), + ]); + } + TextMessage::InputChanged { path, value, size } => { + let path = path.split(",").map(|x| x.parse::().unwrap()).collect::>(); + log::info!("Path {:?} to value {}", path, value); + responses.push_back(Operation::SetText { path: path, text: value, size }.into()); + } + } + } + } + fn actions(&self) -> ActionList { + actions!(TextMessageDiscriminant; PlaceText) + } +} diff --git a/frontend/assets/24px-full-color/node-type-text.svg b/frontend/assets/24px-full-color/node-type-text.svg new file mode 100644 index 0000000000..265bb62ec4 --- /dev/null +++ b/frontend/assets/24px-full-color/node-type-text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 8165d7c96d..2752d9d16b 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -76,7 +76,7 @@ - + @@ -214,6 +214,22 @@ // Fallback values if JS hasn't set these to integers yet width: 100%; height: 100%; + + // Style editable text boxes + textarea { + color: black; + border: none; + outline: none; + background: none; + padding: 0; + margin-top: 0px; + + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + + resize: none; /*remove the resize handle on the bottom right*/ + } } } } diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index 31cff45af7..fb22e7bcaf 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -44,7 +44,8 @@
- + +
{{ layer.name }} diff --git a/frontend/src/components/widgets/labels/IconLabel.vue b/frontend/src/components/widgets/labels/IconLabel.vue index 208aaf81a9..742d47ceb9 100644 --- a/frontend/src/components/widgets/labels/IconLabel.vue +++ b/frontend/src/components/widgets/labels/IconLabel.vue @@ -113,6 +113,7 @@ import MouseHintMMBDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-drag.svg" import NodeTypePath from "@/../assets/24px-full-color/node-type-path.svg"; import NodeTypeFolder from "@/../assets/24px-full-color/node-type-folder.svg"; +import NodeTypeText from "@/../assets/24px-full-color/node-type-text.svg"; const icons = { LayoutSelectTool: { component: LayoutSelectTool, size: 24 }, @@ -194,6 +195,7 @@ const icons = { MouseHintMMBDrag: { component: MouseHintMMBDrag, size: 16 }, NodeTypePath: { component: NodeTypePath, size: 24 }, NodeTypeFolder: { component: NodeTypeFolder, size: 24 }, + NodeTypeText: { component: NodeTypeText, size: 24 }, }; const components = Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])); diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 17a7eb549c..00648cc612 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -21,7 +21,7 @@ const wasm = import("@/../wasm/pkg").then(panicProxy); window.addEventListener("resize", onWindowResize); onWindowResize(); - document.addEventListener("contextmenu", (e) => e.preventDefault()); + // document.addEventListener("contextmenu", (e) => e.preventDefault()); document.addEventListener("fullscreenchange", () => fullscreenModeChanged()); window.addEventListener("keyup", onKeyUp); diff --git a/frontend/src/utilities/input.ts b/frontend/src/utilities/input.ts index 611359137b..84d168eb78 100644 --- a/frontend/src/utilities/input.ts +++ b/frontend/src/utilities/input.ts @@ -5,6 +5,7 @@ import { panicProxy } from "@/utilities/panic-proxy"; const wasm = import("@/../wasm/pkg").then(panicProxy); let viewportMouseInteractionOngoing = false; +let editingTextField: HTMLTextAreaElement | undefined; // Keyboard events @@ -73,6 +74,13 @@ export async function onMouseMove(e: MouseEvent) { } export async function onMouseDown(e: MouseEvent) { + function resize() { + if (editingTextField) { + editingTextField.style.height = "5px"; + editingTextField.style.height = `${editingTextField.scrollHeight}px`; + } + } + const target = e.target && (e.target as HTMLElement); const inCanvas = target && target.closest(".canvas"); const inDialog = target && target.closest(".dialog-modal .floating-menu-content"); @@ -86,7 +94,20 @@ export async function onMouseDown(e: MouseEvent) { e.stopPropagation(); } - if (inCanvas) viewportMouseInteractionOngoing = true; + if (inCanvas) { + if (target.nodeName === "TEXTAREA") { + const TextArea = target as HTMLTextAreaElement; + TextArea.addEventListener("input", resize); + editingTextField = TextArea; + } else if (editingTextField) { + if (editingTextField.dataset.path) { + (await wasm).on_input_changed(editingTextField.dataset.path, editingTextField.value, editingTextField.clientWidth, editingTextField.clientHeight); + } else { + console.error("Edited text had not path attribute"); + } + editingTextField = undefined; + } else viewportMouseInteractionOngoing = true; + } if (viewportMouseInteractionOngoing) { const modifiers = makeModifiersBitfield(e); diff --git a/frontend/src/utilities/response-handler.ts b/frontend/src/utilities/response-handler.ts index 5ecd3bbb49..467d89fc5f 100644 --- a/frontend/src/utilities/response-handler.ts +++ b/frontend/src/utilities/response-handler.ts @@ -417,6 +417,7 @@ export enum LayerType { Line = "Line", PolyLine = "PolyLine", Ellipse = "Ellipse", + Text = "Text", } function newLayerType(input: any): LayerType { switch (input) { @@ -434,6 +435,8 @@ function newLayerType(input: any): LayerType { return LayerType.PolyLine; case "Ellipse": return LayerType.Ellipse; + case "Text": + return LayerType.Text; default: throw Error(`Received invalid input as an enum variant for LayerType: ${input}`); } diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index 54361cfa2f..ebb4612cbc 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -150,6 +150,13 @@ pub fn bounds_of_viewports(bounds_of_viewports: &[f64]) { dispatch(message); } +/// When text field is edited +#[wasm_bindgen] +pub fn on_input_changed(path: String, value: String, width: f64, height: f64) { + let message = TextMessage::InputChanged { path, value, size: [width, height] }; + dispatch(message); +} + /// Mouse movement within the screenspace bounds of the viewport #[wasm_bindgen] pub fn on_mouse_move(x: f64, y: f64, mouse_keys: u8, modifiers: u8) { diff --git a/graphene/Cargo.toml b/graphene/Cargo.toml index 44fb5830f4..0cfe12ccda 100644 --- a/graphene/Cargo.toml +++ b/graphene/Cargo.toml @@ -17,3 +17,4 @@ kurbo = { git = "https://github.com/GraphiteEditor/kurbo.git", features = [ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } glam = { version = "0.17", features = ["serde"] } +fonterator = { git = "https://github.com/0HyperCube/fonterator.git" } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index f4d34ddb66..f997d34e2e 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -3,11 +3,11 @@ use std::{ hash::{Hash, Hasher}, }; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, UVec2}; use serde::{Deserialize, Serialize}; use crate::{ - layers::{self, Folder, Layer, LayerData, LayerDataType, Shape}, + layers::{self, Folder, Layer, LayerData, LayerDataType, Shape, Text}, DocumentError, DocumentResponse, LayerId, Operation, Quad, }; @@ -33,8 +33,8 @@ impl Document { } /// Wrapper around render, that returns the whole document as a Response. - pub fn render_root(&mut self) -> String { - self.root.render(&mut vec![]); + pub fn render_root(&mut self, text_editable: bool) -> String { + self.root.render(&mut vec![], &mut vec![], text_editable); self.root.cache.clone() } @@ -105,6 +105,7 @@ impl Document { Ok(match self.layer(common_prefix_of_path)?.data { LayerDataType::Folder(_) => common_prefix_of_path, LayerDataType::Shape(_) => &common_prefix_of_path[..common_prefix_of_path.len() - 1], + LayerDataType::Text(_) => &common_prefix_of_path[..common_prefix_of_path.len() - 1], }) } @@ -313,6 +314,13 @@ impl Document { use DocumentResponse::*; let responses = match &operation { + Operation::AddText { path, insert_index, style } => { + let layer = Layer::new(LayerDataType::Text(Text::from_string("hello".to_string(), *style)), DAffine2::IDENTITY.to_cols_array()); + + self.set_layer(path, layer, *insert_index)?; + + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + } Operation::AddEllipse { path, insert_index, transform, style } => { let layer = Layer::new(LayerDataType::Shape(Shape::ellipse(*style)), *transform); @@ -495,6 +503,7 @@ impl Document { shape.path = bez_path.clone(); } LayerDataType::Folder(_) => (), + LayerDataType::Text(_) => (), } Some(vec![DocumentChanged, LayerChanged { path: path.clone() }]) } @@ -561,6 +570,19 @@ impl Document { self.mark_as_dirty(path)?; Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) } + Operation::SetText { path, text, size } => { + let layer = self.layer_mut(path)?; + match &mut layer.data { + LayerDataType::Text(t) => { + t.text = text.to_string(); + t.rerender(); + t.size = DVec2::from_slice(size); + } + _ => return Err(DocumentError::NotText), + } + self.mark_as_dirty(path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + } }; Ok(responses) } diff --git a/graphene/src/layers/folder.rs b/graphene/src/layers/folder.rs index f617c7ec76..e30633c3af 100644 --- a/graphene/src/layers/folder.rs +++ b/graphene/src/layers/folder.rs @@ -15,9 +15,11 @@ pub struct Folder { } impl LayerData for Folder { - fn render(&mut self, svg: &mut String, transforms: &mut Vec) { - for layer in &mut self.layers { - let _ = writeln!(svg, "{}", layer.render(transforms)); + fn render(&mut self, svg: &mut String, transforms: &mut Vec, path: &mut Vec, text_editable: bool) { + for (layer, id) in &mut self.layers.iter_mut().zip(&self.layer_ids) { + path.push(*id); + let _ = writeln!(svg, "{}", layer.render(transforms, path, text_editable)); + path.pop(); } } diff --git a/graphene/src/layers/mod.rs b/graphene/src/layers/mod.rs index 225d55338b..5383ca2fe2 100644 --- a/graphene/src/layers/mod.rs +++ b/graphene/src/layers/mod.rs @@ -10,6 +10,10 @@ pub mod simple_shape; pub use simple_shape::Shape; pub mod folder; + +pub mod text_layer; +pub use text_layer::Text; + use crate::LayerId; use crate::{DocumentError, Quad}; pub use folder::Folder; @@ -18,7 +22,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Write; pub trait LayerData { - fn render(&mut self, svg: &mut String, transforms: &mut Vec); + fn render(&mut self, svg: &mut String, transforms: &mut Vec, path: &mut Vec, text_editable: bool); fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>); fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>; } @@ -27,6 +31,7 @@ pub trait LayerData { pub enum LayerDataType { Folder(Folder), Shape(Shape), + Text(Text), } impl LayerDataType { @@ -34,6 +39,7 @@ impl LayerDataType { match self { LayerDataType::Shape(s) => s, LayerDataType::Folder(f) => f, + LayerDataType::Text(t) => t, } } @@ -41,13 +47,14 @@ impl LayerDataType { match self { LayerDataType::Shape(s) => s, LayerDataType::Folder(f) => f, + LayerDataType::Text(t) => t, } } } impl LayerData for LayerDataType { - fn render(&mut self, svg: &mut String, transforms: &mut Vec) { - self.inner_mut().render(svg, transforms) + fn render(&mut self, svg: &mut String, transforms: &mut Vec, path: &mut Vec, text_editable: bool) { + self.inner_mut().render(svg, transforms, path, text_editable) } fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>) { self.inner().intersects_quad(quad, path, intersections) @@ -102,14 +109,14 @@ impl Layer { } } - pub fn render(&mut self, transforms: &mut Vec) -> &str { + pub fn render(&mut self, transforms: &mut Vec, path: &mut Vec, text_editable: bool) -> &str { if !self.visible { return ""; } if self.cache_dirty { transforms.push(self.transform); self.thumbnail_cache.clear(); - self.data.render(&mut self.thumbnail_cache, transforms); + self.data.render(&mut self.thumbnail_cache, transforms, path, text_editable); self.cache.clear(); let _ = writeln!(self.cache, r#") { + fn render(&mut self, svg: &mut String, transforms: &mut Vec, _path: &mut Vec, _text_editable: bool) { let mut path = self.path.clone(); let transform = self.transform(transforms); let inverse = transform.inverse(); diff --git a/graphene/src/layers/text_layer.rs b/graphene/src/layers/text_layer.rs new file mode 100644 index 0000000000..b4dd49690e --- /dev/null +++ b/graphene/src/layers/text_layer.rs @@ -0,0 +1,106 @@ +use glam::{DAffine2, DMat2, DVec2}; +use kurbo::{Affine, BezPath, Rect, Shape}; + +use crate::intersection::intersect_quad_bez_path; +use crate::LayerId; +use crate::Quad; + +use super::style; +use super::style::PathStyle; +use super::LayerData; + +use serde::{Deserialize, Serialize}; +use std::fmt::Write; +fn glam_to_kurbo(transform: DAffine2) -> Affine { + Affine::new(transform.to_cols_array()) +} +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct Text { + pub text: String, + pub style: style::PathStyle, + pub bezpath: BezPath, + pub render_index: i32, + pub size: DVec2, +} + +impl LayerData for Text { + fn render(&mut self, svg: &mut String, transforms: &mut Vec, path: &mut Vec, text_editable: bool) { + log::info!("Path {:?} size{:?}", path, self.size); + let _ = svg.write_str(r#")">"#); + let size = format!("style=\"width:{}px;height:{}px\"", self.size.x, self.size.y); + if text_editable { + let _ = write!( + svg, + r#""#, + self.style.render(), + size, + path.iter().map(|x| x.to_string()).collect::>().join(","), + self.text + ); + } else { + let mut path = self.bezpath.clone(); + let transform = self.transform(transforms); + let inverse = transform.inverse(); + if !inverse.is_finite() { + let _ = write!(svg, ""); + return; + } + path.apply_affine(glam_to_kurbo(transform)); + + let _ = writeln!(svg, r#""#); + let _ = write!(svg, r#""#, path.to_svg(), self.style.render()); + let _ = svg.write_str(""); + } + } + + fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> { + let mut path = self.bezpath.clone(); + if transform.matrix2 == DMat2::ZERO { + return None; + } + path.apply_affine(glam_to_kurbo(transform)); + + use kurbo::Shape; + let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box(); + Some([(x0, y0).into(), (x1, y1).into()]) + } + + fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>) { + if intersect_quad_bez_path(quad, &self.bezpath, true) { + intersections.push(path.clone()); + } + } +} + +impl Text { + pub fn transform(&self, transforms: &[DAffine2]) -> DAffine2 { + let start = match self.render_index { + -1 => 0, + x => (transforms.len() as i32 - x).max(0) as usize, + }; + transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY) + } + + pub fn from_string(text: String, style: PathStyle) -> Self { + let mut result = Self { + style, + text, + bezpath: BezPath::new(), + render_index: 1, + size: DVec2::new(200., 50.), + }; + result.rerender(); + result + } + pub fn rerender(&mut self) { + let mut font = fonterator::source_font(); + let iter = font.render(&self.text, 13800, -1000); + self.bezpath = kurbo::BezPath::from_vec(iter.map(|p| p).collect()); + self.bezpath + .apply_affine(glam_to_kurbo(DAffine2::from_translation(DVec2::new(0., -1.8)) * DAffine2::from_scale(DVec2::splat(17.6)))); + } +} diff --git a/graphene/src/lib.rs b/graphene/src/lib.rs index b4739f634b..1c67f628f0 100644 --- a/graphene/src/lib.rs +++ b/graphene/src/lib.rs @@ -19,5 +19,6 @@ pub enum DocumentError { NotAFolder, NonReorderableSelection, NotAShape, + NotText, InvalidFile(String), } diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index a6527cd7c1..4b4314ba44 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -14,6 +14,11 @@ use serde::{Deserialize, Serialize}; #[repr(C)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub enum Operation { + AddText { + path: Vec, + insert_index: isize, + style: style::PathStyle, + }, AddEllipse { path: Vec, insert_index: isize, @@ -138,6 +143,11 @@ pub enum Operation { path: Vec, color: Color, }, + SetText { + path: Vec, + text: String, + size: [f64; 2], + }, } impl Operation {