Migrate text layers to nodes (#1155)

* Initial work towards text to node

* Add the text generate node

* Implement live edit

* Fix merge error

* Cleanup text tool

* Implement text

* Fix transforms

* Fix broken image frame

* Double click to edit text

* Fix rendering text on load

* Moving whilst editing

* Better text properties

* Prevent changing vector when there is a Text node

* Push node api

* Use node fn macro

* Stable ids

* Image module as a seperate file

* Explain check for "Input Frame" node

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-04-27 03:07:43 +01:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 271f9d5158
commit ef93f8442a
44 changed files with 1081 additions and 1142 deletions
@@ -34,7 +34,6 @@ impl PathOutline {
// Get the bezpath from the shape or text
let subpath = match &document_layer.data {
LayerDataType::Shape(layer_shape) => Some(layer_shape.shape.clone()),
LayerDataType::Text(text) => Some(text.to_subpath_nonmut(render_data)),
LayerDataType::NodeGraphFrame(frame) => frame.as_vector_data().map(|vector_data| Subpath::from_bezier_crate(&vector_data.subpaths)),
_ => document_layer.aabb_for_transform(DAffine2::IDENTITY, render_data).map(|[p1, p2]| Subpath::new_rect(p1, p2)),
}?;
@@ -863,9 +863,11 @@ impl Fsm for SelectToolFsmState {
// Check that only one layer is selected
if selected_layers.next().is_none() {
if let Ok(layer) = document.document_legacy.layer(layer_path) {
if let LayerDataType::Text(_) = layer.data {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into());
responses.push_back(TextToolMessage::EditSelected.into());
if let Ok(network) = layer.as_node_graph() {
if network.nodes.values().any(|node| node.name == "Text") {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into());
responses.push_back(TextToolMessage::EditSelected.into());
}
}
}
}
@@ -1249,15 +1251,16 @@ fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, interse
fn edit_layer_deepest_manipulation(intersect: &Layer, responses: &mut VecDeque<Message>) {
match &intersect.data {
LayerDataType::Text(_) => {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into());
responses.push_back(TextToolMessage::Interact.into());
}
LayerDataType::Shape(_) => {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into());
}
LayerDataType::NodeGraphFrame(frame) if frame.as_vector_data().is_some() => {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into());
LayerDataType::NodeGraphFrame(graph_frame) if graph_frame.as_vector_data().is_some() => {
if graph_frame.network.nodes.values().any(|node| node.name == "Text") {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into());
responses.push_back(TextToolMessage::EditSelected.into());
} else {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into());
}
}
_ => {}
}
@@ -5,17 +5,22 @@ use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMot
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widgets::input_widgets::{FontInput, NumberInput};
use crate::messages::portfolio::document::node_graph::new_text_network;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use document_legacy::intersection::Quad;
use document_legacy::layers::layer_info::LayerDataType;
use document_legacy::layers::layer_info::Layer;
use document_legacy::layers::style::{self, Fill, RenderData, Stroke};
use document_legacy::LayerId;
use document_legacy::Operation;
use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork};
use graphene_core::text::{load_face, Font};
use graphene_core::Color;
use serde::{Deserialize, Serialize};
#[derive(Default)]
@@ -166,7 +171,7 @@ impl ToolTransition for TextTool {
EventToMessageMap {
document_dirty: Some(TextToolMessage::DocumentIsDirty.into()),
tool_abort: Some(TextToolMessage::Abort.into()),
selection_changed: None,
selection_changed: Some(TextToolMessage::DocumentIsDirty.into()),
}
}
}
@@ -177,17 +182,175 @@ enum TextToolFsmState {
Ready,
Editing,
}
#[derive(Clone, Debug)]
pub struct EditingText {
text: String,
font: Font,
font_size: f64,
color: Color,
transform: DAffine2,
}
#[derive(Clone, Debug, Default)]
struct TextToolData {
layer_path: Vec<LayerId>,
overlays: Vec<Vec<LayerId>>,
editing_text: Option<EditingText>,
new_text: String,
}
impl TextToolData {
/// Set the editing state of the currently modifying layer
fn set_editing(&self, editable: bool, responses: &mut VecDeque<Message>) {
fn set_editing(&self, editable: bool, render_data: &RenderData, responses: &mut VecDeque<Message>) {
let path = self.layer_path.clone();
responses.push_back(DocumentMessage::SetTextboxEditability { path, editable }.into());
responses.add(Operation::SetLayerVisibility { path, visible: !editable });
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
responses.add(FrontendMessage::DisplayEditableTextbox {
text: editing_text.text.clone(),
line_width: None,
font_size: editing_text.font_size,
color: editing_text.color,
url: render_data.font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
transform: editing_text.transform.to_cols_array(),
});
} else {
responses.add(FrontendMessage::DisplayRemoveEditableTextbox);
}
}
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> {
let transform = document.document_legacy.multiply_transforms(&self.layer_path).ok()?;
let layer = document.document_legacy.layer(&self.layer_path).ok()?;
let color = layer
.style()
.ok()
.map_or(Color::BLACK, |style| if let Fill::Solid(solid_color) = style.fill() { *solid_color } else { Color::BLACK });
let network = get_network(&self.layer_path, document)?;
let node_id = get_text_node_id(network)?;
let node = network.nodes.get(&node_id)?;
let (text, font, font_size) = Self::extract_text_node_inputs(node)?;
self.editing_text = Some(EditingText {
text: text.clone(),
font: font.clone(),
font_size,
color,
transform,
});
self.new_text = text.clone();
Some(())
}
fn start_editing_layer(&mut self, layer_path: &[LayerId], tool_state: TextToolFsmState, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) {
if tool_state == TextToolFsmState::Editing {
self.set_editing(false, render_data, responses);
}
self.layer_path = layer_path.into();
self.load_layer_text_node(document);
responses.add(DocumentMessage::StartTransaction);
self.set_editing(true, render_data, responses);
let replacement_selected_layers = vec![self.layer_path.clone()];
responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers });
}
fn extract_text_node_inputs(node: &DocumentNode) -> Option<(&String, &Font, f64)> {
let NodeInput::Value { tagged_value: TaggedValue::String(text), .. } = &node.inputs[1] else { return None; };
let NodeInput::Value { tagged_value: TaggedValue::Font(font), .. } = &node.inputs[2] else { return None; };
let NodeInput::Value { tagged_value: TaggedValue::F64(font_size), .. } = &node.inputs[3] else { return None; };
Some((text, font, *font_size))
}
fn interact(&mut self, state: TextToolFsmState, mouse: DVec2, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) -> TextToolFsmState {
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse - tolerance, mouse + tolerance]);
// Check if the user has selected an existing text layer
if let Some(clicked_text_layer_path) = document.document_legacy.intersects_quad_root(quad, render_data).last().filter(|l| is_text_layer(document, l)) {
self.start_editing_layer(clicked_text_layer_path, state, document, render_data, responses);
TextToolFsmState::Editing
}
// Create new text
else if let Some(editing_text) = self.editing_text.as_ref().filter(|_| state == TextToolFsmState::Ready) {
responses.add(DocumentMessage::StartTransaction);
let network = new_text_network(String::new(), editing_text.font.clone(), editing_text.font_size as f64);
responses.add(Operation::AddNodeGraphFrame {
path: self.layer_path.clone(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
network,
});
responses.add(GraphOperationMessage::FillSet {
layer: self.layer_path.clone(),
fill: Fill::solid(editing_text.color),
});
responses.add(GraphOperationMessage::TransformSet {
layer: self.layer_path.clone(),
transform: editing_text.transform,
transform_in: TransformIn::Viewport,
skip_rerender: true,
});
self.set_editing(true, render_data, responses);
let replacement_selected_layers = vec![self.layer_path.clone()];
responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers });
TextToolFsmState::Editing
} else {
// Removing old text as editable
self.set_editing(false, render_data, responses);
resize_overlays(&mut self.overlays, responses, 0);
TextToolFsmState::Ready
}
}
pub fn update_bounds_overlay(&mut self, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) -> Option<()> {
resize_overlays(&mut self.overlays, responses, 1);
let editing_text = self.editing_text.as_ref()?;
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(&data));
let far = graphene_core::text::bounding_box(&self.new_text, buzz_face, editing_text.font_size, None);
let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.document_legacy.multiply_transforms(&self.layer_path).ok()? * quad;
let bounds = transformed_quad.bounding_box();
let operation = Operation::SetLayerTransformInViewport {
path: self.overlays[0].clone(),
transform: transform_from_box(bounds[0], bounds[1]),
};
responses.add(DocumentMessage::Overlays(operation.into()));
Some(())
}
fn get_bounds(&self, text: &str, render_data: &RenderData) -> Option<[DVec2; 2]> {
let editing_text = self.editing_text.as_ref()?;
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(&data));
let subpaths = graphene_core::text::to_path(&text, buzz_face, editing_text.font_size, None);
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box());
let combined_bounds = bounds.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]).unwrap_or_default();
Some(combined_bounds)
}
fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) -> Option<()> {
let layer = self.layer_path.clone();
let old_bounds = self.get_bounds(&self.editing_text.as_ref()?.text, render_data)?;
let new_bounds = self.get_bounds(new_text, render_data)?;
responses.add(GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds });
Some(())
}
}
@@ -198,7 +361,7 @@ fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] {
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());
responses.add(DocumentMessage::Overlays(operation.into()));
}
while overlays.len() < newlen {
let path = vec![generate_uuid()];
@@ -210,206 +373,152 @@ fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Me
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolData, responses: &mut VecDeque<Message>, render_data: &RenderData) {
let visible_text_layers = document.selected_visible_text_layers().collect::<Vec<_>>();
resize_overlays(&mut tool_data.overlays, responses, visible_text_layers.len());
let bounds = visible_text_layers
.into_iter()
.zip(&tool_data.overlays)
.filter_map(|(layer_path, overlay_path)| {
document
.document_legacy
.layer(layer_path)
.unwrap()
.aabb_for_transform(document.document_legacy.multiply_transforms(layer_path).unwrap(), render_data)
.map(|bounds| (bounds, overlay_path))
})
.collect::<Vec<_>>();
let get_bounds = |layer: &Layer, path: &[LayerId], document: &DocumentMessageHandler, render_data: &RenderData| {
let node_graph = layer.as_node_graph().ok()?;
let node_id = get_text_node_id(node_graph)?;
let document_node = node_graph.nodes.get(&node_id)?;
let (text, font, font_size) = TextToolData::extract_text_node_inputs(document_node)?;
let buzz_face = render_data.font_cache.get(font).map(|data| load_face(&data));
let far = graphene_core::text::bounding_box(&text, buzz_face, font_size, None);
let quad = Quad::from_box([DVec2::ZERO, far]);
let multiplied = document.document_legacy.multiply_transforms(path).ok()? * quad;
Some(multiplied.bounding_box())
};
let bounds = document.selected_layers().filter_map(|path| match document.document_legacy.layer(path) {
Ok(layer) => get_bounds(layer, path, document, render_data),
Err(_) => None,
});
let bounds = bounds.collect::<Vec<_>>();
let new_len = bounds.len();
for (bounds, overlay_path) in bounds {
for (bounds, overlay_path) in bounds.iter().zip(&tool_data.overlays) {
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());
responses.add(DocumentMessage::Overlays(operation.into()));
}
resize_overlays(&mut tool_data.overlays, responses, new_len);
}
fn set_edit_layer(layer_path: &[LayerId], tool_state: TextToolFsmState, tool_data: &mut TextToolData, responses: &mut VecDeque<Message>) {
if tool_state == TextToolFsmState::Editing {
tool_data.set_editing(false, responses);
fn get_network<'a>(layer_path: &[LayerId], document: &'a DocumentMessageHandler) -> Option<&'a NodeNetwork> {
let layer = document.document_legacy.layer(layer_path).ok()?;
layer.as_node_graph().ok()
}
fn get_text_node_id(network: &NodeNetwork) -> Option<NodeId> {
network.nodes.iter().find(|(_, node)| node.name == "Text").map(|(&id, _)| id)
}
fn is_text_layer(document: &DocumentMessageHandler, layer_path: &[LayerId]) -> bool {
let Some(network) = get_network(layer_path, document) else { return false; };
get_text_node_id(network).is_some()
}
fn can_edit_selected(document: &DocumentMessageHandler) -> Option<Vec<LayerId>> {
let mut selected_layers = document.selected_layers();
let layer_path = selected_layers.next()?.to_vec();
// Check that only one layer is selected
if selected_layers.next().is_some() {
return None;
}
tool_data.layer_path = layer_path.into();
if !is_text_layer(document, &layer_path) {
return None;
}
responses.push_back(DocumentMessage::StartTransaction.into());
tool_data.set_editing(true, responses);
let replacement_selected_layers = vec![tool_data.layer_path.clone()];
responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into());
Some(layer_path)
}
impl Fsm for TextToolFsmState {
type ToolData = TextToolData;
type ToolOptions = TextOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
ToolActionHandlerData {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, transition_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData {
document,
global_tool_data,
input,
render_data,
..
}: &mut ToolActionHandlerData,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use TextToolFsmState::*;
use TextToolMessage::*;
} = transition_data;
if let ToolMessage::Text(event) = event {
match (self, event) {
(state, DocumentIsDirty) => {
(TextToolFsmState::Editing, TextToolMessage::DocumentIsDirty) => {
responses.add(FrontendMessage::DisplayEditableTextboxTransform {
transform: document.document_legacy.multiply_transforms(&tool_data.layer_path).ok().unwrap_or_default().to_cols_array(),
});
tool_data.update_bounds_overlay(document, render_data, responses);
TextToolFsmState::Editing
}
(state, TextToolMessage::DocumentIsDirty) => {
update_overlays(document, tool_data, responses, render_data);
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]);
(state, TextToolMessage::Interact) => {
tool_data.editing_text = Some(EditingText {
text: String::new(),
transform: DAffine2::from_translation(input.mouse.position),
font_size: tool_options.font_size as f64,
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
color: global_tool_data.primary_color,
});
tool_data.new_text = String::new();
tool_data.layer_path = document.get_path_for_new_layer();
// Check if the user has selected an existing text layer
let new_state = if let Some(clicked_text_layer_path) = document
.document_legacy
.intersects_quad_root(quad, render_data)
.last()
.filter(|l| document.document_legacy.layer(l).map(|l| l.as_text().is_ok()).unwrap_or(false))
{
set_edit_layer(clicked_text_layer_path, state, tool_data, responses);
Editing
}
// Create new text
else if state == TextToolFsmState::Ready {
responses.push_back(DocumentMessage::StartTransaction.into());
let transform = DAffine2::from_translation(input.mouse.position);
let font_size = tool_options.font_size;
let font_name = tool_options.font_name.clone();
let font_style = tool_options.font_style.clone();
tool_data.layer_path = document.get_path_for_new_layer();
responses.push_back(
Operation::AddText {
path: tool_data.layer_path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
insert_index: -1,
text: String::new(),
style: style::PathStyle::new(None, Fill::solid(global_tool_data.primary_color)),
size: font_size as f64,
font_name,
font_style,
}
.into(),
);
responses.push_back(
GraphOperationMessage::TransformSet {
layer: tool_data.layer_path.clone(),
transform,
transform_in: TransformIn::Viewport,
skip_rerender: false,
}
.into(),
);
tool_data.set_editing(true, responses);
let replacement_selected_layers = vec![tool_data.layer_path.clone()];
responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into());
Editing
} else {
// Removing old text as editable
tool_data.set_editing(false, responses);
resize_overlays(&mut tool_data.overlays, responses, 0);
Ready
};
new_state
tool_data.interact(state, input.mouse.position, document, &render_data, responses)
}
(state, EditSelected) => {
let mut selected_layers = document.selected_layers();
if let Some(layer_path) = selected_layers.next() {
// Check that only one layer is selected
if selected_layers.next().is_none() {
if let Ok(layer) = document.document_legacy.layer(layer_path) {
if let LayerDataType::Text(_) = layer.data {
set_edit_layer(layer_path, state, tool_data, responses);
return Editing;
}
}
}
(state, TextToolMessage::EditSelected) => {
if let Some(layer_path) = can_edit_selected(document) {
tool_data.start_editing_layer(&layer_path, state, document, render_data, responses);
return TextToolFsmState::Editing;
}
state
}
(state, Abort) => {
(state, TextToolMessage::Abort) => {
if state == TextToolFsmState::Editing {
tool_data.set_editing(false, responses);
tool_data.set_editing(false, render_data, responses);
}
resize_overlays(&mut tool_data.overlays, responses, 0);
Ready
TextToolFsmState::Ready
}
(Editing, CommitText) => {
responses.push_back(FrontendMessage::TriggerTextCommit.into());
(TextToolFsmState::Editing, TextToolMessage::CommitText) => {
responses.add(FrontendMessage::TriggerTextCommit);
Editing
TextToolFsmState::Editing
}
(Editing, TextChange { new_text }) => {
let path = tool_data.layer_path.clone();
responses.push_back(Operation::SetTextContent { path, new_text }.into());
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => {
let layer_path = tool_data.layer_path.clone();
let network = get_network(&layer_path, document).unwrap();
tool_data.fix_text_bounds(&new_text, document, render_data, responses);
responses.add(NodeGraphMessage::SetQualifiedInputValue {
layer_path,
node_path: vec![get_text_node_id(network).unwrap()],
input_index: 1,
value: TaggedValue::String(new_text),
});
tool_data.set_editing(false, responses);
tool_data.set_editing(false, render_data, responses);
resize_overlays(&mut tool_data.overlays, responses, 0);
Ready
TextToolFsmState::Ready
}
(Editing, UpdateBounds { new_text }) => {
resize_overlays(&mut tool_data.overlays, responses, 1);
let text = document.document_legacy.layer(&tool_data.layer_path).unwrap().as_text().unwrap();
let quad = text.bounding_box(&new_text, text.load_face(render_data));
let transformed_quad = document.document_legacy.multiply_transforms(&tool_data.layer_path).unwrap() * quad;
let bounds = transformed_quad.bounding_box();
let operation = Operation::SetLayerTransformInViewport {
path: tool_data.overlays[0].clone(),
transform: transform_from_box(bounds[0], bounds[1]),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
Editing
(TextToolFsmState::Editing, TextToolMessage::UpdateBounds { new_text }) => {
tool_data.new_text = new_text;
tool_data.update_bounds_overlay(document, render_data, responses);
TextToolFsmState::Editing
}
_ => self,
}
@@ -427,10 +536,10 @@ impl Fsm for TextToolFsmState {
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }.into());
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text });
}
}