mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 05:18:11 +08:00
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 <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
0da8dabba9
commit
a66920aa1c
@@ -115,6 +115,10 @@ pub enum DocumentMessage {
|
||||
SetSnapping {
|
||||
snap: bool,
|
||||
},
|
||||
SetTexboxEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
SetViewMode {
|
||||
view_mode: ViewMode,
|
||||
},
|
||||
|
||||
@@ -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<Item = &[LayerId]> {
|
||||
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<Item = &[LayerId]> {
|
||||
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<DocumentMessage, &InputPreprocessorMessageHandler> 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());
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<f64>, 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 },
|
||||
|
||||
@@ -14,4 +14,5 @@ pub enum MouseCursorIcon {
|
||||
ZoomOut,
|
||||
Grabbing,
|
||||
Crosshair,
|
||||
Text,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ pub enum InputMapperMessage {
|
||||
KeyUp(Key),
|
||||
|
||||
// Messages
|
||||
DoubleClick,
|
||||
MouseScroll,
|
||||
PointerMove,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum InputPreprocessorMessage {
|
||||
BoundsOfViewports { bounds_of_viewports: Vec<ViewportBounds> },
|
||||
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 },
|
||||
|
||||
@@ -49,6 +49,14 @@ impl MessageHandler<InputPreprocessorMessage, ()> 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,6 +28,9 @@ pub enum ToolMessage {
|
||||
// Text(TextMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Text(TextMessage),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Fill(FillMessage),
|
||||
// #[remain::unsorted]
|
||||
// #[child]
|
||||
|
||||
@@ -6,7 +6,7 @@ pub enum ToolOptions {
|
||||
Crop {},
|
||||
Navigate {},
|
||||
Eyedropper {},
|
||||
Text {},
|
||||
Text { font_size: u32 },
|
||||
Fill {},
|
||||
Gradient {},
|
||||
Brush {},
|
||||
|
||||
@@ -11,3 +11,4 @@ pub mod rectangle;
|
||||
pub mod select;
|
||||
pub mod shape;
|
||||
pub mod shared;
|
||||
pub mod text;
|
||||
|
||||
@@ -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<ToolMessage, ToolActionHandlerData<'a>> 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;
|
||||
|
||||
@@ -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<ToolMessage, ToolActionHandlerData<'a>> for Text {
|
||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
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<LayerId>,
|
||||
overlays: Vec<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
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<Vec<LayerId>>, responses: &mut VecDeque<Message>, 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<Message>) {
|
||||
let visible_text_layers = document.selected_visible_text_layers().collect::<Vec<_>>();
|
||||
|
||||
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<Message>,
|
||||
) -> 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<Message>) {
|
||||
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<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }.into());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user