diff --git a/.vscode/settings.json b/.vscode/settings.json index 86d88457a0..827bfeca95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -51,6 +51,5 @@ "files.insertFinalNewline": true, "files.associations": { "*.graphite": "json" - }, - "rust-analyzer.checkOnSave": false + } } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index f5482cdeb1..ecf80dc09d 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -26,13 +26,17 @@ pub enum FrontendMessage { }, DisplayEditableTextbox { text: String, - #[serde(rename = "lineWidth")] - line_width: Option, + #[serde(rename = "lineHeightRatio")] + line_height_ratio: f64, #[serde(rename = "fontSize")] font_size: f64, color: Color, url: String, transform: [f64; 6], + #[serde(rename = "maxWidth")] + max_width: Option, + #[serde(rename = "maxHeight")] + max_height: Option, }, DisplayEditableTextboxTransform { transform: [f64; 6], diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 9ff6e41294..f29c0fb2be 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -154,7 +154,10 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Escape); action_dispatch=EyedropperToolMessage::Abort), // // 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(Enter); modifiers=[Accel], action_dispatch=TextToolMessage::CommitText), // diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 5455e5cd87..b99af14b92 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -6,7 +6,7 @@ use crate::messages::prelude::*; use bezier_rs::Subpath; use graph_craft::document::NodeId; 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::style::{Fill, Stroke}; use graphene_core::vector::PointId; @@ -93,9 +93,7 @@ pub enum GraphOperationMessage { id: NodeId, text: String, font: Font, - size: f64, - line_height_ratio: f64, - character_spacing: f64, + typesetting: TypesettingConfig, parent: LayerNodeIdentifier, insert_index: usize, }, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 9fa5becf87..3852b60301 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -7,7 +7,7 @@ use crate::messages::prelude::*; use graph_craft::document::{NodeId, NodeInput}; 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::Color; use graphene_std::vector::convert_usvg_path; @@ -174,15 +174,13 @@ impl MessageHandler> for Gr id, text, font, - size, - line_height_ratio, - character_spacing, + typesetting, parent, insert_index, } => { let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); 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, &[]); responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() }); responses.add(NodeGraphMessage::RunDocumentGraph); @@ -279,7 +277,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, } usvg::Node::Text(text) => { 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)); } } diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 789adec155..495286d8f8 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -9,7 +9,7 @@ use graph_craft::concrete; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; 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::style::{Fill, Stroke}; 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 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(); @@ -187,9 +187,11 @@ impl<'a> ModifyInputsContext<'a> { Some(NodeInput::scope("editor-api")), Some(NodeInput::value(TaggedValue::String(text), false)), Some(NodeInput::value(TaggedValue::Font(font), false)), - Some(NodeInput::value(TaggedValue::F64(size), false)), - Some(NodeInput::value(TaggedValue::F64(line_height_ratio), false)), - Some(NodeInput::value(TaggedValue::F64(character_spacing), false)), + Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)), + Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), 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(); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index a5d8ef5c86..58fec6a867 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -16,7 +16,7 @@ use graph_craft::imaginate_input::ImaginateSamplingMethod; use graph_craft::ProtoNodeIdentifier; use graphene_core::raster::brush_cache::BrushCache; 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::vector::VectorData; use graphene_core::*; @@ -2112,9 +2112,11 @@ fn static_nodes() -> Vec { TaggedValue::Font(Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.into(), graphene_core::consts::DEFAULT_FONT_STYLE.into())), false, ), - NodeInput::value(TaggedValue::F64(24.), false), - NodeInput::value(TaggedValue::F64(1.2), false), - NodeInput::value(TaggedValue::F64(1.), false), + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().font_size), false), + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), 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() }, @@ -2126,6 +2128,8 @@ fn static_nodes() -> Vec { "Size".to_string(), "Line Height".to_string(), "Character Spacing".to_string(), + "Max Width".to_string(), + "Max Height".to_string(), ], output_names: vec!["Vector".to_string()], ..Default::default() diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 1ca81ace6a..871353ab33 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -661,6 +661,27 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na .on_commit(commit_value) .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 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 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 }]; 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: line_height_ratio }); result.push(LayoutGroup::Row { widgets: character_spacing }); + result.push(LayoutGroup::Row { widgets: max_width }); + result.push(LayoutGroup::Row { widgets: max_height }); result } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index cb0b15dff6..9bde14ea78 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -32,17 +32,44 @@ impl core::hash::Hash for OverlayContext { impl OverlayContext { pub fn quad(&mut self, quad: Quad, color_fill: Option<&str>) { + self.dashed_quad(quad, color_fill, None, None); + } + + pub fn dashed_quad(&mut self, quad: Quad, color_fill: Option<&str>, dash_width: Option, gap_width: Option) { + // 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(); + array.push(&JsValue::from(dash_width - 1.)); + array.push(&JsValue::from(gap_width)); + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + 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>) { @@ -50,8 +77,7 @@ impl OverlayContext { } pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option, gap_width: Option) { - let start = start.round() - DVec2::splat(0.5); - let end = end.round() - DVec2::splat(0.5); + // 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(); @@ -61,24 +87,24 @@ impl OverlayContext { .set_line_dash(&JsValue::from(array)) .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) .ok(); - } else { - let array = js_sys::Array::new(); - self.render_context - .set_line_dash(&JsValue::from(array)) - .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) - .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.move_to(start.x, start.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.stroke(); - // Reset the dash pattern to solid after drawing - self.render_context - .set_line_dash(&JsValue::from(js_sys::Array::new())) - .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) - .ok(); + // 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 manipulator_handle(&mut self, position: DVec2, selected: bool) { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index c658fb1de4..3e01f1556c 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -15,7 +15,7 @@ use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use graph_craft::document::value::TaggedValue; 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 interpreted_executor::dynamic_executor::IntrospectError; @@ -530,7 +530,7 @@ impl MessageHandler> 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 - if reference == "Text" && inputs_count == 4 { + if reference == "Text" && inputs_count != 8 { let node_definition = resolve_document_node_type(reference).unwrap(); let document_node = node_definition.default_node_template().document_node; document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone()); @@ -541,12 +541,34 @@ impl MessageHandler> 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, 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, 4), NodeInput::value(TaggedValue::F64(1.), false), &[]); - document - .network_interface - .set_input(&InputConnector::node(*node_id, 5), NodeInput::value(TaggedValue::F64(1.), false), &[]); + document.network_interface.set_input( + &InputConnector::node(*node_id, 4), + if inputs_count == 6 { + old_inputs[4].clone() + } else { + 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 diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index de134e5ab5..1b3a13aaed 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -4,7 +4,7 @@ use crate::messages::prelude::*; use bezier_rs::Subpath; use graph_craft::document::{value::TaggedValue, NodeId, NodeInput}; 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::PointId; use graphene_core::Color; @@ -127,7 +127,7 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn } /// 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 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(line_height_ratio)) = inputs[4].as_value() else { return None }; let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None }; - - Some((text, font, font_size, line_height_ratio, character_spacing)) + 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 }; + + 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 { diff --git a/editor/src/messages/tool/common_functionality/resize.rs b/editor/src/messages/tool/common_functionality/resize.rs index fe4c2218d8..a593144aaf 100644 --- a/editor/src/messages/tool/common_functionality/resize.rs +++ b/editor/src/messages/tool/common_functionality/resize.rs @@ -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::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 glam::{DAffine2, DVec2, Vec2Swizzles}; -use super::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration}; - #[derive(Clone, Debug, Default)] 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, pub snap_manager: SnapManager, } @@ -29,6 +27,8 @@ impl Resize { 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]> { let layer = self.layer?; @@ -41,7 +41,12 @@ impl Resize { self.layer.take(); 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 mouse = input.mouse.position; 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); } - Some(points_viewport) + points_viewport } pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option { diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index a6406730d1..dbedeae347 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -112,7 +112,7 @@ struct ArtboardToolData { drag_current: DVec2, auto_panning: AutoPanning, snap_candidates: Vec, - dragging_current_artboad_location: IVec2, + dragging_current_artboard_location: IVec2, } impl ArtboardToolData { @@ -140,7 +140,7 @@ impl ArtboardToolData { fn start_resizing(&mut self, _selected_edges: (bool, bool, bool, bool), _document: &DocumentMessageHandler, _input: &InputPreprocessorMessageHandler) { if let Some(bounds) = &mut self.bounding_box_manager { 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(), }); - let translation = position.round().as_ivec2() - self.dragging_current_artboad_location; - self.dragging_current_artboad_location = position.round().as_ivec2(); - for child in self.selected_artboard.unwrap().children(&document.metadata()) { + let translation = position.round().as_ivec2() - self.dragging_current_artboard_location; + self.dragging_current_artboard_location = position.round().as_ivec2(); + 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()); responses.add(GraphOperationMessage::TransformChange { 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) -> Self { 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 ToolMessage::Artboard(event) = event else { return self }; match (self, event) { (state, ArtboardToolMessage::Overlays(mut overlay_context)) => { if state != ArtboardToolFsmState::Drawing { diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 6b86a36064..2640d96d6b 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -326,9 +326,7 @@ impl Fsm for BrushToolFsmState { responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning)); } - let ToolMessage::Brush(event) = event else { - return self; - }; + let ToolMessage::Brush(event) = event else { return self }; match (self, event) { (BrushToolFsmState::Ready, BrushToolMessage::DragStart) => { responses.add(DocumentMessage::StartTransaction); diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index 801563a9c0..d694941d4d 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -188,9 +188,7 @@ impl Fsm for EllipseToolFsmState { let shape_data = &mut tool_data.data; - let ToolMessage::Ellipse(event) = event else { - return self; - }; + let ToolMessage::Ellipse(event) = event else { return self }; match (self, event) { (_, EllipseToolMessage::Overlays(mut overlay_context)) => { shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index da52a95ead..1f815e8b4e 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -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) -> Self { let ToolActionHandlerData { global_tool_data, input, .. } = tool_action_data; - let ToolMessage::Eyedropper(event) = event else { - return self; - }; + let ToolMessage::Eyedropper(event) = event else { return self }; match (self, event) { // Ready -> Sampling (EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index e56c934692..9600883d90 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -81,10 +81,7 @@ impl Fsm for FillToolFsmState { document, global_tool_data, input, .. } = handler_data; - let ToolMessage::Fill(event) = event else { - return self; - }; - + let ToolMessage::Fill(event) = event else { return self }; match (self, event) { (FillToolFsmState::Ready, color_event) => { let Some(layer_identifier) = document.click(input) else { diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index 7f31bcec3e..9fd3ca09ee 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -194,9 +194,7 @@ impl Fsm for FreehandToolFsmState { .. } = tool_action_data; - let ToolMessage::Freehand(event) = event else { - return self; - }; + let ToolMessage::Freehand(event) = event else { return self }; match (self, event) { (_, FreehandToolMessage::Overlays(mut overlay_context)) => { path_endpoint_overlays(document, shape_editor, &mut overlay_context); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 0cdcdd4051..9e45a30c6e 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -241,10 +241,7 @@ impl Fsm for GradientToolFsmState { document, global_tool_data, input, .. } = tool_action_data; - let ToolMessage::Gradient(event) = event else { - return self; - }; - + let ToolMessage::Gradient(event) = event else { return self }; match (self, event) { (_, GradientToolMessage::Overlays(mut overlay_context)) => { let selected = tool_data.selected_gradient.as_ref(); diff --git a/editor/src/messages/tool/tool_messages/imaginate_tool.rs b/editor/src/messages/tool/tool_messages/imaginate_tool.rs index d0462829fd..79a952865d 100644 --- a/editor/src/messages/tool/tool_messages/imaginate_tool.rs +++ b/editor/src/messages/tool/tool_messages/imaginate_tool.rs @@ -91,9 +91,7 @@ impl Fsm for ImaginateToolFsmState { ) -> Self { let shape_data = &mut tool_data.data; - let ToolMessage::Imaginate(event) = event else { - return self; - }; + let ToolMessage::Imaginate(event) = event else { return self }; match (self, event) { (ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => { shape_data.start(document, input); diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs index 437820d676..76cf518d92 100644 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ b/editor/src/messages/tool/tool_messages/line_tool.rs @@ -163,9 +163,7 @@ impl Fsm for LineToolFsmState { document, global_tool_data, input, .. } = tool_action_data; - let ToolMessage::Line(event) = event else { - return self; - }; + let ToolMessage::Line(event) = event else { return self }; match (self, event) { (_, LineToolMessage::Overlays(mut overlay_context)) => { tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); diff --git a/editor/src/messages/tool/tool_messages/navigate_tool.rs b/editor/src/messages/tool/tool_messages/navigate_tool.rs index e6aaa1ffbd..daf4c146e3 100644 --- a/editor/src/messages/tool/tool_messages/navigate_tool.rs +++ b/editor/src/messages/tool/tool_messages/navigate_tool.rs @@ -95,10 +95,7 @@ impl Fsm for NavigateToolFsmState { _tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { - let ToolMessage::Navigate(navigate) = message else { - return self; - }; - + let ToolMessage::Navigate(navigate) = message else { return self }; match navigate { NavigateToolMessage::PointerUp { zoom_in } => { if self == NavigateToolFsmState::ZoomOrClickZooming { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 2b6da0ecf3..3bb5cc763f 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -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) -> Self { let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data; - let ToolMessage::Path(event) = event else { - return self; - }; - + let ToolMessage::Path(event) = event else { return self }; match (self, event) { (_, PathToolMessage::SelectionChanged) => { // Set the newly targeted layers to visible diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index e35ed2b0a9..ca601ec012 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -550,9 +550,7 @@ impl Fsm for PenToolFsmState { transform = DAffine2::IDENTITY; } - let ToolMessage::Pen(event) = event else { - return self; - }; + let ToolMessage::Pen(event) = event else { return self }; match (self, event) { (_, PenToolMessage::SelectionChanged) => { responses.add(OverlaysMessage::Draw); diff --git a/editor/src/messages/tool/tool_messages/polygon_tool.rs b/editor/src/messages/tool/tool_messages/polygon_tool.rs index e35f5ab990..90fc61a843 100644 --- a/editor/src/messages/tool/tool_messages/polygon_tool.rs +++ b/editor/src/messages/tool/tool_messages/polygon_tool.rs @@ -234,9 +234,7 @@ impl Fsm for PolygonToolFsmState { let polygon_data = &mut tool_data.data; - let ToolMessage::Polygon(event) = event else { - return self; - }; + let ToolMessage::Polygon(event) = event else { return self }; match (self, event) { (_, PolygonToolMessage::Overlays(mut overlay_context)) => { polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs index fd36a0fceb..80e167b477 100644 --- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs +++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs @@ -102,7 +102,6 @@ impl<'a> MessageHandler> for Rectang self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); return; }; - match action { RectangleOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; @@ -193,10 +192,7 @@ impl Fsm for RectangleToolFsmState { ) -> Self { let shape_data = &mut tool_data.data; - let ToolMessage::Rectangle(event) = event else { - return self; - }; - + let ToolMessage::Rectangle(event) = event else { return self }; match (self, event) { (_, RectangleToolMessage::Overlays(mut overlay_context)) => { shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 9783aebe69..f459fa9f9f 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -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::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; 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::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; 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 graphene_core::renderer::Quad; +use graphene_core::text::load_face; use graphene_std::renderer::Rect; use graphene_std::vector::misc::BooleanOperation; @@ -402,11 +403,9 @@ impl Fsm for SelectToolFsmState { type ToolOptions = (); fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque) -> Self { - let ToolActionHandlerData { document, input, .. } = tool_action_data; + let ToolActionHandlerData { document, input, font_cache, .. } = tool_action_data; - let ToolMessage::Select(event) = event else { - return self; - }; + let ToolMessage::Select(event) = event else { return self }; match (self, event) { (_, SelectToolMessage::Overlays(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(), &[])) { 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 diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 87acb17196..b680d81e06 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -194,9 +194,7 @@ impl Fsm for SplineToolFsmState { document, global_tool_data, input, .. } = tool_action_data; - let ToolMessage::Spline(event) = event else { - return self; - }; + let ToolMessage::Spline(event) = event else { return self }; match (self, event) { (_, SplineToolMessage::CanvasTransformed) => self, (SplineToolFsmState::Ready, SplineToolMessage::DragStart) => { diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 2a4c5cba21..52dd657771 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -1,17 +1,20 @@ #![allow(clippy::too_many_arguments)] use super::tool_prelude::*; +use crate::consts::DRAG_THRESHOLD; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; 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::network_interface::InputConnector; 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::snapping::SnapData; +use crate::messages::tool::common_functionality::{auto_panning::AutoPanning, resize::Resize}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; 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::Color; @@ -54,8 +57,12 @@ pub enum TextToolMessage { // Tool-specific messages CommitText, + DragStart, + DragStop, EditSelected, Interact, + PointerMove { center: Key, lock_ratio: Key }, + PointerOutsideViewport { center: Key, lock_ratio: Key }, TextChange { new_text: String, is_right_click: bool }, UpdateBounds { new_text: String }, UpdateOptions(TextOptionsUpdate), @@ -122,7 +129,7 @@ fn create_text_widgets(tool: &TextTool) -> Vec { .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap())).into()) .widget_holder(); let character_spacing = NumberInput::new(Some(tool.options.character_spacing)) - .label("Character Spacing") + .label("Char. Spacing") .int() .min(0.) .max((1_u64 << f64::MANTISSA_DIGITS) as f64) @@ -193,13 +200,19 @@ impl<'a> MessageHandler> for TextToo fn actions(&self) -> ActionList { match self.fsm_state { TextToolFsmState::Ready => actions!(TextToolMessageDiscriminant; - Interact, + DragStart, + PointerMove, ), TextToolFsmState::Editing => actions!(TextToolMessageDiscriminant; - Interact, + DragStart, Abort, 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)] enum TextToolFsmState { + /// The tool is ready to place or edit text. #[default] Ready, + /// The user is typing in the interactive viewport text area. 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)] pub struct EditingText { text: String, font: Font, - font_size: f64, - line_height_ratio: f64, - character_spacing: f64, + typesetting: TypesettingConfig, color: Option, transform: DAffine2, } @@ -238,6 +256,10 @@ struct TextToolData { layer: LayerNodeIdentifier, editing_text: Option, 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 { @@ -259,11 +281,13 @@ impl TextToolData { 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, + line_height_ratio: editing_text.typesetting.line_height_ratio, + font_size: editing_text.typesetting.font_size, color: editing_text.color.unwrap_or(Color::BLACK), url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(), transform: editing_text.transform.to_cols_array(), + max_width: editing_text.typesetting.max_width, + max_height: editing_text.typesetting.max_height, }); } else { // 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<()> { 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 (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 { text: text.clone(), font: font.clone(), - font_size, - line_height_ratio, - character_spacing, + typesetting, color: Some(color), transform, }); @@ -318,76 +340,59 @@ impl TextToolData { }; } - fn interact( - &mut self, - state: TextToolFsmState, - input: &InputPreprocessorMessageHandler, - document: &DocumentMessageHandler, - font_cache: &FontCache, - responses: &mut VecDeque, - ) -> TextToolFsmState { - // Check if the user has selected an existing text layer - if let Some(clicked_text_layer_path) = document + fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, font_cache: &FontCache, responses: &mut VecDeque) { + // Create new text + self.new_text = String::new(); + 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(), + 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 { + document .metadata() .all_layers() .filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text")) .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()`"); 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 transformed_quad = document.metadata().transform_to_viewport(layer) * quad; let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y); 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, .. } = transition_data; - let ToolMessage::Text(event) = event else { - return self; - }; + let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + .unwrap() + .with_alpha(0.05) + .rgba_hex(); + + let ToolMessage::Text(event) = event else { return self }; match (self, event) { (TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => { responses.add(FrontendMessage::DisplayEditableTextboxTransform { @@ -430,51 +438,47 @@ impl Fsm for TextToolFsmState { }); 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 far = graphene_core::text::bounding_box( - &tool_data.new_text, - buzz_face, - editing_text.font_size, - editing_text.line_height_ratio, - editing_text.character_spacing, - None, - ); + let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face, editing_text.typesetting); if far.x != 0. && far.y != 0. { let quad = Quad::from_box([DVec2::ZERO, far]); 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 } (_, TextToolMessage::Overlays(mut overlay_context)) => { - for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { - let Some((text, font, font_size, line_height_ratio, character_spacing)) = 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, font_size, line_height_ratio, character_spacing, None); - let quad = Quad::from_box([DVec2::ZERO, far]); - let multiplied = document.metadata().transform_to_viewport(layer) * quad; - overlay_context.quad(multiplied, None); + if matches!(self, Self::Placing | Self::Dragging) { + // Get the updated selection box bounds + let quad = Quad::from_box(tool_data.cached_resize_bounds); + + // Draw a bounding box on the layers to be selected + for layer in document.intersect_quad_no_artboards(quad, input) { + overlay_context.quad( + Quad::from_box(document.metadata().bounding_box_viewport(layer).unwrap_or([DVec2::ZERO; 2])), + 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 } - (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) => { if let Some(layer) = can_edit_selected(document) { tool_data.start_editing_layer(layer, state, document, font_cache, responses); @@ -484,12 +488,87 @@ impl Fsm for TextToolFsmState { state } (state, TextToolMessage::Abort) => { + input.mouse.finish_transaction(tool_data.resize.viewport_drag_start(document), responses); + tool_data.resize.cleanup(responses); + if state == TextToolFsmState::Editing { tool_data.set_editing(false, font_cache, responses); } 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) => { if tool_data.new_text.is_empty() { return tool_data.delete_empty_layer(font_cache, responses); @@ -540,18 +619,31 @@ impl Fsm for TextToolFsmState { let hint_data = match self { TextToolFsmState::Ready => HintData(vec![ 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")]), ]), TextToolFsmState::Editing => HintData(vec![HintGroup(vec![ HintInfo::keys([Key::Control, Key::Enter], "").add_mac_keys([Key::Command, Key::Enter]), 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 }); } fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }); + let cursor = match self { + TextToolFsmState::Dragging => MouseCursorIcon::Crosshair, + _ => MouseCursorIcon::Text, + }; + responses.add(FrontendMessage::UpdateMouseCursor { cursor }); } } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 64433a38e7..1301cefb7a 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -296,6 +296,12 @@ canvasCursor = cursorString; } + function preventTextEditingScroll(e: Event) { + if (!(e.target instanceof HTMLElement)) return; + e.target.scrollTop = 0; + e.target.scrollLeft = 0; + } + // Text entry export function triggerTextCommit() { if (!textInput) return; @@ -317,8 +323,9 @@ textInput.contentEditable = "true"; textInput.style.transformOrigin = "0 0"; - textInput.style.width = displayEditableTextbox.lineWidth ? `${displayEditableTextbox.lineWidth}px` : "max-content"; - textInput.style.height = "auto"; + textInput.style.width = displayEditableTextbox.maxWidth ? `${displayEditableTextbox.maxWidth}px` : "max-content"; + textInput.style.height = displayEditableTextbox.maxHeight ? `${displayEditableTextbox.maxHeight}px` : "auto"; + textInput.style.lineHeight = `${displayEditableTextbox.lineHeightRatio}`; textInput.style.fontSize = `${displayEditableTextbox.fontSize}px`; textInput.style.color = displayEditableTextbox.color.toHexOptionalAlpha() || "transparent"; @@ -498,7 +505,7 @@
{#if showTextInput} -
+
{/if}
@@ -721,14 +728,21 @@ } } + .text-input { + word-break: break-all; + } + .text-input div { cursor: text; background: none; border: none; margin: 0; padding: 0; - overflow: visible; + overflow-x: visible; + overflow-y: hidden; + overflow-wrap: anywhere; white-space: pre-wrap; + word-break: normal; display: inline-block; // Workaround to force Chrome to display the flashing text entry cursor when text is empty padding-left: 1px; diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index feba002f3b..52aab6895b 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -828,7 +828,7 @@ export class UpdateDocumentLayerStructureJs extends JsMessage { export class DisplayEditableTextbox extends JsMessage { readonly text!: string; - readonly lineWidth!: undefined | number; + readonly lineHeightRatio!: number; readonly fontSize!: number; @@ -838,6 +838,10 @@ export class DisplayEditableTextbox extends JsMessage { readonly url!: string; readonly transform!: number[]; + + readonly maxWidth!: undefined | number; + + readonly maxHeight!: undefined | number; } export class DisplayEditableTextboxTransform extends JsMessage { diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index fcfe37d595..4802e649dc 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -60,113 +60,152 @@ fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64, line_height_rati (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); - - if trailing_space { - buffer.push_str(" "); - } } -fn wrap_word(line_width: Option, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64) -> bool { - if let Some(line_width) = line_width { - let word_length: f64 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance as f64 * character_spacing).sum(); +fn wrap_word(max_width: Option, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64, space_glyph: Option) -> bool { + if let Some(max_width) = max_width { + // 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; - if scaled_word_length + x_pos > line_width { + if scaled_word_length + x_pos > max_width { return true; } } false } -pub fn to_path(str: &str, buzz_face: Option, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option) -> Vec> { +#[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, + pub max_height: Option, +} + +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, typesetting: TypesettingConfig) -> Vec> { let buzz_face = match buzz_face { Some(face) => face, // Show blank layer if font has not loaded 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 { current_subpath: Subpath::new(Vec::new(), false), other_subpaths: Vec::new(), pos: 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, id: PointId::ZERO, }; for line in str.split('\n') { - let length = line.split(' ').count(); - for (index, word) in line.split(' ').enumerate() { - push_str(&mut buffer, word, index != length - 1); + for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() { + push_str(&mut buffer, word); 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); } for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) { - if let Some(line_width) = line_width { - if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale * character_spacing) >= line_width { + let glyph_id = GlyphId(glyph_info.glyph_id as u16); + 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); } } + // 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; - 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() { 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(); } + builder.pos = DVec2::new(0., builder.pos.y + line_height); } + builder.other_subpaths } -pub fn bounding_box(str: &str, buzz_face: Option, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option) -> DVec2 { +pub fn bounding_box(str: &str, buzz_face: Option, typesetting: TypesettingConfig) -> DVec2 { let buzz_face = match buzz_face { Some(face) => face, // Show blank layer if font has not loaded 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 bounds = DVec2::ZERO; for line in str.split('\n') { - let length = line.split(' ').count(); - for (index, word) in line.split(' ').enumerate() { - push_str(&mut buffer, word, index != length - 1); + for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() { + push_str(&mut buffer, word); 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); } - for glyph_position in glyph_buffer.glyph_positions() { - if let Some(line_width) = line_width { - if pos.x + (glyph_position.x_advance as f64 * scale * character_spacing) >= line_width { + for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) { + let glyph_id = GlyphId(glyph_info.glyph_id as u16); + 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(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(); } 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 @@ -175,3 +214,33 @@ pub fn bounding_box(str: &str, buzz_face: Option, font_size: f6 pub fn load_face(data: &[u8]) -> rustybuzz::Face { 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 { + 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); +} diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 6ff0866b3d..812156850f 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -120,6 +120,7 @@ tagged_value! { U64(u64), #[cfg_attr(feature = "serde", serde(alias = "F32"))] // TODO: Eventually remove this alias (probably starting late 2024) F64(f64), + OptionalF64(Option), Bool(bool), UVec2(UVec2), IVec2(IVec2), diff --git a/node-graph/gstd/src/text.rs b/node-graph/gstd/src/text.rs index 32f8db7f81..dea27dec2e 100644 --- a/node-graph/gstd/src/text.rs +++ b/node-graph/gstd/src/text.rs @@ -1,5 +1,5 @@ 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}; #[node_macro::node(category(""))] @@ -11,7 +11,17 @@ fn text<'i: 'n>( #[default(24.)] font_size: f64, #[default(1.2)] line_height_ratio: f64, #[default(1.)] character_spacing: f64, + #[default(None)] max_width: Option, + #[default(None)] max_height: Option, ) -> crate::vector::VectorData { 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) }