From 121a68ad3c5d9ae323124062b966467e0f96b55c Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sun, 30 Jan 2022 15:10:10 +0000 Subject: [PATCH] Implement the Text Tool and text layer MVP (#492) * Add text tool * Double click with the select tool to edit text * Fix (I think?) transitioning to select tool * Commit and abort text editing * Transition to a contenteditable div and autosize * Fix right click blocking * Cleanup hints * Ctrl + enter leaves text edit mode * Render indervidual bounding boxes for text * Re-format space indents * Reflect font size in the textarea * Fix change tool behaviour * Remove starting text * Populate the cache (caused doc load bug) * Remove console log * Chrome display the flashing text entry cursor * Update overlay on input * Cleanup input.ts * Fix bounding boxes * Apply review feedback * Remove manual test * Remove svg from gitignore Co-authored-by: Keavon Chambers --- Cargo.lock | 60 +++ editor/src/document/document_message.rs | 4 + .../src/document/document_message_handler.rs | 55 ++- editor/src/document/layer_panel.rs | 3 + editor/src/frontend/frontend_message.rs | 3 + editor/src/frontend/utility_types.rs | 1 + editor/src/input/input_mapper.rs | 16 +- editor/src/input/input_mapper_message.rs | 1 + .../src/input/input_preprocessor_message.rs | 1 + .../input_preprocessor_message_handler.rs | 8 + editor/src/lib.rs | 1 + editor/src/viewport_tools/tool.rs | 16 +- editor/src/viewport_tools/tool_message.rs | 3 + editor/src/viewport_tools/tool_options.rs | 2 +- editor/src/viewport_tools/tools/mod.rs | 1 + editor/src/viewport_tools/tools/select.rs | 27 +- editor/src/viewport_tools/tools/text.rs | 355 ++++++++++++++++++ frontend/src/App.vue | 1 + frontend/src/components/panels/Document.vue | 96 ++++- .../widgets/options/ToolOptions.vue | 2 +- frontend/src/dispatcher/js-messages.ts | 18 +- frontend/src/lifetime/input.ts | 38 +- frontend/wasm/src/api.rs | 25 ++ graphene/Cargo.toml | 4 + graphene/src/document.rs | 49 ++- graphene/src/error.rs | 1 + graphene/src/layers/layer_info.rs | 18 + graphene/src/layers/mod.rs | 1 + .../src/layers/text/SourceSansPro/OFL.txt | 93 +++++ .../SourceSansPro/SourceSansPro-Regular.ttf | Bin 0 -> 248504 bytes graphene/src/layers/text/mod.rs | 146 +++++++ graphene/src/layers/text/to_kurbo.rs | 143 +++++++ graphene/src/operation.rs | 16 + 33 files changed, 1152 insertions(+), 56 deletions(-) create mode 100644 editor/src/viewport_tools/tools/text.rs create mode 100644 graphene/src/layers/text/SourceSansPro/OFL.txt create mode 100644 graphene/src/layers/text/SourceSansPro/SourceSansPro-Regular.ttf create mode 100644 graphene/src/layers/text/mod.rs create mode 100644 graphene/src/layers/text/to_kurbo.rs diff --git a/Cargo.lock b/Cargo.lock index 49bdaabb1..895343d81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,12 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +[[package]] +name = "bytemuck" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -110,7 +116,9 @@ dependencies = [ "glam", "kurbo", "log", + "rustybuzz", "serde", + "ttf-parser", ] [[package]] @@ -280,6 +288,22 @@ dependencies = [ "syn", ] +[[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.9" @@ -341,6 +365,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + [[package]] name = "spin" version = "0.9.2" @@ -390,6 +420,36 @@ dependencies = [ "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" diff --git a/editor/src/document/document_message.rs b/editor/src/document/document_message.rs index 7946f1474..6942bea21 100644 --- a/editor/src/document/document_message.rs +++ b/editor/src/document/document_message.rs @@ -115,6 +115,10 @@ pub enum DocumentMessage { SetSnapping { snap: bool, }, + SetTexboxEditability { + path: Vec, + editable: bool, + }, SetViewMode { view_mode: ViewMode, }, diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 478d960e3..3cf5feab5 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -1,5 +1,5 @@ use super::clipboards::Clipboard; -use super::layer_panel::{layer_panel_entry, LayerMetadata, LayerPanelEntry, RawBuffer}; +use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer}; use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis, VectorManipulatorSegment, VectorManipulatorShape}; use super::vectorize_layer_metadata; use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler}; @@ -146,11 +146,11 @@ impl DocumentMessageHandler { _ => return None, }; - let shape = match &layer.ok()?.data { - LayerDataType::Shape(shape) => Some(shape), - LayerDataType::Folder(_) => None, + let (path, closed) = match &layer.ok()?.data { + LayerDataType::Shape(shape) => Some((shape.path.clone(), shape.closed)), + LayerDataType::Text(text) => Some((text.to_bez_path_nonmut(), true)), + _ => None, }?; - let path = shape.path.clone(); let segments = path .segments() @@ -170,7 +170,7 @@ impl DocumentMessageHandler { path, segments, transform: viewport_transform, - closed: shape.closed, + closed, }) }); @@ -204,6 +204,16 @@ impl DocumentMessageHandler { }) } + pub fn selected_visible_text_layers(&self) -> impl Iterator { + self.selected_layers().filter(|path| match self.graphene_document.layer(path) { + Ok(layer) => { + let discriminant: LayerDataTypeDiscriminant = (&layer.data).into(); + layer.visible && discriminant == LayerDataTypeDiscriminant::Text + } + Err(_) => false, + }) + } + pub fn visible_layers(&self) -> impl Iterator { self.all_layers().filter(|path| match self.graphene_document.layer(path) { Ok(layer) => layer.visible, @@ -216,17 +226,14 @@ impl DocumentMessageHandler { for (id, layer) in folder.layer_ids.iter().zip(folder.layers()).rev() { data.push(*id); space += 1; - match layer.data { - LayerDataType::Shape(_) => (), - LayerDataType::Folder(ref folder) => { - path.push(*id); - if self.layer_metadata(path).expanded { - structure.push(space); - self.serialize_structure(folder, structure, data, path); - space = 0; - } - path.pop(); + if let LayerDataType::Folder(ref folder) = layer.data { + path.push(*id); + if self.layer_metadata(path).expanded { + structure.push(space); + self.serialize_structure(folder, structure, data, path); + space = 0; } + path.pop(); } } structure.push(space | 1 << 63); @@ -962,6 +969,22 @@ impl MessageHandler for Docum SetSnapping { snap } => { self.snapping_enabled = snap; } + SetTexboxEditability { path, editable } => { + let text = self.graphene_document.layer(&path).unwrap().as_text().unwrap(); + responses.push_back(DocumentOperation::SetTextEditability { path, editable }.into()); + if editable { + responses.push_back( + FrontendMessage::DisplayEditableTextbox { + text: text.text.clone(), + line_width: text.line_width, + font_size: text.size, + } + .into(), + ); + } else { + responses.push_back(FrontendMessage::DisplayRemoveEditableTextbox.into()); + } + } SetViewMode { view_mode } => { self.view_mode = view_mode; responses.push_front(DocumentMessage::DirtyRenderDocument.into()); diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index f637a1b6e..5c899f789 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -97,6 +97,7 @@ pub struct LayerPanelEntry { pub enum LayerDataTypeDiscriminant { Folder, Shape, + Text, } impl fmt::Display for LayerDataTypeDiscriminant { @@ -104,6 +105,7 @@ impl fmt::Display for LayerDataTypeDiscriminant { let name = match self { LayerDataTypeDiscriminant::Folder => "Folder", LayerDataTypeDiscriminant::Shape => "Shape", + LayerDataTypeDiscriminant::Text => "Text", }; formatter.write_str(name) @@ -117,6 +119,7 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant { match data { Folder(_) => LayerDataTypeDiscriminant::Folder, Shape(_) => LayerDataTypeDiscriminant::Shape, + Text(_) => LayerDataTypeDiscriminant::Text, } } } diff --git a/editor/src/frontend/frontend_message.rs b/editor/src/frontend/frontend_message.rs index 6bcaff992..5e9969330 100644 --- a/editor/src/frontend/frontend_message.rs +++ b/editor/src/frontend/frontend_message.rs @@ -18,12 +18,15 @@ pub enum FrontendMessage { DisplayDialogError { title: String, description: String }, DisplayDialogPanic { panic_info: String, title: String, description: String }, DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer }, + DisplayEditableTextbox { text: String, line_width: Option, font_size: f64 }, + DisplayRemoveEditableTextbox, // Trigger prefix: cause a browser API to do something TriggerFileDownload { document: String, name: String }, TriggerFileUpload, TriggerIndexedDbRemoveDocument { document_id: u64 }, TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String }, + TriggerTextCommit, // Update prefix: give the frontend a new value or state for it to use UpdateActiveDocument { document_id: u64 }, diff --git a/editor/src/frontend/utility_types.rs b/editor/src/frontend/utility_types.rs index 6bc51118f..cf0c2fd29 100644 --- a/editor/src/frontend/utility_types.rs +++ b/editor/src/frontend/utility_types.rs @@ -14,4 +14,5 @@ pub enum MouseCursorIcon { ZoomOut, Grabbing, Crosshair, + Text, } diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 1b492a5c7..e5b924f78 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -14,6 +14,7 @@ pub struct Mapping { pub key_down: [KeyMappingEntries; NUMBER_OF_KEYS], pub pointer_move: KeyMappingEntries, pub mouse_scroll: KeyMappingEntries, + pub double_click: KeyMappingEntries, } impl Default for Mapping { @@ -44,6 +45,7 @@ impl Default for Mapping { entry! {action=SelectMessage::MouseMove { snap_angle: KeyShift }, message=InputMapperMessage::PointerMove}, entry! {action=SelectMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb}, entry! {action=SelectMessage::DragStop, key_up=Lmb}, + entry! {action=SelectMessage::EditText, message=InputMapperMessage::DoubleClick}, entry! {action=SelectMessage::Abort, key_down=Rmb}, entry! {action=SelectMessage::Abort, key_down=KeyEscape}, // Navigate @@ -59,6 +61,10 @@ impl Default for Mapping { // Eyedropper entry! {action=EyedropperMessage::LeftMouseDown, key_down=Lmb}, entry! {action=EyedropperMessage::RightMouseDown, key_down=Rmb}, + // Text + entry! {action=TextMessage::Interact, key_up=Lmb}, + entry! {action=TextMessage::Abort, key_down=KeyEscape}, + entry! {action=TextMessage::CommitText, key_down=KeyEnter, modifiers=[KeyControl]}, // Rectangle entry! {action=RectangleMessage::DragStart, key_down=Lmb}, entry! {action=RectangleMessage::DragStop, key_up=Lmb}, @@ -105,6 +111,7 @@ impl Default for Mapping { entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Select }, key_down=KeyV}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Navigate }, key_down=KeyZ}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }, key_down=KeyI}, + entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Text }, key_down=KeyT}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Fill }, key_down=KeyF}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Path }, key_down=KeyA}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Pen }, key_down=KeyP}, @@ -203,7 +210,7 @@ impl Default for Mapping { entry! {action=GlobalMessage::LogDebug, key_down=Key2}, entry! {action=GlobalMessage::LogTrace, key_down=Key3}, ]; - let (mut key_up, mut key_down, mut pointer_move, mut mouse_scroll) = mappings; + let (mut key_up, mut key_down, mut pointer_move, mut mouse_scroll, mut double_click) = mappings; // TODO: Hardcode these 10 lines into 10 lines of declarations, or make this use a macro to do all 10 in one line const NUMBER_KEYS: [Key; 10] = [Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9]; @@ -226,12 +233,14 @@ impl Default for Mapping { } sort(&mut pointer_move); sort(&mut mouse_scroll); + sort(&mut double_click); Self { key_up, key_down, pointer_move, mouse_scroll, + double_click, } } } @@ -243,6 +252,7 @@ impl Mapping { let list = match message { KeyDown(key) => &self.key_down[key as usize], KeyUp(key) => &self.key_up[key as usize], + DoubleClick => &self.double_click, MouseScroll => &self.mouse_scroll, PointerMove => &self.pointer_move, }; @@ -331,6 +341,7 @@ mod input_mapper_macros { let mut key_down = KeyMappingEntries::key_array(); let mut pointer_move: KeyMappingEntries = Default::default(); let mut mouse_scroll: KeyMappingEntries = Default::default(); + let mut double_click: KeyMappingEntries = Default::default(); $( for entry in $entry { let arr = match entry.trigger { @@ -338,11 +349,12 @@ mod input_mapper_macros { InputMapperMessage::KeyUp(key) => &mut key_up[key as usize], InputMapperMessage::MouseScroll => &mut mouse_scroll, InputMapperMessage::PointerMove => &mut pointer_move, + InputMapperMessage::DoubleClick => &mut double_click, }; arr.push(entry.clone()); } )* - (key_up, key_down, pointer_move, mouse_scroll) + (key_up, key_down, pointer_move, mouse_scroll, double_click) }}; } diff --git a/editor/src/input/input_mapper_message.rs b/editor/src/input/input_mapper_message.rs index 829376b60..454ebcaa5 100644 --- a/editor/src/input/input_mapper_message.rs +++ b/editor/src/input/input_mapper_message.rs @@ -16,6 +16,7 @@ pub enum InputMapperMessage { KeyUp(Key), // Messages + DoubleClick, MouseScroll, PointerMove, } diff --git a/editor/src/input/input_preprocessor_message.rs b/editor/src/input/input_preprocessor_message.rs index 04c287c81..53297fa89 100644 --- a/editor/src/input/input_preprocessor_message.rs +++ b/editor/src/input/input_preprocessor_message.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum InputPreprocessorMessage { BoundsOfViewports { bounds_of_viewports: Vec }, + DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, KeyDown { key: Key, modifier_keys: ModifierKeys }, KeyUp { key: Key, modifier_keys: ModifierKeys }, MouseDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, diff --git a/editor/src/input/input_preprocessor_message_handler.rs b/editor/src/input/input_preprocessor_message_handler.rs index 025bc70a5..93a4d58ec 100644 --- a/editor/src/input/input_preprocessor_message_handler.rs +++ b/editor/src/input/input_preprocessor_message_handler.rs @@ -49,6 +49,14 @@ impl MessageHandler for InputPreprocessorMessageHa ); } } + InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => { + self.handle_modifier_keys(modifier_keys, responses); + + let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); + self.mouse.position = mouse_state.position; + + responses.push_back(InputMapperMessage::DoubleClick.into()); + } InputPreprocessorMessage::KeyDown { key, modifier_keys } => { self.handle_modifier_keys(modifier_keys, responses); self.keyboard.set(key as usize); diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 37443cebc..1e04b002a 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -81,6 +81,7 @@ pub mod message_prelude { pub use crate::viewport_tools::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant}; pub use crate::viewport_tools::tools::select::{SelectMessage, SelectMessageDiscriminant}; pub use crate::viewport_tools::tools::shape::{ShapeMessage, ShapeMessageDiscriminant}; + pub use crate::viewport_tools::tools::text::{TextMessage, TextMessageDiscriminant}; pub use graphite_proc_macros::*; pub use std::collections::VecDeque; diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index a955e4059..39fabf765 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -76,7 +76,7 @@ impl Default for ToolFsmState { Crop => crop::Crop, Navigate => navigate::Navigate, Eyedropper => eyedropper::Eyedropper, - // Text => text::Text, + Text => text::Text, Fill => fill::Fill, // Gradient => gradient::Gradient, // Brush => brush::Brush, @@ -208,7 +208,7 @@ impl ToolType { ToolType::Crop => ToolOptions::Crop {}, ToolType::Navigate => ToolOptions::Navigate {}, ToolType::Eyedropper => ToolOptions::Eyedropper {}, - ToolType::Text => ToolOptions::Text {}, + ToolType::Text => ToolOptions::Text { font_size: 14 }, ToolType::Fill => ToolOptions::Fill {}, ToolType::Gradient => ToolOptions::Gradient {}, ToolType::Brush => ToolOptions::Brush {}, @@ -241,10 +241,10 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy match message_type { StandardToolMessageType::DocumentIsDirty => match tool { ToolType::Select => Some(SelectMessage::DocumentIsDirty.into()), - ToolType::Crop => None, // Some(CropMessage::DocumentIsDirty.into()), - ToolType::Navigate => None, // Some(NavigateMessage::DocumentIsDirty.into()), - ToolType::Eyedropper => None, // Some(EyedropperMessage::DocumentIsDirty.into()), - ToolType::Text => None, // Some(TextMessage::DocumentIsDirty.into()), + ToolType::Crop => None, // Some(CropMessage::DocumentIsDirty.into()), + ToolType::Navigate => None, // Some(NavigateMessage::DocumentIsDirty.into()), + ToolType::Eyedropper => None, // Some(EyedropperMessage::DocumentIsDirty.into()), + ToolType::Text => Some(TextMessage::DocumentIsDirty.into()), ToolType::Fill => None, // Some(FillMessage::DocumentIsDirty.into()), ToolType::Gradient => None, // Some(GradientMessage::DocumentIsDirty.into()), ToolType::Brush => None, // Some(BrushMessage::DocumentIsDirty.into()), @@ -267,7 +267,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy // ToolType::Crop => Some(CropMessage::Abort.into()), ToolType::Navigate => Some(NavigateMessage::Abort.into()), ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()), - // ToolType::Text => Some(TextMessage::Abort.into()), + ToolType::Text => Some(TextMessage::Abort.into()), ToolType::Fill => Some(FillMessage::Abort.into()), // ToolType::Gradient => Some(GradientMessage::Abort.into()), // ToolType::Brush => Some(BrushMessage::Abort.into()), @@ -297,7 +297,7 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType { Crop(_) => ToolType::Crop, Navigate(_) => ToolType::Navigate, Eyedropper(_) => ToolType::Eyedropper, - // Text(_) => ToolType::Text, + Text(_) => ToolType::Text, Fill(_) => ToolType::Fill, // Gradient(_) => ToolType::Gradient, // Brush(_) => ToolType::Brush, diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index b801a0beb..24578d8d4 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -28,6 +28,9 @@ pub enum ToolMessage { // Text(TextMessage), #[remain::unsorted] #[child] + Text(TextMessage), + #[remain::unsorted] + #[child] Fill(FillMessage), // #[remain::unsorted] // #[child] diff --git a/editor/src/viewport_tools/tool_options.rs b/editor/src/viewport_tools/tool_options.rs index 4ca47bacd..7da44712e 100644 --- a/editor/src/viewport_tools/tool_options.rs +++ b/editor/src/viewport_tools/tool_options.rs @@ -6,7 +6,7 @@ pub enum ToolOptions { Crop {}, Navigate {}, Eyedropper {}, - Text {}, + Text { font_size: u32 }, Fill {}, Gradient {}, Brush {}, diff --git a/editor/src/viewport_tools/tools/mod.rs b/editor/src/viewport_tools/tools/mod.rs index 721243557..c6c1334fb 100644 --- a/editor/src/viewport_tools/tools/mod.rs +++ b/editor/src/viewport_tools/tools/mod.rs @@ -11,3 +11,4 @@ pub mod rectangle; pub mod select; pub mod shape; pub mod shared; +pub mod text; diff --git a/editor/src/viewport_tools/tools/select.rs b/editor/src/viewport_tools/tools/select.rs index 8d3dff87c..cb0af2217 100644 --- a/editor/src/viewport_tools/tools/select.rs +++ b/editor/src/viewport_tools/tools/select.rs @@ -8,7 +8,7 @@ use crate::input::InputPreprocessorMessageHandler; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::snapping::SnapHandler; -use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; use graphene::document::Document; use graphene::intersection::Quad; @@ -43,6 +43,7 @@ pub enum SelectMessage { add_to_selection: Key, }, DragStop, + EditText, FlipHorizontal, FlipVertical, MouseMove { @@ -75,9 +76,9 @@ impl<'a> MessageHandler> for Select { use SelectToolFsmState::*; match self.fsm_state { - Ready => actions!(SelectMessageDiscriminant; DragStart), - Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove), - DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort), + Ready => actions!(SelectMessageDiscriminant; DragStart, EditText), + Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, EditText), + DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditText), } } } @@ -173,6 +174,24 @@ impl Fsm for SelectToolFsmState { buffer.into_iter().rev().for_each(|message| responses.push_front(message)); self } + (_, EditText) => { + let mouse_pos = input.mouse.position; + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); + + if document + .graphene_document + .intersects_quad_root(quad) + .last() + .map(|l| document.graphene_document.layer(l).map(|l| l.as_text().is_ok()).unwrap_or(false)) + .unwrap_or(false) + { + responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()); + responses.push_back(TextMessage::Interact.into()); + } + + self + } (Ready, DragStart { add_to_selection }) => { data.drag_start = input.mouse.position; data.drag_current = input.mouse.position; diff --git a/editor/src/viewport_tools/tools/text.rs b/editor/src/viewport_tools/tools/text.rs new file mode 100644 index 000000000..91bd2fc6d --- /dev/null +++ b/editor/src/viewport_tools/tools/text.rs @@ -0,0 +1,355 @@ +use crate::consts::{COLOR_ACCENT, SELECTION_TOLERANCE}; +use crate::document::DocumentMessageHandler; +use crate::frontend::utility_types::MouseCursorIcon; +use crate::input::keyboard::{Key, MouseMotion}; +use crate::input::InputPreprocessorMessageHandler; +use crate::message_prelude::*; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; +use crate::viewport_tools::tool_options::ToolOptions; + +use glam::{DAffine2, DVec2}; +use graphene::intersection::Quad; +use graphene::layers::style::{self, Fill, Stroke}; +use graphene::Operation; +use kurbo::Shape; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct Text { + fsm_state: TextToolFsmState, + data: TextToolData, +} + +#[remain::sorted] +#[impl_message(Message, ToolMessage, Text)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum TextMessage { + // Standard messages + #[remain::unsorted] + Abort, + + #[remain::unsorted] + DocumentIsDirty, + + // Tool-specific messages + CommitText, + Interact, + TextChange { + new_text: String, + }, + UpdateBounds { + new_text: String, + }, +} + +impl<'a> MessageHandler> for Text { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + if action == ToolMessage::UpdateCursor { + self.fsm_state.update_cursor(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + self.fsm_state.update_cursor(responses); + } + } + + fn actions(&self) -> ActionList { + use TextToolFsmState::*; + + match self.fsm_state { + Ready => actions!(TextMessageDiscriminant; Interact), + Editing => actions!(TextMessageDiscriminant; Interact, Abort, CommitText), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TextToolFsmState { + Ready, + Editing, +} + +impl Default for TextToolFsmState { + fn default() -> Self { + TextToolFsmState::Ready + } +} + +#[derive(Clone, Debug, Default)] +struct TextToolData { + path: Vec, + overlays: Vec>, +} + +fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] { + DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array() +} + +fn resize_overlays(overlays: &mut Vec>, responses: &mut VecDeque, newlen: usize) { + while overlays.len() > newlen { + let operation = Operation::DeleteLayer { path: overlays.pop().unwrap() }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + } + while overlays.len() < newlen { + let path = vec![generate_uuid()]; + overlays.push(path.clone()); + + let operation = Operation::AddOverlayRect { + path, + transform: DAffine2::ZERO.to_cols_array(), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())), + }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + } +} + +fn update_overlays(document: &DocumentMessageHandler, data: &mut TextToolData, responses: &mut VecDeque) { + let visible_text_layers = document.selected_visible_text_layers().collect::>(); + + resize_overlays(&mut data.overlays, responses, visible_text_layers.len()); + + for (layer_path, overlay_path) in visible_text_layers.into_iter().zip(&data.overlays) { + let bounds = document + .graphene_document + .layer(layer_path) + .unwrap() + .current_bounding_box_with_transform(document.graphene_document.multiply_transforms(layer_path).unwrap()) + .unwrap(); + + let operation = Operation::SetLayerTransformInViewport { + path: overlay_path.to_vec(), + transform: transform_from_box(bounds[0], bounds[1]), + }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + } +} + +impl Fsm for TextToolFsmState { + type ToolData = TextToolData; + + fn transition( + self, + event: ToolMessage, + document: &DocumentMessageHandler, + tool_data: &DocumentToolData, + data: &mut Self::ToolData, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) -> Self { + use TextMessage::*; + use TextToolFsmState::*; + + if let ToolMessage::Text(event) = event { + match (self, event) { + (state, DocumentIsDirty) => { + update_overlays(document, data, responses); + + state + } + (state, Interact) => { + let mouse_pos = input.mouse.position; + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); + + let new_state = if let Some(l) = document + .graphene_document + .intersects_quad_root(quad) + .last() + .filter(|l| document.graphene_document.layer(l).map(|l| l.as_text().is_ok()).unwrap_or(false)) + // Editing existing text + { + if state == TextToolFsmState::Editing { + responses.push_back( + DocumentMessage::SetTexboxEditability { + path: data.path.clone(), + editable: false, + } + .into(), + ); + } + + data.path = l.clone(); + + responses.push_back( + DocumentMessage::SetTexboxEditability { + path: data.path.clone(), + editable: true, + } + .into(), + ); + responses.push_back( + DocumentMessage::SetSelectedLayers { + replacement_selected_layers: vec![data.path.clone()], + } + .into(), + ); + + Editing + } + // Creating new text + else if state == TextToolFsmState::Ready { + let transform = DAffine2::from_translation(input.mouse.position).to_cols_array(); + let font_size = match tool_data.tool_options.get(&ToolType::Text) { + Some(&ToolOptions::Text { font_size }) => font_size, + _ => 14, + }; + data.path = vec![generate_uuid()]; + + responses.push_back( + Operation::AddText { + path: data.path.clone(), + transform: DAffine2::ZERO.to_cols_array(), + insert_index: -1, + text: r#""#.to_string(), + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 0.)), None), + size: font_size as f64, + } + .into(), + ); + responses.push_back(Operation::SetLayerTransformInViewport { path: data.path.clone(), transform }.into()); + + responses.push_back( + DocumentMessage::SetTexboxEditability { + path: data.path.clone(), + editable: true, + } + .into(), + ); + + responses.push_back( + DocumentMessage::SetSelectedLayers { + replacement_selected_layers: vec![data.path.clone()], + } + .into(), + ); + + Editing + } else { + // Removing old text as editable + responses.push_back( + DocumentMessage::SetTexboxEditability { + path: data.path.clone(), + editable: false, + } + .into(), + ); + + resize_overlays(&mut data.overlays, responses, 0); + + Ready + }; + + new_state + } + (state, Abort) => { + if state == TextToolFsmState::Editing { + responses.push_back( + DocumentMessage::SetTexboxEditability { + path: data.path.clone(), + editable: false, + } + .into(), + ); + } + + resize_overlays(&mut data.overlays, responses, 0); + + Ready + } + (Editing, CommitText) => { + responses.push_back(FrontendMessage::TriggerTextCommit.into()); + + Editing + } + (Editing, TextChange { new_text }) => { + responses.push_back(Operation::SetTextContent { path: data.path.clone(), new_text }.into()); + + responses.push_back( + DocumentMessage::SetTexboxEditability { + path: data.path.clone(), + editable: false, + } + .into(), + ); + + resize_overlays(&mut data.overlays, responses, 0); + + Ready + } + (Editing, UpdateBounds { new_text }) => { + resize_overlays(&mut data.overlays, responses, 1); + let mut path = document.graphene_document.layer(&data.path).unwrap().as_text().unwrap().bounding_box(&new_text).to_path(0.1); + + fn glam_to_kurbo(transform: DAffine2) -> kurbo::Affine { + kurbo::Affine::new(transform.to_cols_array()) + } + + path.apply_affine(glam_to_kurbo(document.graphene_document.multiply_transforms(&data.path).unwrap())); + + let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box(); + + let operation = Operation::SetLayerTransformInViewport { + path: data.overlays[0].clone(), + transform: transform_from_box(DVec2::new(x0, y0), DVec2::new(x1, y1)), + }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + + Editing + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + TextToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Add Text"), + plus: false, + }, + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Edit Text"), + plus: false, + }, + ])]), + TextToolFsmState::Editing => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyEnter])], + mouse: None, + label: String::from("Commit Edit"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyEscape])], + mouse: None, + label: String::from("Discard Edit"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }.into()); + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b6160ce54..aa7c577d7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -266,6 +266,7 @@ export default defineComponent({ dialog: this.dialog, documents: this.documents, fullscreen: this.fullscreen, + inputManager: this.inputManager, }; }, data() { diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 2b3d08ed3..a180ce803 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -78,7 +78,7 @@ - +