Add max width/height to text layers and draggable text boxes to the Text tool (#2118)

* Make progress in text tool

* Add line_width to gcore and gstd

* minor fix

* Dragging sets line_width correctly

* Get draw overlay to work

* Typo fix

* Make progress in text tool

* Add line_width to gcore and gstd

* minor fix

* Dragging sets line_width correctly

* Get draw overlay to work

* Typo fix

* Improve text bounding box

* Add toggle for editing line width

* Take absolute value of drag

* Fix optional properties

* Code review

* Attempt to add box height and abort with keys

* Attempt to add key modifiers and snap manager

* Use resize for improved dragging

* Refactor typesetting configuration into a struct

* Fix missing px unit in frontend

* Remove lines on rendered text

* Fix backwards compatibility

* Refactor lenient slection as an associate function in tool data

* Add dashed quad to text nodes

* Use correct names for max height and width

* Additional renames and reorder

* ReResolve conflict

* Code review and improvements

---------

Co-authored-by: hypercube <0hypercube@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Nitish Choudhary
2024-12-31 20:50:47 -08:00
committed by GitHub
co-authored by hypercube Keavon Chambers
parent f225756655
commit 66357540bb
34 changed files with 529 additions and 271 deletions
+1 -2
View File
@@ -51,6 +51,5 @@
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.associations": { "files.associations": {
"*.graphite": "json" "*.graphite": "json"
}, }
"rust-analyzer.checkOnSave": false
} }
@@ -26,13 +26,17 @@ pub enum FrontendMessage {
}, },
DisplayEditableTextbox { DisplayEditableTextbox {
text: String, text: String,
#[serde(rename = "lineWidth")] #[serde(rename = "lineHeightRatio")]
line_width: Option<f64>, line_height_ratio: f64,
#[serde(rename = "fontSize")] #[serde(rename = "fontSize")]
font_size: f64, font_size: f64,
color: Color, color: Color,
url: String, url: String,
transform: [f64; 6], transform: [f64; 6],
#[serde(rename = "maxWidth")]
max_width: Option<f64>,
#[serde(rename = "maxHeight")]
max_height: Option<f64>,
}, },
DisplayEditableTextboxTransform { DisplayEditableTextboxTransform {
transform: [f64; 6], transform: [f64; 6],
@@ -154,7 +154,10 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Escape); action_dispatch=EyedropperToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=EyedropperToolMessage::Abort),
// //
// TextToolMessage // TextToolMessage
entry!(KeyUp(MouseLeft); action_dispatch=TextToolMessage::Interact), entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=TextToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
entry!(KeyDown(MouseLeft); action_dispatch=TextToolMessage::DragStart),
entry!(KeyUp(MouseLeft); action_dispatch=TextToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=TextToolMessage::CommitText),
entry!(KeyDown(Escape); action_dispatch=TextToolMessage::CommitText), entry!(KeyDown(Escape); action_dispatch=TextToolMessage::CommitText),
entry!(KeyDown(Enter); modifiers=[Accel], action_dispatch=TextToolMessage::CommitText), entry!(KeyDown(Enter); modifiers=[Accel], action_dispatch=TextToolMessage::CommitText),
// //
@@ -6,7 +6,7 @@ use crate::messages::prelude::*;
use bezier_rs::Subpath; use bezier_rs::Subpath;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font; use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::PointId; use graphene_core::vector::PointId;
@@ -93,9 +93,7 @@ pub enum GraphOperationMessage {
id: NodeId, id: NodeId,
text: String, text: String,
font: Font, font: Font,
size: f64, typesetting: TypesettingConfig,
line_height_ratio: f64,
character_spacing: f64,
parent: LayerNodeIdentifier, parent: LayerNodeIdentifier,
insert_index: usize, insert_index: usize,
}, },
@@ -7,7 +7,7 @@ use crate::messages::prelude::*;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::text::Font; use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, LineCap, LineJoin, Stroke}; use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, LineCap, LineJoin, Stroke};
use graphene_core::Color; use graphene_core::Color;
use graphene_std::vector::convert_usvg_path; use graphene_std::vector::convert_usvg_path;
@@ -174,15 +174,13 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
id, id,
text, text,
font, font,
size, typesetting,
line_height_ratio,
character_spacing,
parent, parent,
insert_index, insert_index,
} => { } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id); let layer = modify_inputs.create_layer(id);
modify_inputs.insert_text(text, font, size, line_height_ratio, character_spacing, layer); modify_inputs.insert_text(text, font, typesetting, layer);
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() }); responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() });
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
@@ -279,7 +277,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
} }
usvg::Node::Text(text) => { usvg::Node::Text(text) => {
let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_core::consts::DEFAULT_FONT_STYLE.to_string()); let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_core::consts::DEFAULT_FONT_STYLE.to_string());
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, 24., 1.2, 1., layer); modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
modify_inputs.fill_set(Fill::Solid(Color::BLACK)); modify_inputs.fill_set(Fill::Solid(Color::BLACK));
} }
} }
@@ -9,7 +9,7 @@ use graph_craft::concrete;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font; use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::{PointId, VectorModificationType}; use graphene_core::vector::{PointId, VectorModificationType};
@@ -179,7 +179,7 @@ impl<'a> ModifyInputsContext<'a> {
} }
} }
pub fn insert_text(&mut self, text: String, font: Font, size: f64, line_height_ratio: f64, character_spacing: f64, layer: LayerNodeIdentifier) { pub fn insert_text(&mut self, text: String, font: Font, typesetting: TypesettingConfig, layer: LayerNodeIdentifier) {
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template(); let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template(); let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template(); let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
@@ -187,9 +187,11 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::scope("editor-api")), Some(NodeInput::scope("editor-api")),
Some(NodeInput::value(TaggedValue::String(text), false)), Some(NodeInput::value(TaggedValue::String(text), false)),
Some(NodeInput::value(TaggedValue::Font(font), false)), Some(NodeInput::value(TaggedValue::Font(font), false)),
Some(NodeInput::value(TaggedValue::F64(size), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)),
Some(NodeInput::value(TaggedValue::F64(line_height_ratio), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)),
Some(NodeInput::value(TaggedValue::F64(character_spacing), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)),
]); ]);
let text_id = NodeId::new(); let text_id = NodeId::new();
@@ -16,7 +16,7 @@ use graph_craft::imaginate_input::ImaginateSamplingMethod;
use graph_craft::ProtoNodeIdentifier; use graph_craft::ProtoNodeIdentifier;
use graphene_core::raster::brush_cache::BrushCache; use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, Image, ImageFrame, NoiseType, RedGreenBlue, RedGreenBlueAlpha}; use graphene_core::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, Image, ImageFrame, NoiseType, RedGreenBlue, RedGreenBlueAlpha};
use graphene_core::text::Font; use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::transform::Footprint; use graphene_core::transform::Footprint;
use graphene_core::vector::VectorData; use graphene_core::vector::VectorData;
use graphene_core::*; use graphene_core::*;
@@ -2112,9 +2112,11 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
TaggedValue::Font(Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.into(), graphene_core::consts::DEFAULT_FONT_STYLE.into())), TaggedValue::Font(Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.into(), graphene_core::consts::DEFAULT_FONT_STYLE.into())),
false, false,
), ),
NodeInput::value(TaggedValue::F64(24.), false), NodeInput::value(TaggedValue::F64(TypesettingConfig::default().font_size), false),
NodeInput::value(TaggedValue::F64(1.2), false), NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false),
NodeInput::value(TaggedValue::F64(1.), false), NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
], ],
..Default::default() ..Default::default()
}, },
@@ -2126,6 +2128,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
"Size".to_string(), "Size".to_string(),
"Line Height".to_string(), "Line Height".to_string(),
"Character Spacing".to_string(), "Character Spacing".to_string(),
"Max Width".to_string(),
"Max Height".to_string(),
], ],
output_names: vec!["Vector".to_string()], output_names: vec!["Vector".to_string()],
..Default::default() ..Default::default()
@@ -661,6 +661,27 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na
.on_commit(commit_value) .on_commit(commit_value)
.widget_holder(), .widget_holder(),
]), ]),
Some(&TaggedValue::OptionalF64(x)) => {
// TODO: Don't wipe out the previously set value (setting it back to the default of 100) when reenabling this checkbox back to Some from None
let toggle_enabled = move |checkbox_input: &CheckboxInput| TaggedValue::OptionalF64(if checkbox_input.checked { Some(100.) } else { None });
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
// The checkbox toggles if the value is Some or None
CheckboxInput::new(x.is_some())
.on_update(update_value(toggle_enabled, node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
number_props
.value(x)
.on_update(update_value(move |x: &NumberInput| TaggedValue::OptionalF64(x.value), node_id, index))
.disabled(x.is_none())
.on_commit(commit_value)
.widget_holder(),
]);
}
_ => {} _ => {}
} }
@@ -1734,6 +1755,8 @@ pub(crate) fn text_properties(document_node: &DocumentNode, node_id: NodeId, _co
let size = number_widget(document_node, node_id, 3, "Size", NumberInput::default().unit(" px").min(1.), true); let size = number_widget(document_node, node_id, 3, "Size", NumberInput::default().unit(" px").min(1.), true);
let line_height_ratio = number_widget(document_node, node_id, 4, "Line Height", NumberInput::default().min(0.).step(0.1), true); let line_height_ratio = number_widget(document_node, node_id, 4, "Line Height", NumberInput::default().min(0.).step(0.1), true);
let character_spacing = number_widget(document_node, node_id, 5, "Character Spacing", NumberInput::default().min(0.).step(0.1), true); let character_spacing = number_widget(document_node, node_id, 5, "Character Spacing", NumberInput::default().min(0.).step(0.1), true);
let max_width = number_widget(document_node, node_id, 6, "Max Width", NumberInput::default().unit(" px").min(1.), false);
let max_height = number_widget(document_node, node_id, 7, "Max Height", NumberInput::default().unit(" px").min(1.), false);
let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }]; let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }];
if let Some(style) = style { if let Some(style) = style {
@@ -1742,6 +1765,8 @@ pub(crate) fn text_properties(document_node: &DocumentNode, node_id: NodeId, _co
result.push(LayoutGroup::Row { widgets: size }); result.push(LayoutGroup::Row { widgets: size });
result.push(LayoutGroup::Row { widgets: line_height_ratio }); result.push(LayoutGroup::Row { widgets: line_height_ratio });
result.push(LayoutGroup::Row { widgets: character_spacing }); result.push(LayoutGroup::Row { widgets: character_spacing });
result.push(LayoutGroup::Row { widgets: max_width });
result.push(LayoutGroup::Row { widgets: max_height });
result result
} }
@@ -32,26 +32,11 @@ impl core::hash::Hash for OverlayContext {
impl OverlayContext { impl OverlayContext {
pub fn quad(&mut self, quad: Quad, color_fill: Option<&str>) { pub fn quad(&mut self, quad: Quad, color_fill: Option<&str>) {
self.render_context.begin_path(); self.dashed_quad(quad, color_fill, None, None);
self.render_context.move_to(quad.0[3].x.round() - 0.5, quad.0[3].y.round() - 0.5);
for i in 0..4 {
self.render_context.line_to(quad.0[i].x.round() - 0.5, quad.0[i].y.round() - 0.5);
}
if let Some(color_fill) = color_fill {
self.render_context.set_fill_style_str(color_fill);
self.render_context.fill();
}
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.stroke();
} }
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) { pub fn dashed_quad(&mut self, quad: Quad, color_fill: Option<&str>, dash_width: Option<f64>, gap_width: Option<f64>) {
self.dashed_line(start, end, color, None, None) // Set the dash pattern
}
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>, gap_width: Option<f64>) {
let start = start.round() - DVec2::splat(0.5);
let end = end.round() - DVec2::splat(0.5);
if let Some(dash_width) = dash_width { if let Some(dash_width) = dash_width {
let gap_width = gap_width.unwrap_or(1.); let gap_width = gap_width.unwrap_or(1.);
let array = js_sys::Array::new(); let array = js_sys::Array::new();
@@ -61,24 +46,65 @@ impl OverlayContext {
.set_line_dash(&JsValue::from(array)) .set_line_dash(&JsValue::from(array))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok(); .ok();
} else { }
self.render_context.begin_path();
self.render_context.move_to(quad.0[3].x.round() - 0.5, quad.0[3].y.round() - 0.5);
for i in 0..4 {
self.render_context.line_to(quad.0[i].x.round() - 0.5, quad.0[i].y.round() - 0.5);
}
if let Some(color_fill) = color_fill {
self.render_context.set_fill_style_str(color_fill);
self.render_context.fill();
}
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.stroke();
// Reset the dash pattern back to solid
if dash_width.is_some() {
self.render_context
.set_line_dash(&JsValue::from(js_sys::Array::new()))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
}
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
self.dashed_line(start, end, color, None, None)
}
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>, gap_width: Option<f64>) {
// Set the dash pattern
if let Some(dash_width) = dash_width {
let gap_width = gap_width.unwrap_or(1.);
let array = js_sys::Array::new(); let array = js_sys::Array::new();
array.push(&JsValue::from(dash_width - 1.));
array.push(&JsValue::from(gap_width));
self.render_context self.render_context
.set_line_dash(&JsValue::from(array)) .set_line_dash(&JsValue::from(array))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok(); .ok();
} }
let start = start.round() - DVec2::splat(0.5);
let end = end.round() - DVec2::splat(0.5);
self.render_context.begin_path(); self.render_context.begin_path();
self.render_context.move_to(start.x, start.y); self.render_context.move_to(start.x, start.y);
self.render_context.line_to(end.x, end.y); self.render_context.line_to(end.x, end.y);
self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE)); self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE));
self.render_context.stroke(); self.render_context.stroke();
// Reset the dash pattern to solid after drawing // Reset the dash pattern back to solid
self.render_context if dash_width.is_some() {
.set_line_dash(&JsValue::from(js_sys::Array::new())) self.render_context
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) .set_line_dash(&JsValue::from(js_sys::Array::new()))
.ok(); .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
} }
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) { pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) {
@@ -15,7 +15,7 @@ use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_core::text::Font; use graphene_core::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, FillType, Gradient}; use graphene_std::vector::style::{Fill, FillType, Gradient};
use interpreted_executor::dynamic_executor::IntrospectError; use interpreted_executor::dynamic_executor::IntrospectError;
@@ -530,7 +530,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
} }
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
if reference == "Text" && inputs_count == 4 { if reference == "Text" && inputs_count != 8 {
let node_definition = resolve_document_node_type(reference).unwrap(); let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node; let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone()); document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone());
@@ -541,12 +541,34 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), &[]); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), &[]);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), &[]); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), &[]);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), &[]); document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), &[]);
document document.network_interface.set_input(
.network_interface &InputConnector::node(*node_id, 4),
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::F64(1.), false), &[]); if inputs_count == 6 {
document old_inputs[4].clone()
.network_interface } else {
.set_input(&InputConnector::node(*node_id, 5), NodeInput::value(TaggedValue::F64(1.), false), &[]); NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
},
&[],
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 5),
if inputs_count == 6 {
old_inputs[5].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
},
&[],
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 6),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
&[],
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 7),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
&[],
);
} }
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
@@ -4,7 +4,7 @@ use crate::messages::prelude::*;
use bezier_rs::Subpath; use bezier_rs::Subpath;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput}; use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font; use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::style::Gradient; use graphene_core::vector::style::Gradient;
use graphene_core::vector::PointId; use graphene_core::vector::PointId;
use graphene_core::Color; use graphene_core::Color;
@@ -127,7 +127,7 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
} }
/// Gets properties from the Text node /// Gets properties from the Text node
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, f64, f64, f64)> { pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig)> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Text")?; let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Text")?;
let Some(TaggedValue::String(text)) = &inputs[1].as_value() else { return None }; let Some(TaggedValue::String(text)) = &inputs[1].as_value() else { return None };
@@ -135,8 +135,17 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
let Some(&TaggedValue::F64(font_size)) = inputs[3].as_value() else { return None }; let Some(&TaggedValue::F64(font_size)) = inputs[3].as_value() else { return None };
let Some(&TaggedValue::F64(line_height_ratio)) = inputs[4].as_value() else { return None }; let Some(&TaggedValue::F64(line_height_ratio)) = inputs[4].as_value() else { return None };
let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None }; let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None };
let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None };
let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None };
Some((text, font, font_size, line_height_ratio, character_spacing)) let typesetting = TypesettingConfig {
font_size,
line_height_ratio,
max_width,
character_spacing,
max_height,
};
Some((text, font, typesetting))
} }
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> { pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
@@ -1,15 +1,13 @@
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use crate::messages::{input_mapper::utility_types::input_keyboard::Key, portfolio::document::graph_operation::utility_types::TransformIn}; use crate::messages::{input_mapper::utility_types::input_keyboard::Key, portfolio::document::graph_operation::utility_types::TransformIn};
use glam::{DAffine2, DVec2, Vec2Swizzles}; use glam::{DAffine2, DVec2, Vec2Swizzles};
use super::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Resize { pub struct Resize {
drag_start: ViewportPosition, /// Stored as a document position so the start doesn't move if the canvas is panned.
drag_start: DVec2,
pub layer: Option<LayerNodeIdentifier>, pub layer: Option<LayerNodeIdentifier>,
pub snap_manager: SnapManager, pub snap_manager: SnapManager,
} }
@@ -29,6 +27,8 @@ impl Resize {
root_transform.transform_point2(self.drag_start) root_transform.transform_point2(self.drag_start)
} }
/// Compute the drag start and end based on the current mouse position. If the layer doesn't exist, returns [`None`].
/// If you want to draw even without a layer, use [`Resize::calculate_points_ignore_layer`].
pub fn calculate_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> Option<[DVec2; 2]> { pub fn calculate_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> Option<[DVec2; 2]> {
let layer = self.layer?; let layer = self.layer?;
@@ -41,7 +41,12 @@ impl Resize {
self.layer.take(); self.layer.take();
return None; return None;
} }
Some(self.calculate_points_ignore_layer(document, input, center, lock_ratio))
}
/// Compute the drag start and end based on the current mouse position. Ignores the state of the layer.
/// If you want to only draw whilst a layer exists, use [`Resize::calculate_points`].
pub fn calculate_points_ignore_layer(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> [DVec2; 2] {
let start = self.viewport_drag_start(document); let start = self.viewport_drag_start(document);
let mouse = input.mouse.position; let mouse = input.mouse.position;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz); let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
@@ -88,7 +93,7 @@ impl Resize {
self.snap_manager.update_indicator(snapped); self.snap_manager.update_indicator(snapped);
} }
Some(points_viewport) points_viewport
} }
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> { pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
@@ -112,7 +112,7 @@ struct ArtboardToolData {
drag_current: DVec2, drag_current: DVec2,
auto_panning: AutoPanning, auto_panning: AutoPanning,
snap_candidates: Vec<SnapCandidatePoint>, snap_candidates: Vec<SnapCandidatePoint>,
dragging_current_artboad_location: IVec2, dragging_current_artboard_location: IVec2,
} }
impl ArtboardToolData { impl ArtboardToolData {
@@ -140,7 +140,7 @@ impl ArtboardToolData {
fn start_resizing(&mut self, _selected_edges: (bool, bool, bool, bool), _document: &DocumentMessageHandler, _input: &InputPreprocessorMessageHandler) { fn start_resizing(&mut self, _selected_edges: (bool, bool, bool, bool), _document: &DocumentMessageHandler, _input: &InputPreprocessorMessageHandler) {
if let Some(bounds) = &mut self.bounding_box_manager { if let Some(bounds) = &mut self.bounding_box_manager {
bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.); bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.);
self.dragging_current_artboad_location = bounds.bounds[0].round().as_ivec2(); self.dragging_current_artboard_location = bounds.bounds[0].round().as_ivec2();
} }
} }
@@ -200,9 +200,9 @@ impl ArtboardToolData {
dimensions: size.round().as_ivec2(), dimensions: size.round().as_ivec2(),
}); });
let translation = position.round().as_ivec2() - self.dragging_current_artboad_location; let translation = position.round().as_ivec2() - self.dragging_current_artboard_location;
self.dragging_current_artboad_location = position.round().as_ivec2(); self.dragging_current_artboard_location = position.round().as_ivec2();
for child in self.selected_artboard.unwrap().children(&document.metadata()) { for child in self.selected_artboard.unwrap().children(document.metadata()) {
let local_translation = document.metadata().downstream_transform_to_document(child).inverse().transform_vector2(-translation.as_dvec2()); let local_translation = document.metadata().downstream_transform_to_document(child).inverse().transform_vector2(-translation.as_dvec2());
responses.add(GraphOperationMessage::TransformChange { responses.add(GraphOperationMessage::TransformChange {
layer: child, layer: child,
@@ -221,12 +221,9 @@ impl Fsm for ArtboardToolFsmState {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self { fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData { document, input, .. } = tool_action_data; let ToolActionHandlerData { document, input, .. } = tool_action_data;
let ToolMessage::Artboard(event) = event else {
return self;
};
let hovered = ArtboardToolData::hovered_artboard(document, input).is_some(); let hovered = ArtboardToolData::hovered_artboard(document, input).is_some();
let ToolMessage::Artboard(event) = event else { return self };
match (self, event) { match (self, event) {
(state, ArtboardToolMessage::Overlays(mut overlay_context)) => { (state, ArtboardToolMessage::Overlays(mut overlay_context)) => {
if state != ArtboardToolFsmState::Drawing { if state != ArtboardToolFsmState::Drawing {
@@ -326,9 +326,7 @@ impl Fsm for BrushToolFsmState {
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning)); responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning));
} }
let ToolMessage::Brush(event) = event else { let ToolMessage::Brush(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(BrushToolFsmState::Ready, BrushToolMessage::DragStart) => { (BrushToolFsmState::Ready, BrushToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@@ -188,9 +188,7 @@ impl Fsm for EllipseToolFsmState {
let shape_data = &mut tool_data.data; let shape_data = &mut tool_data.data;
let ToolMessage::Ellipse(event) = event else { let ToolMessage::Ellipse(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, EllipseToolMessage::Overlays(mut overlay_context)) => { (_, EllipseToolMessage::Overlays(mut overlay_context)) => {
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
@@ -82,9 +82,7 @@ impl Fsm for EyedropperToolFsmState {
fn transition(self, event: ToolMessage, _tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self { fn transition(self, event: ToolMessage, _tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData { global_tool_data, input, .. } = tool_action_data; let ToolActionHandlerData { global_tool_data, input, .. } = tool_action_data;
let ToolMessage::Eyedropper(event) = event else { let ToolMessage::Eyedropper(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
// Ready -> Sampling // Ready -> Sampling
(EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => { (EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => {
@@ -81,10 +81,7 @@ impl Fsm for FillToolFsmState {
document, global_tool_data, input, .. document, global_tool_data, input, ..
} = handler_data; } = handler_data;
let ToolMessage::Fill(event) = event else { let ToolMessage::Fill(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(FillToolFsmState::Ready, color_event) => { (FillToolFsmState::Ready, color_event) => {
let Some(layer_identifier) = document.click(input) else { let Some(layer_identifier) = document.click(input) else {
@@ -194,9 +194,7 @@ impl Fsm for FreehandToolFsmState {
.. ..
} = tool_action_data; } = tool_action_data;
let ToolMessage::Freehand(event) = event else { let ToolMessage::Freehand(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, FreehandToolMessage::Overlays(mut overlay_context)) => { (_, FreehandToolMessage::Overlays(mut overlay_context)) => {
path_endpoint_overlays(document, shape_editor, &mut overlay_context); path_endpoint_overlays(document, shape_editor, &mut overlay_context);
@@ -241,10 +241,7 @@ impl Fsm for GradientToolFsmState {
document, global_tool_data, input, .. document, global_tool_data, input, ..
} = tool_action_data; } = tool_action_data;
let ToolMessage::Gradient(event) = event else { let ToolMessage::Gradient(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, GradientToolMessage::Overlays(mut overlay_context)) => { (_, GradientToolMessage::Overlays(mut overlay_context)) => {
let selected = tool_data.selected_gradient.as_ref(); let selected = tool_data.selected_gradient.as_ref();
@@ -91,9 +91,7 @@ impl Fsm for ImaginateToolFsmState {
) -> Self { ) -> Self {
let shape_data = &mut tool_data.data; let shape_data = &mut tool_data.data;
let ToolMessage::Imaginate(event) = event else { let ToolMessage::Imaginate(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => { (ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
shape_data.start(document, input); shape_data.start(document, input);
@@ -163,9 +163,7 @@ impl Fsm for LineToolFsmState {
document, global_tool_data, input, .. document, global_tool_data, input, ..
} = tool_action_data; } = tool_action_data;
let ToolMessage::Line(event) = event else { let ToolMessage::Line(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, LineToolMessage::Overlays(mut overlay_context)) => { (_, LineToolMessage::Overlays(mut overlay_context)) => {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
@@ -95,10 +95,7 @@ impl Fsm for NavigateToolFsmState {
_tool_options: &Self::ToolOptions, _tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
let ToolMessage::Navigate(navigate) = message else { let ToolMessage::Navigate(navigate) = message else { return self };
return self;
};
match navigate { match navigate {
NavigateToolMessage::PointerUp { zoom_in } => { NavigateToolMessage::PointerUp { zoom_in } => {
if self == NavigateToolFsmState::ZoomOrClickZooming { if self == NavigateToolFsmState::ZoomOrClickZooming {
@@ -588,10 +588,7 @@ impl Fsm for PathToolFsmState {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self { fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data; let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
let ToolMessage::Path(event) = event else { let ToolMessage::Path(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, PathToolMessage::SelectionChanged) => { (_, PathToolMessage::SelectionChanged) => {
// Set the newly targeted layers to visible // Set the newly targeted layers to visible
@@ -550,9 +550,7 @@ impl Fsm for PenToolFsmState {
transform = DAffine2::IDENTITY; transform = DAffine2::IDENTITY;
} }
let ToolMessage::Pen(event) = event else { let ToolMessage::Pen(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, PenToolMessage::SelectionChanged) => { (_, PenToolMessage::SelectionChanged) => {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
@@ -234,9 +234,7 @@ impl Fsm for PolygonToolFsmState {
let polygon_data = &mut tool_data.data; let polygon_data = &mut tool_data.data;
let ToolMessage::Polygon(event) = event else { let ToolMessage::Polygon(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, PolygonToolMessage::Overlays(mut overlay_context)) => { (_, PolygonToolMessage::Overlays(mut overlay_context)) => {
polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
@@ -102,7 +102,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Rectang
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
return; return;
}; };
match action { match action {
RectangleOptionsUpdate::FillColor(color) => { RectangleOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color; self.options.fill.custom_color = color;
@@ -193,10 +192,7 @@ impl Fsm for RectangleToolFsmState {
) -> Self { ) -> Self {
let shape_data = &mut tool_data.data; let shape_data = &mut tool_data.data;
let ToolMessage::Rectangle(event) = event else { let ToolMessage::Rectangle(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, RectangleToolMessage::Overlays(mut overlay_context)) => { (_, RectangleToolMessage::Overlays(mut overlay_context)) => {
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
@@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate};
use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name};
use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::pivot::Pivot;
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::transformation_cage::*; use crate::messages::tool::common_functionality::transformation_cage::*;
@@ -17,6 +17,7 @@ use crate::messages::tool::common_functionality::{auto_panning::AutoPanning, mea
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::text::load_face;
use graphene_std::renderer::Rect; use graphene_std::renderer::Rect;
use graphene_std::vector::misc::BooleanOperation; use graphene_std::vector::misc::BooleanOperation;
@@ -402,11 +403,9 @@ impl Fsm for SelectToolFsmState {
type ToolOptions = (); type ToolOptions = ();
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self { fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData { document, input, .. } = tool_action_data; let ToolActionHandlerData { document, input, font_cache, .. } = tool_action_data;
let ToolMessage::Select(event) = event else { let ToolMessage::Select(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, SelectToolMessage::Overlays(mut overlay_context)) => { (_, SelectToolMessage::Overlays(mut overlay_context)) => {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
@@ -424,6 +423,17 @@ impl Fsm for SelectToolFsmState {
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
{ {
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
if is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text") {
let (text, font, typesetting) = get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool in `interact()`");
let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face, typesetting);
let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(layer) * quad;
overlay_context.dashed_quad(transformed_quad, None, Some(7.), Some(5.));
}
} }
// Update bounds // Update bounds
@@ -194,9 +194,7 @@ impl Fsm for SplineToolFsmState {
document, global_tool_data, input, .. document, global_tool_data, input, ..
} = tool_action_data; } = tool_action_data;
let ToolMessage::Spline(event) = event else { let ToolMessage::Spline(event) = event else { return self };
return self;
};
match (self, event) { match (self, event) {
(_, SplineToolMessage::CanvasTransformed) => self, (_, SplineToolMessage::CanvasTransformed) => self,
(SplineToolFsmState::Ready, SplineToolMessage::DragStart) => { (SplineToolFsmState::Ready, SplineToolMessage::DragStart) => {
@@ -1,17 +1,20 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::consts::DRAG_THRESHOLD;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
use crate::messages::tool::common_functionality::snapping::SnapData;
use crate::messages::tool::common_functionality::{auto_panning::AutoPanning, resize::Resize};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::text::{load_face, Font, FontCache}; use graphene_core::text::{load_face, Font, FontCache, TypesettingConfig};
use graphene_core::vector::style::Fill; use graphene_core::vector::style::Fill;
use graphene_core::Color; use graphene_core::Color;
@@ -54,8 +57,12 @@ pub enum TextToolMessage {
// Tool-specific messages // Tool-specific messages
CommitText, CommitText,
DragStart,
DragStop,
EditSelected, EditSelected,
Interact, Interact,
PointerMove { center: Key, lock_ratio: Key },
PointerOutsideViewport { center: Key, lock_ratio: Key },
TextChange { new_text: String, is_right_click: bool }, TextChange { new_text: String, is_right_click: bool },
UpdateBounds { new_text: String }, UpdateBounds { new_text: String },
UpdateOptions(TextOptionsUpdate), UpdateOptions(TextOptionsUpdate),
@@ -122,7 +129,7 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap())).into()) .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap())).into())
.widget_holder(); .widget_holder();
let character_spacing = NumberInput::new(Some(tool.options.character_spacing)) let character_spacing = NumberInput::new(Some(tool.options.character_spacing))
.label("Character Spacing") .label("Char. Spacing")
.int() .int()
.min(0.) .min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
@@ -193,13 +200,19 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
match self.fsm_state { match self.fsm_state {
TextToolFsmState::Ready => actions!(TextToolMessageDiscriminant; TextToolFsmState::Ready => actions!(TextToolMessageDiscriminant;
Interact, DragStart,
PointerMove,
), ),
TextToolFsmState::Editing => actions!(TextToolMessageDiscriminant; TextToolFsmState::Editing => actions!(TextToolMessageDiscriminant;
Interact, DragStart,
Abort, Abort,
CommitText, CommitText,
), ),
TextToolFsmState::Placing | TextToolFsmState::Dragging => actions!(TextToolMessageDiscriminant;
DragStop,
Abort,
PointerMove,
),
} }
} }
} }
@@ -218,17 +231,22 @@ impl ToolTransition for TextTool {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum TextToolFsmState { enum TextToolFsmState {
/// The tool is ready to place or edit text.
#[default] #[default]
Ready, Ready,
/// The user is typing in the interactive viewport text area.
Editing, Editing,
/// The user is clicking to add a new text layer, but hasn't dragged or released the left mouse button yet.
Placing,
/// The user is dragging to create a new text area.
Dragging,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct EditingText { pub struct EditingText {
text: String, text: String,
font: Font, font: Font,
font_size: f64, typesetting: TypesettingConfig,
line_height_ratio: f64,
character_spacing: f64,
color: Option<Color>, color: Option<Color>,
transform: DAffine2, transform: DAffine2,
} }
@@ -238,6 +256,10 @@ struct TextToolData {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
editing_text: Option<EditingText>, editing_text: Option<EditingText>,
new_text: String, new_text: String,
resize: Resize,
auto_panning: AutoPanning,
// Since the overlays must be drawn without knowledge of the inputs
cached_resize_bounds: [DVec2; 2],
} }
impl TextToolData { impl TextToolData {
@@ -259,11 +281,13 @@ impl TextToolData {
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) { if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
responses.add(FrontendMessage::DisplayEditableTextbox { responses.add(FrontendMessage::DisplayEditableTextbox {
text: editing_text.text.clone(), text: editing_text.text.clone(),
line_width: None, line_height_ratio: editing_text.typesetting.line_height_ratio,
font_size: editing_text.font_size, font_size: editing_text.typesetting.font_size,
color: editing_text.color.unwrap_or(Color::BLACK), color: editing_text.color.unwrap_or(Color::BLACK),
url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(), url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
transform: editing_text.transform.to_cols_array(), transform: editing_text.transform.to_cols_array(),
max_width: editing_text.typesetting.max_width,
max_height: editing_text.typesetting.max_height,
}); });
} else { } else {
// Check if DisplayRemoveEditableTextbox is already in the responses queue // Check if DisplayRemoveEditableTextbox is already in the responses queue
@@ -279,13 +303,11 @@ impl TextToolData {
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> { fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> {
let transform = document.metadata().transform_to_viewport(self.layer); let transform = document.metadata().transform_to_viewport(self.layer);
let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK); let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK);
let (text, font, font_size, line_height_ratio, character_spacing) = graph_modification_utils::get_text(self.layer, &document.network_interface)?; let (text, font, typesetting) = graph_modification_utils::get_text(self.layer, &document.network_interface)?;
self.editing_text = Some(EditingText { self.editing_text = Some(EditingText {
text: text.clone(), text: text.clone(),
font: font.clone(), font: font.clone(),
font_size, typesetting,
line_height_ratio,
character_spacing,
color: Some(color), color: Some(color),
transform, transform,
}); });
@@ -318,76 +340,59 @@ impl TextToolData {
}; };
} }
fn interact( fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
&mut self, // Create new text
state: TextToolFsmState, self.new_text = String::new();
input: &InputPreprocessorMessageHandler, responses.add(DocumentMessage::AddTransaction);
document: &DocumentMessageHandler,
font_cache: &FontCache, self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
responses: &mut VecDeque<Message>,
) -> TextToolFsmState { responses.add(GraphOperationMessage::NewTextLayer {
// Check if the user has selected an existing text layer id: self.layer.to_node(),
if let Some(clicked_text_layer_path) = document text: String::new(),
font: editing_text.font.clone(),
typesetting: editing_text.typesetting,
parent: document.new_layer_parent(true),
insert_index: 0,
});
responses.add(Message::StartBuffer);
responses.add(GraphOperationMessage::FillSet {
layer: self.layer,
fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None },
});
responses.add(GraphOperationMessage::TransformSet {
layer: self.layer,
transform: editing_text.transform,
transform_in: TransformIn::Viewport,
skip_rerender: true,
});
self.editing_text = Some(editing_text);
self.set_editing(true, font_cache, responses);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
responses.add(NodeGraphMessage::RunDocumentGraph);
}
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, font_cache: &FontCache) -> Option<LayerNodeIdentifier> {
document
.metadata() .metadata()
.all_layers() .all_layers()
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text")) .filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text"))
.find(|&layer| { .find(|&layer| {
let (text, font, font_size, line_height_ratio, character_spacing) = let (text, font, typesetting) =
graph_modification_utils::get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool in `interact()`"); graph_modification_utils::get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool in `interact()`");
let buzz_face = font_cache.get(font).map(|data| load_face(data)); let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, line_height_ratio, character_spacing, None); let far = graphene_core::text::bounding_box(text, buzz_face, typesetting);
let quad = Quad::from_box([DVec2::ZERO, far]); let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(layer) * quad; let transformed_quad = document.metadata().transform_to_viewport(layer) * quad;
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y); let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
transformed_quad.contains(mouse) transformed_quad.contains(mouse)
}) { })
self.start_editing_layer(clicked_text_layer_path, state, document, font_cache, responses);
TextToolFsmState::Editing
}
// Create new text
else if let Some(editing_text) = self.editing_text.as_ref().filter(|_| state == TextToolFsmState::Ready) {
responses.add(DocumentMessage::AddTransaction);
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
responses.add(GraphOperationMessage::NewTextLayer {
id: self.layer.to_node(),
text: String::new(),
font: editing_text.font.clone(),
size: editing_text.font_size,
line_height_ratio: editing_text.line_height_ratio,
character_spacing: editing_text.character_spacing,
parent: document.new_layer_bounding_artboard(input),
insert_index: 0,
});
responses.add(Message::StartBuffer);
responses.add(GraphOperationMessage::FillSet {
layer: self.layer,
fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None },
});
responses.add(GraphOperationMessage::TransformSet {
layer: self.layer,
transform: editing_text.transform,
transform_in: TransformIn::Viewport,
skip_rerender: true,
});
self.set_editing(true, font_cache, responses);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
responses.add(NodeGraphMessage::RunDocumentGraph);
TextToolFsmState::Editing
} else {
// Removing old text as editable
self.set_editing(false, font_cache, responses);
TextToolFsmState::Ready
}
} }
} }
@@ -420,9 +425,12 @@ impl Fsm for TextToolFsmState {
font_cache, font_cache,
.. ..
} = transition_data; } = transition_data;
let ToolMessage::Text(event) = event else { let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
return self; .unwrap()
}; .with_alpha(0.05)
.rgba_hex();
let ToolMessage::Text(event) = event else { return self };
match (self, event) { match (self, event) {
(TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => { (TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => {
responses.add(FrontendMessage::DisplayEditableTextboxTransform { responses.add(FrontendMessage::DisplayEditableTextboxTransform {
@@ -430,51 +438,47 @@ impl Fsm for TextToolFsmState {
}); });
if let Some(editing_text) = tool_data.editing_text.as_ref() { if let Some(editing_text) = tool_data.editing_text.as_ref() {
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data)); let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box( let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face, editing_text.typesetting);
&tool_data.new_text,
buzz_face,
editing_text.font_size,
editing_text.line_height_ratio,
editing_text.character_spacing,
None,
);
if far.x != 0. && far.y != 0. { if far.x != 0. && far.y != 0. {
let quad = Quad::from_box([DVec2::ZERO, far]); let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad; let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
overlay_context.quad(transformed_quad, None); overlay_context.quad(transformed_quad, Some(&("#".to_string() + &fill_color)));
} }
} }
TextToolFsmState::Editing TextToolFsmState::Editing
} }
(_, TextToolMessage::Overlays(mut overlay_context)) => { (_, TextToolMessage::Overlays(mut overlay_context)) => {
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { if matches!(self, Self::Placing | Self::Dragging) {
let Some((text, font, font_size, line_height_ratio, character_spacing)) = graph_modification_utils::get_text(layer, &document.network_interface) else { // Get the updated selection box bounds
continue; let quad = Quad::from_box(tool_data.cached_resize_bounds);
};
let buzz_face = font_cache.get(font).map(|data| load_face(data)); // Draw a bounding box on the layers to be selected
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, line_height_ratio, character_spacing, None); for layer in document.intersect_quad_no_artboards(quad, input) {
let quad = Quad::from_box([DVec2::ZERO, far]); overlay_context.quad(
let multiplied = document.metadata().transform_to_viewport(layer) * quad; Quad::from_box(document.metadata().bounding_box_viewport(layer).unwrap_or([DVec2::ZERO; 2])),
overlay_context.quad(multiplied, None); Some(&("#".to_string() + &fill_color)),
);
}
overlay_context.quad(quad, Some(&("#".to_string() + &fill_color)));
} else {
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) {
let Some((text, font, typesetting)) = graph_modification_utils::get_text(layer, &document.network_interface) else {
continue;
};
let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face, typesetting);
let quad = Quad::from_box([DVec2::ZERO, far]);
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
overlay_context.quad(multiplied, None);
}
} }
tool_data.resize.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self self
} }
(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,
line_height_ratio: tool_options.line_height_ratio,
character_spacing: tool_options.character_spacing,
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
color: tool_options.fill.active_color(),
});
tool_data.new_text = String::new();
tool_data.interact(state, input, document, font_cache, responses)
}
(state, TextToolMessage::EditSelected) => { (state, TextToolMessage::EditSelected) => {
if let Some(layer) = can_edit_selected(document) { if let Some(layer) = can_edit_selected(document) {
tool_data.start_editing_layer(layer, state, document, font_cache, responses); tool_data.start_editing_layer(layer, state, document, font_cache, responses);
@@ -484,12 +488,87 @@ impl Fsm for TextToolFsmState {
state state
} }
(state, TextToolMessage::Abort) => { (state, TextToolMessage::Abort) => {
input.mouse.finish_transaction(tool_data.resize.viewport_drag_start(document), responses);
tool_data.resize.cleanup(responses);
if state == TextToolFsmState::Editing { if state == TextToolFsmState::Editing {
tool_data.set_editing(false, font_cache, responses); tool_data.set_editing(false, font_cache, responses);
} }
TextToolFsmState::Ready TextToolFsmState::Ready
} }
(TextToolFsmState::Ready, TextToolMessage::DragStart) => {
tool_data.resize.start(document, input);
tool_data.cached_resize_bounds = [tool_data.resize.viewport_drag_start(document); 2];
TextToolFsmState::Placing
}
(Self::Placing | TextToolFsmState::Dragging, TextToolMessage::PointerMove { center, lock_ratio }) => {
tool_data.cached_resize_bounds = tool_data.resize.calculate_points_ignore_layer(document, input, center, lock_ratio);
responses.add(OverlaysMessage::Draw);
// Auto-panning
let messages = [
TextToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
TextToolMessage::PointerMove { center, lock_ratio }.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
TextToolFsmState::Dragging
}
(_, TextToolMessage::PointerMove { .. }) => {
tool_data.resize.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
responses.add(OverlaysMessage::Draw);
self
}
(TextToolFsmState::Placing | TextToolFsmState::Dragging, TextToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning setup
let _ = tool_data.auto_panning.shift_viewport(input, responses);
TextToolFsmState::Dragging
}
(state, TextToolMessage::PointerOutsideViewport { center, lock_ratio }) => {
// Auto-panning stop
let messages = [
TextToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
TextToolMessage::PointerMove { center, lock_ratio }.into(),
];
tool_data.auto_panning.stop(&messages, responses);
state
}
(TextToolFsmState::Placing | TextToolFsmState::Dragging, TextToolMessage::DragStop) => {
let [start, end] = tool_data.cached_resize_bounds;
let has_dragged = (start - end).length_squared() > DRAG_THRESHOLD * DRAG_THRESHOLD;
// Check if the user has clicked (no dragging) on some existing text
if !has_dragged {
if let Some(clicked_text_layer_path) = TextToolData::check_click(document, input, font_cache) {
tool_data.start_editing_layer(clicked_text_layer_path, self, document, font_cache, responses);
return TextToolFsmState::Editing;
}
}
// Otherwise create some new text
let constraint_size = has_dragged.then_some((start - end).abs());
let editing_text = EditingText {
text: String::new(),
transform: DAffine2::from_translation(start),
typesetting: TypesettingConfig {
font_size: tool_options.font_size,
line_height_ratio: tool_options.line_height_ratio,
max_width: constraint_size.map(|size| size.x),
character_spacing: tool_options.character_spacing,
max_height: constraint_size.map(|size| size.y),
},
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
color: tool_options.fill.active_color(),
};
tool_data.new_text(document, editing_text, font_cache, responses);
TextToolFsmState::Editing
}
(TextToolFsmState::Editing, TextToolMessage::CommitText) => { (TextToolFsmState::Editing, TextToolMessage::CommitText) => {
if tool_data.new_text.is_empty() { if tool_data.new_text.is_empty() {
return tool_data.delete_empty_layer(font_cache, responses); return tool_data.delete_empty_layer(font_cache, responses);
@@ -540,18 +619,31 @@ impl Fsm for TextToolFsmState {
let hint_data = match self { let hint_data = match self {
TextToolFsmState::Ready => HintData(vec![ TextToolFsmState::Ready => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Place Text")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Place Text")]),
HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Place Text Box"),
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Edit Text")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Edit Text")]),
]), ]),
TextToolFsmState::Editing => HintData(vec![HintGroup(vec![ TextToolFsmState::Editing => HintData(vec![HintGroup(vec![
HintInfo::keys([Key::Control, Key::Enter], "").add_mac_keys([Key::Command, Key::Enter]), HintInfo::keys([Key::Control, Key::Enter], "").add_mac_keys([Key::Command, Key::Enter]),
HintInfo::keys([Key::Escape], "Commit Changes").prepend_slash(), HintInfo::keys([Key::Escape], "Commit Changes").prepend_slash(),
])]), ])]),
TextToolFsmState::Placing | TextToolFsmState::Dragging => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]),
]),
}; };
responses.add(FrontendMessage::UpdateInputHints { hint_data }); responses.add(FrontendMessage::UpdateInputHints { hint_data });
} }
fn update_cursor(&self, responses: &mut VecDeque<Message>) { fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }); let cursor = match self {
TextToolFsmState::Dragging => MouseCursorIcon::Crosshair,
_ => MouseCursorIcon::Text,
};
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
} }
} }
+18 -4
View File
@@ -296,6 +296,12 @@
canvasCursor = cursorString; canvasCursor = cursorString;
} }
function preventTextEditingScroll(e: Event) {
if (!(e.target instanceof HTMLElement)) return;
e.target.scrollTop = 0;
e.target.scrollLeft = 0;
}
// Text entry // Text entry
export function triggerTextCommit() { export function triggerTextCommit() {
if (!textInput) return; if (!textInput) return;
@@ -317,8 +323,9 @@
textInput.contentEditable = "true"; textInput.contentEditable = "true";
textInput.style.transformOrigin = "0 0"; textInput.style.transformOrigin = "0 0";
textInput.style.width = displayEditableTextbox.lineWidth ? `${displayEditableTextbox.lineWidth}px` : "max-content"; textInput.style.width = displayEditableTextbox.maxWidth ? `${displayEditableTextbox.maxWidth}px` : "max-content";
textInput.style.height = "auto"; textInput.style.height = displayEditableTextbox.maxHeight ? `${displayEditableTextbox.maxHeight}px` : "auto";
textInput.style.lineHeight = `${displayEditableTextbox.lineHeightRatio}`;
textInput.style.fontSize = `${displayEditableTextbox.fontSize}px`; textInput.style.fontSize = `${displayEditableTextbox.fontSize}px`;
textInput.style.color = displayEditableTextbox.color.toHexOptionalAlpha() || "transparent"; textInput.style.color = displayEditableTextbox.color.toHexOptionalAlpha() || "transparent";
@@ -498,7 +505,7 @@
</svg> </svg>
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}> <div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
{#if showTextInput} {#if showTextInput}
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" /> <div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} />
{/if} {/if}
</div> </div>
<canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas> <canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas>
@@ -721,14 +728,21 @@
} }
} }
.text-input {
word-break: break-all;
}
.text-input div { .text-input div {
cursor: text; cursor: text;
background: none; background: none;
border: none; border: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: visible; overflow-x: visible;
overflow-y: hidden;
overflow-wrap: anywhere;
white-space: pre-wrap; white-space: pre-wrap;
word-break: normal;
display: inline-block; display: inline-block;
// Workaround to force Chrome to display the flashing text entry cursor when text is empty // Workaround to force Chrome to display the flashing text entry cursor when text is empty
padding-left: 1px; padding-left: 1px;
+5 -1
View File
@@ -828,7 +828,7 @@ export class UpdateDocumentLayerStructureJs extends JsMessage {
export class DisplayEditableTextbox extends JsMessage { export class DisplayEditableTextbox extends JsMessage {
readonly text!: string; readonly text!: string;
readonly lineWidth!: undefined | number; readonly lineHeightRatio!: number;
readonly fontSize!: number; readonly fontSize!: number;
@@ -838,6 +838,10 @@ export class DisplayEditableTextbox extends JsMessage {
readonly url!: string; readonly url!: string;
readonly transform!: number[]; readonly transform!: number[];
readonly maxWidth!: undefined | number;
readonly maxHeight!: undefined | number;
} }
export class DisplayEditableTextboxTransform extends JsMessage { export class DisplayEditableTextboxTransform extends JsMessage {
+100 -31
View File
@@ -60,113 +60,152 @@ fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64, line_height_rati
(scale, line_height, buffer) (scale, line_height, buffer)
} }
fn push_str(buffer: &mut UnicodeBuffer, word: &str, trailing_space: bool) { fn push_str(buffer: &mut UnicodeBuffer, word: &str) {
buffer.push_str(word); buffer.push_str(word);
if trailing_space {
buffer.push_str(" ");
}
} }
fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64) -> bool { fn wrap_word(max_width: Option<f64>, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64, space_glyph: Option<GlyphId>) -> bool {
if let Some(line_width) = line_width { if let Some(max_width) = max_width {
let word_length: f64 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance as f64 * character_spacing).sum(); // We don't word wrap spaces (to match the browser)
let all_glyphs = glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos());
let non_space_glyphs = all_glyphs.take_while(|(_, info)| space_glyph != Some(GlyphId(info.glyph_id as u16)));
let word_length: f64 = non_space_glyphs.map(|(pos, _)| pos.x_advance as f64 * character_spacing).sum();
let scaled_word_length = word_length * font_size; let scaled_word_length = word_length * font_size;
if scaled_word_length + x_pos > line_width { if scaled_word_length + x_pos > max_width {
return true; return true;
} }
} }
false false
} }
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option<f64>) -> Vec<Subpath<PointId>> { #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct TypesettingConfig {
pub font_size: f64,
pub line_height_ratio: f64,
pub character_spacing: f64,
pub max_width: Option<f64>,
pub max_height: Option<f64>,
}
impl Default for TypesettingConfig {
fn default() -> Self {
Self {
font_size: 24.,
line_height_ratio: 1.2,
character_spacing: 1.,
max_width: None,
max_height: None,
}
}
}
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
let buzz_face = match buzz_face { let buzz_face = match buzz_face {
Some(face) => face, Some(face) => face,
// Show blank layer if font has not loaded // Show blank layer if font has not loaded
None => return vec![], None => return vec![],
}; };
let space_glyph = buzz_face.glyph_index(' ');
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size, line_height_ratio); let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio);
let mut builder = Builder { let mut builder = Builder {
current_subpath: Subpath::new(Vec::new(), false), current_subpath: Subpath::new(Vec::new(), false),
other_subpaths: Vec::new(), other_subpaths: Vec::new(),
pos: DVec2::ZERO, pos: DVec2::ZERO,
offset: DVec2::ZERO, offset: DVec2::ZERO,
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale, ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * typesetting.font_size / scale,
scale, scale,
id: PointId::ZERO, id: PointId::ZERO,
}; };
for line in str.split('\n') { for line in str.split('\n') {
let length = line.split(' ').count(); for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
for (index, word) in line.split(' ').enumerate() { push_str(&mut buffer, word);
push_str(&mut buffer, word, index != length - 1);
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
if wrap_word(line_width, &glyph_buffer, scale, character_spacing, builder.pos.x) { // Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, builder.pos.x, space_glyph) {
builder.pos = DVec2::new(0., builder.pos.y + line_height); builder.pos = DVec2::new(0., builder.pos.y + line_height);
} }
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) { for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
if let Some(line_width) = line_width { let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale * character_spacing) >= line_width { if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && builder.pos.x + (glyph_position.x_advance as f64 * builder.scale * typesetting.character_spacing) >= max_width {
builder.pos = DVec2::new(0., builder.pos.y + line_height); builder.pos = DVec2::new(0., builder.pos.y + line_height);
} }
} }
// Clip when the height is exceeded
if typesetting.max_height.is_some_and(|max_height| builder.pos.y > max_height) {
return builder.other_subpaths;
}
builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale; builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
buzz_face.outline_glyph(GlyphId(glyph_info.glyph_id as u16), &mut builder); buzz_face.outline_glyph(glyph_id, &mut builder);
if !builder.current_subpath.is_empty() { if !builder.current_subpath.is_empty() {
builder.other_subpaths.push(core::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false))); builder.other_subpaths.push(core::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false)));
} }
builder.pos += DVec2::new(glyph_position.x_advance as f64 * character_spacing, glyph_position.y_advance as f64) * builder.scale; builder.pos += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * builder.scale;
} }
buffer = glyph_buffer.clear(); buffer = glyph_buffer.clear();
} }
builder.pos = DVec2::new(0., builder.pos.y + line_height); builder.pos = DVec2::new(0., builder.pos.y + line_height);
} }
builder.other_subpaths builder.other_subpaths
} }
pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option<f64>) -> DVec2 { pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> DVec2 {
let buzz_face = match buzz_face { let buzz_face = match buzz_face {
Some(face) => face, Some(face) => face,
// Show blank layer if font has not loaded // Show blank layer if font has not loaded
None => return DVec2::ZERO, None => return DVec2::ZERO,
}; };
let space_glyph = buzz_face.glyph_index(' ');
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size, line_height_ratio); let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio);
let mut pos = DVec2::ZERO; let mut pos = DVec2::ZERO;
let mut bounds = DVec2::ZERO; let mut bounds = DVec2::ZERO;
for line in str.split('\n') { for line in str.split('\n') {
let length = line.split(' ').count(); for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
for (index, word) in line.split(' ').enumerate() { push_str(&mut buffer, word);
push_str(&mut buffer, word, index != length - 1);
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
if wrap_word(line_width, &glyph_buffer, scale, character_spacing, pos.x) { // Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, pos.x, space_glyph) {
pos = DVec2::new(0., pos.y + line_height); pos = DVec2::new(0., pos.y + line_height);
} }
for glyph_position in glyph_buffer.glyph_positions() { for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
if let Some(line_width) = line_width { let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if pos.x + (glyph_position.x_advance as f64 * scale * character_spacing) >= line_width { if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && pos.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width {
pos = DVec2::new(0., pos.y + line_height); pos = DVec2::new(0., pos.y + line_height);
} }
} }
pos += DVec2::new(glyph_position.x_advance as f64 * character_spacing, glyph_position.y_advance as f64) * scale; pos += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale;
bounds = bounds.max(pos + DVec2::new(0., line_height));
} }
bounds = bounds.max(pos + DVec2::new(0., line_height));
buffer = glyph_buffer.clear(); buffer = glyph_buffer.clear();
} }
pos = DVec2::new(0., pos.y + line_height); pos = DVec2::new(0., pos.y + line_height);
bounds = bounds.max(pos);
}
if let Some(max_width) = typesetting.max_width {
bounds.x = max_width;
}
if let Some(max_height) = typesetting.max_height {
bounds.y = max_height;
} }
bounds bounds
@@ -175,3 +214,33 @@ pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f6
pub fn load_face(data: &[u8]) -> rustybuzz::Face { pub fn load_face(data: &[u8]) -> rustybuzz::Face {
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed") rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")
} }
struct SplitWordsIncludingSpaces<'a> {
text: &'a str,
start_byte: usize,
}
impl<'a> SplitWordsIncludingSpaces<'a> {
pub fn new(text: &'a str) -> Self {
Self { text, start_byte: 0 }
}
}
impl<'a> Iterator for SplitWordsIncludingSpaces<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let mut eaten_chars = self.text[self.start_byte..].char_indices().skip_while(|(_, c)| *c != ' ').skip_while(|(_, c)| *c == ' ');
let start_byte = self.start_byte;
self.start_byte = eaten_chars.next().map_or(self.text.len(), |(offset, _)| self.start_byte + offset);
(self.start_byte > start_byte).then(|| self.text.get(start_byte..self.start_byte)).flatten()
}
}
#[test]
fn split_words_including_spaces() {
let mut split_words = SplitWordsIncludingSpaces::new("hello world .");
assert_eq!(split_words.next(), Some("hello "));
assert_eq!(split_words.next(), Some("world "));
assert_eq!(split_words.next(), Some("."));
assert_eq!(split_words.next(), None);
}
@@ -120,6 +120,7 @@ tagged_value! {
U64(u64), U64(u64),
#[cfg_attr(feature = "serde", serde(alias = "F32"))] // TODO: Eventually remove this alias (probably starting late 2024) #[cfg_attr(feature = "serde", serde(alias = "F32"))] // TODO: Eventually remove this alias (probably starting late 2024)
F64(f64), F64(f64),
OptionalF64(Option<f64>),
Bool(bool), Bool(bool),
UVec2(UVec2), UVec2(UVec2),
IVec2(IVec2), IVec2(IVec2),
+12 -2
View File
@@ -1,5 +1,5 @@
use graph_craft::wasm_application_io::WasmEditorApi; use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_core::text::TypesettingConfig;
pub use graphene_core::text::{bounding_box, load_face, to_path, Font, FontCache}; pub use graphene_core::text::{bounding_box, load_face, to_path, Font, FontCache};
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
@@ -11,7 +11,17 @@ fn text<'i: 'n>(
#[default(24.)] font_size: f64, #[default(24.)] font_size: f64,
#[default(1.2)] line_height_ratio: f64, #[default(1.2)] line_height_ratio: f64,
#[default(1.)] character_spacing: f64, #[default(1.)] character_spacing: f64,
#[default(None)] max_width: Option<f64>,
#[default(None)] max_height: Option<f64>,
) -> crate::vector::VectorData { ) -> crate::vector::VectorData {
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data)); let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, line_height_ratio, character_spacing, None), false)
let typesetting = TypesettingConfig {
font_size,
line_height_ratio,
character_spacing,
max_width,
max_height,
};
crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, typesetting), false)
} }