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:
0HyperCube
2022-01-30 15:10:10 +00:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 0da8dabba9
commit a66920aa1c
33 changed files with 1152 additions and 56 deletions
+4
View File
@@ -115,6 +115,10 @@ pub enum DocumentMessage {
SetSnapping {
snap: bool,
},
SetTexboxEditability {
path: Vec<LayerId>,
editable: bool,
},
SetViewMode {
view_mode: ViewMode,
},
+39 -16
View File
@@ -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());
+3
View File
@@ -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,
}
}
}
+3
View File
@@ -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 },
+1
View File
@@ -14,4 +14,5 @@ pub enum MouseCursorIcon {
ZoomOut,
Grabbing,
Crosshair,
Text,
}
+14 -2
View File
@@ -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)
}};
}
+1
View File
@@ -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);
+1
View File
@@ -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;
+8 -8
View File
@@ -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]
+1 -1
View File
@@ -6,7 +6,7 @@ pub enum ToolOptions {
Crop {},
Navigate {},
Eyedropper {},
Text {},
Text { font_size: u32 },
Fill {},
Gradient {},
Brush {},
+1
View File
@@ -11,3 +11,4 @@ pub mod rectangle;
pub mod select;
pub mod shape;
pub mod shared;
pub mod text;
+23 -4
View File
@@ -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;
+355
View File
@@ -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());
}
}