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 d2eeeb3b37..5455e5cd87 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 @@ -94,6 +94,8 @@ pub enum GraphOperationMessage { text: String, font: Font, size: f64, + line_height_ratio: f64, + character_spacing: f64, 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 5aab007a42..d84c7c0cf5 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 @@ -182,12 +182,14 @@ impl MessageHandler> for Gr text, font, size, + line_height_ratio, + character_spacing, 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, layer); + modify_inputs.insert_text(text, font, size, line_height_ratio, character_spacing, layer); network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() }); responses.add(NodeGraphMessage::RunDocumentGraph); @@ -284,7 +286,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., layer); + modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, 24., 1.2, 1., 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 870750b99f..9d71bbd72e 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -177,7 +177,7 @@ impl<'a> ModifyInputsContext<'a> { } } - pub fn insert_text(&mut self, text: String, font: Font, size: f64, layer: LayerNodeIdentifier) { + pub fn insert_text(&mut self, text: String, font: Font, size: f64, line_height_ratio: f64, character_spacing: f64, 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(); @@ -186,6 +186,8 @@ impl<'a> ModifyInputsContext<'a> { 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)), ]); let text_id = NodeId(generate_uuid()); 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 4a49d9ef0d..4fc10e0e15 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 @@ -2056,11 +2056,20 @@ fn static_nodes() -> Vec { false, ), NodeInput::value(TaggedValue::F64(24.), false), + NodeInput::value(TaggedValue::F64(1.2), false), + NodeInput::value(TaggedValue::F64(1.), false), ], ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Editor API".to_string(), "Text".to_string(), "Font".to_string(), "Size".to_string()], + input_names: vec![ + "Editor API".to_string(), + "Text".to_string(), + "Font".to_string(), + "Size".to_string(), + "Line Height".to_string(), + "Character Spacing".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 222999ec42..080402625e 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1731,12 +1731,16 @@ pub(crate) fn text_properties(document_node: &DocumentNode, node_id: NodeId, _co let text = text_area_widget(document_node, node_id, 1, "Text", true); let (font, style) = font_inputs(document_node, node_id, 2, "Font", 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 character_spacing = number_widget(document_node, node_id, 5, "Character Spacing", NumberInput::default().min(0.).step(0.1), true); let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }]; if let Some(style) = style { result.push(LayoutGroup::Row { widgets: style }); } result.push(LayoutGroup::Row { widgets: size }); + result.push(LayoutGroup::Row { widgets: line_height_ratio }); + result.push(LayoutGroup::Row { widgets: character_spacing }); result } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index b83fb90360..86140428ab 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -471,8 +471,9 @@ impl MessageHandler> for PortfolioMes log::error!("could not get node in deserialize_document"); continue; }; + let inputs_count = node.inputs.len(); - if reference == "Fill" && node.inputs.len() == 8 { + if reference == "Fill" && 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()); @@ -529,6 +530,26 @@ 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 { + 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()); + + let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), &[]); + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].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, 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), &[]); + } + // Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 if reference == "Merge" || reference == "Artboard" { let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap(); 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 69dbc4136c..de134e5ab5 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -127,14 +127,16 @@ 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)> { +pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, f64, f64, f64)> { 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::Font(font)) = &inputs[2].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(character_spacing)) = inputs[5].as_value() else { return None }; - Some((text, font, font_size)) + Some((text, font, font_size, line_height_ratio, character_spacing)) } pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index ab0540d834..207b74d829 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -24,7 +24,9 @@ pub struct TextTool { } pub struct TextOptions { - font_size: u32, + font_size: f64, + line_height_ratio: f64, + character_spacing: f64, font_name: String, font_style: String, fill: ToolColorOptions, @@ -33,7 +35,9 @@ pub struct TextOptions { impl Default for TextOptions { fn default() -> Self { Self { - font_size: 24, + font_size: 24., + line_height_ratio: 1.2, + character_spacing: 1., font_name: graphene_core::consts::DEFAULT_FONT_FAMILY.into(), font_style: graphene_core::consts::DEFAULT_FONT_STYLE.into(), fill: ToolColorOptions::new_primary(), @@ -63,7 +67,9 @@ pub enum TextOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), Font { family: String, style: String }, - FontSize(u32), + FontSize(f64), + LineHeightRatio(f64), + CharacterSpacing(f64), WorkingColors(Option, Option), } @@ -100,13 +106,29 @@ fn create_text_widgets(tool: &TextTool) -> Vec { .into() }) .widget_holder(); - let size = NumberInput::new(Some(tool.options.font_size as f64)) + let size = NumberInput::new(Some(tool.options.font_size)) .unit(" px") .label("Size") .int() .min(1.) .max((1_u64 << f64::MANTISSA_DIGITS) as f64) - .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()) + .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap())).into()) + .widget_holder(); + let line_height_ratio = NumberInput::new(Some(tool.options.line_height_ratio)) + .label("Line Height") + .int() + .min(0.) + .max((1_u64 << f64::MANTISSA_DIGITS) as f64) + .step(0.1) + .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") + .int() + .min(0.) + .max((1_u64 << f64::MANTISSA_DIGITS) as f64) + .step(0.1) + .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::CharacterSpacing(number_input.value.unwrap())).into()) .widget_holder(); vec![ font, @@ -114,6 +136,10 @@ fn create_text_widgets(tool: &TextTool) -> Vec { style, Separator::new(SeparatorType::Related).widget_holder(), size, + Separator::new(SeparatorType::Related).widget_holder(), + line_height_ratio, + Separator::new(SeparatorType::Related).widget_holder(), + character_spacing, ] } @@ -149,6 +175,8 @@ impl<'a> MessageHandler> for TextToo self.send_layout(responses, LayoutTarget::ToolOptions); } TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size, + TextOptionsUpdate::LineHeightRatio(line_height_ratio) => self.options.line_height_ratio = line_height_ratio, + TextOptionsUpdate::CharacterSpacing(character_spacing) => self.options.character_spacing = character_spacing, TextOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; self.options.fill.color_type = ToolColorType::Custom; @@ -200,6 +228,8 @@ pub struct EditingText { text: String, font: Font, font_size: f64, + line_height_ratio: f64, + character_spacing: f64, color: Option, transform: DAffine2, } @@ -233,11 +263,13 @@ 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) = graph_modification_utils::get_text(self.layer, &document.network_interface)?; + let (text, font, font_size, line_height_ratio, character_spacing) = 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, color: Some(color), transform, }); @@ -295,6 +327,8 @@ impl TextToolData { 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_parent(true), insert_index: 0, }); @@ -364,7 +398,14 @@ 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, None); + 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, + ); 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; @@ -376,11 +417,11 @@ impl Fsm for TextToolFsmState { } (_, TextToolMessage::Overlays(mut overlay_context)) => { for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { - let Some((text, font, font_size)) = graph_modification_utils::get_text(layer, &document.network_interface) else { + 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, None); + 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); @@ -392,7 +433,9 @@ impl Fsm for TextToolFsmState { tool_data.editing_text = Some(EditingText { text: String::new(), transform: DAffine2::from_translation(input.mouse.position), - font_size: tool_options.font_size as f64, + font_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(), }); diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index 9a3b3282b6..fcfe37d595 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -53,9 +53,9 @@ impl OutlineBuilder for Builder { } } -fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64) -> (f64, f64, UnicodeBuffer) { +fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64, line_height_ratio: f64) -> (f64, f64, UnicodeBuffer) { let scale = (buzz_face.units_per_em() as f64).recip() * font_size; - let line_height = font_size; + let line_height = font_size * line_height_ratio; let buffer = UnicodeBuffer::new(); (scale, line_height, buffer) } @@ -68,10 +68,10 @@ fn push_str(buffer: &mut UnicodeBuffer, word: &str, trailing_space: bool) { } } -fn wrap_word(line_width: Option, glyph_buffer: &GlyphBuffer, scale: f64, x_pos: f64) -> bool { +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: i32 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance).sum(); - let scaled_word_length = word_length as f64 * scale; + let word_length: f64 = glyph_buffer.glyph_positions().iter().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 { return true; @@ -80,14 +80,14 @@ fn wrap_word(line_width: Option, glyph_buffer: &GlyphBuffer, scale: f64, x_ false } -pub fn to_path(str: &str, buzz_face: Option, font_size: f64, line_width: Option) -> Vec> { +pub fn to_path(str: &str, buzz_face: Option, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option) -> Vec> { let buzz_face = match buzz_face { Some(face) => face, // Show blank layer if font has not loaded None => return vec![], }; - let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size); + let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size, line_height_ratio); let mut builder = Builder { current_subpath: Subpath::new(Vec::new(), false), @@ -105,13 +105,13 @@ pub fn to_path(str: &str, buzz_face: Option, font_size: f64, li push_str(&mut buffer, word, index != length - 1); let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); - if wrap_word(line_width, &glyph_buffer, scale, builder.pos.x) { + if wrap_word(line_width, &glyph_buffer, scale, character_spacing, builder.pos.x) { 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) >= line_width { + if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale * character_spacing) >= line_width { builder.pos = DVec2::new(0., builder.pos.y + line_height); } } @@ -121,7 +121,7 @@ pub fn to_path(str: &str, buzz_face: Option, font_size: f64, li 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, glyph_position.y_advance as f64) * builder.scale; + builder.pos += DVec2::new(glyph_position.x_advance as f64 * character_spacing, glyph_position.y_advance as f64) * builder.scale; } buffer = glyph_buffer.clear(); @@ -131,14 +131,14 @@ pub fn to_path(str: &str, buzz_face: Option, font_size: f64, li builder.other_subpaths } -pub fn bounding_box(str: &str, buzz_face: Option, font_size: f64, line_width: Option) -> DVec2 { +pub fn bounding_box(str: &str, buzz_face: Option, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option) -> DVec2 { let buzz_face = match buzz_face { Some(face) => face, // Show blank layer if font has not loaded None => return DVec2::ZERO, }; - let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size); + let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size, line_height_ratio); let mut pos = DVec2::ZERO; let mut bounds = DVec2::ZERO; @@ -150,17 +150,17 @@ pub fn bounding_box(str: &str, buzz_face: Option, font_size: f6 let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); - if wrap_word(line_width, &glyph_buffer, scale, pos.x) { + if wrap_word(line_width, &glyph_buffer, scale, character_spacing, pos.x) { 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) >= line_width { + if pos.x + (glyph_position.x_advance as f64 * scale * character_spacing) >= line_width { pos = DVec2::new(0., pos.y + line_height); } } - pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * scale; + pos += DVec2::new(glyph_position.x_advance as f64 * character_spacing, glyph_position.y_advance as f64) * scale; } bounds = bounds.max(pos + DVec2::new(0., line_height)); diff --git a/node-graph/gstd/src/text.rs b/node-graph/gstd/src/text.rs index e2e320ce59..32f8db7f81 100644 --- a/node-graph/gstd/src/text.rs +++ b/node-graph/gstd/src/text.rs @@ -3,7 +3,15 @@ use graph_craft::wasm_application_io::WasmEditorApi; pub use graphene_core::text::{bounding_box, load_face, to_path, Font, FontCache}; #[node_macro::node(category(""))] -fn text<'i: 'n>(_: (), editor: &'i WasmEditorApi, text: String, font_name: Font, #[default(24)] font_size: f64) -> crate::vector::VectorData { +fn text<'i: 'n>( + _: (), + editor: &'i WasmEditorApi, + text: String, + font_name: Font, + #[default(24.)] font_size: f64, + #[default(1.2)] line_height_ratio: f64, + #[default(1.)] character_spacing: f64, +) -> 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, None), false) + crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, line_height_ratio, character_spacing, None), false) }