Add line height and character spacing to the Text node (#2016)

This commit is contained in:
Keavon Chambers 2024-10-01 12:28:27 -07:00 committed by GitHub
parent 904cf09c79
commit 2d86fb24ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 127 additions and 34 deletions

View file

@ -94,6 +94,8 @@ pub enum GraphOperationMessage {
text: String, text: String,
font: Font, font: Font,
size: f64, size: f64,
line_height_ratio: f64,
character_spacing: f64,
parent: LayerNodeIdentifier, parent: LayerNodeIdentifier,
insert_index: usize, insert_index: usize,
}, },

View file

@ -182,12 +182,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
text, text,
font, font,
size, size,
line_height_ratio,
character_spacing,
parent, parent,
insert_index, insert_index,
} => { } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id); let layer = modify_inputs.create_layer(id);
modify_inputs.insert_text(text, font, size, layer); modify_inputs.insert_text(text, font, size, line_height_ratio, character_spacing, layer);
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() }); responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() });
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
@ -284,7 +286,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
} }
usvg::Node::Text(text) => { usvg::Node::Text(text) => {
let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_core::consts::DEFAULT_FONT_STYLE.to_string()); let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_core::consts::DEFAULT_FONT_STYLE.to_string());
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, 24., 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)); modify_inputs.fill_set(Fill::Solid(Color::BLACK));
} }
} }

View file

@ -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 stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template(); let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template(); let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
@ -186,6 +186,8 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::String(text), false)), Some(NodeInput::value(TaggedValue::String(text), false)),
Some(NodeInput::value(TaggedValue::Font(font), false)), Some(NodeInput::value(TaggedValue::Font(font), false)),
Some(NodeInput::value(TaggedValue::F64(size), false)), Some(NodeInput::value(TaggedValue::F64(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()); let text_id = NodeId(generate_uuid());

View file

@ -2056,11 +2056,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
false, false,
), ),
NodeInput::value(TaggedValue::F64(24.), false), NodeInput::value(TaggedValue::F64(24.), false),
NodeInput::value(TaggedValue::F64(1.2), false),
NodeInput::value(TaggedValue::F64(1.), false),
], ],
..Default::default() ..Default::default()
}, },
persistent_node_metadata: DocumentNodePersistentMetadata { 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()], output_names: vec!["Vector".to_string()],
..Default::default() ..Default::default()
}, },

View file

@ -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 text = text_area_widget(document_node, node_id, 1, "Text", true);
let (font, style) = font_inputs(document_node, node_id, 2, "Font", 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 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 }]; let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }];
if let Some(style) = style { if let Some(style) = style {
result.push(LayoutGroup::Row { widgets: style }); result.push(LayoutGroup::Row { widgets: style });
} }
result.push(LayoutGroup::Row { widgets: size }); result.push(LayoutGroup::Row { widgets: size });
result.push(LayoutGroup::Row { widgets: line_height_ratio });
result.push(LayoutGroup::Row { widgets: character_spacing });
result result
} }

View file

@ -471,8 +471,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
log::error!("could not get node in deserialize_document"); log::error!("could not get node in deserialize_document");
continue; 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 node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node; let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone()); document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone());
@ -529,6 +530,26 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
} }
} }
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
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 // Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946
if reference == "Merge" || reference == "Artboard" { if reference == "Merge" || reference == "Artboard" {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap(); let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();

View file

@ -127,14 +127,16 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
} }
/// Gets properties from the Text node /// Gets properties from the Text node
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, f64)> { 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 inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Text")?;
let Some(TaggedValue::String(text)) = &inputs[1].as_value() else { return None }; let Some(TaggedValue::String(text)) = &inputs[1].as_value() else { return None };
let Some(TaggedValue::Font(font)) = &inputs[2].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(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<f64> { pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {

View file

@ -24,7 +24,9 @@ pub struct TextTool {
} }
pub struct TextOptions { pub struct TextOptions {
font_size: u32, font_size: f64,
line_height_ratio: f64,
character_spacing: f64,
font_name: String, font_name: String,
font_style: String, font_style: String,
fill: ToolColorOptions, fill: ToolColorOptions,
@ -33,7 +35,9 @@ pub struct TextOptions {
impl Default for TextOptions { impl Default for TextOptions {
fn default() -> Self { fn default() -> Self {
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_name: graphene_core::consts::DEFAULT_FONT_FAMILY.into(),
font_style: graphene_core::consts::DEFAULT_FONT_STYLE.into(), font_style: graphene_core::consts::DEFAULT_FONT_STYLE.into(),
fill: ToolColorOptions::new_primary(), fill: ToolColorOptions::new_primary(),
@ -63,7 +67,9 @@ pub enum TextOptionsUpdate {
FillColor(Option<Color>), FillColor(Option<Color>),
FillColorType(ToolColorType), FillColorType(ToolColorType),
Font { family: String, style: String }, Font { family: String, style: String },
FontSize(u32), FontSize(f64),
LineHeightRatio(f64),
CharacterSpacing(f64),
WorkingColors(Option<Color>, Option<Color>), WorkingColors(Option<Color>, Option<Color>),
} }
@ -100,13 +106,29 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
.into() .into()
}) })
.widget_holder(); .widget_holder();
let size = NumberInput::new(Some(tool.options.font_size as f64)) let size = NumberInput::new(Some(tool.options.font_size))
.unit(" px") .unit(" px")
.label("Size") .label("Size")
.int() .int()
.min(1.) .min(1.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64) .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(); .widget_holder();
vec![ vec![
font, font,
@ -114,6 +136,10 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
style, style,
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
size, 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<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
self.send_layout(responses, LayoutTarget::ToolOptions); self.send_layout(responses, LayoutTarget::ToolOptions);
} }
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size, 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) => { TextOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color; self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom; self.options.fill.color_type = ToolColorType::Custom;
@ -200,6 +228,8 @@ pub struct EditingText {
text: String, text: String,
font: Font, font: Font,
font_size: f64, font_size: f64,
line_height_ratio: f64,
character_spacing: f64,
color: Option<Color>, color: Option<Color>,
transform: DAffine2, transform: DAffine2,
} }
@ -233,11 +263,13 @@ impl TextToolData {
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> { fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> {
let transform = document.metadata().transform_to_viewport(self.layer); let transform = document.metadata().transform_to_viewport(self.layer);
let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK); let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK);
let (text, font, font_size) = 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 { self.editing_text = Some(EditingText {
text: text.clone(), text: text.clone(),
font: font.clone(), font: font.clone(),
font_size, font_size,
line_height_ratio,
character_spacing,
color: Some(color), color: Some(color),
transform, transform,
}); });
@ -295,6 +327,8 @@ impl TextToolData {
text: String::new(), text: String::new(),
font: editing_text.font.clone(), font: editing_text.font.clone(),
size: editing_text.font_size, 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), parent: document.new_layer_parent(true),
insert_index: 0, insert_index: 0,
}); });
@ -364,7 +398,14 @@ impl Fsm for TextToolFsmState {
}); });
if let Some(editing_text) = tool_data.editing_text.as_ref() { if let Some(editing_text) = tool_data.editing_text.as_ref() {
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data)); let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(&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. { if far.x != 0. && far.y != 0. {
let quad = Quad::from_box([DVec2::ZERO, far]); let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad; let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
@ -376,11 +417,11 @@ impl Fsm for TextToolFsmState {
} }
(_, TextToolMessage::Overlays(mut overlay_context)) => { (_, TextToolMessage::Overlays(mut overlay_context)) => {
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { 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; continue;
}; };
let buzz_face = font_cache.get(font).map(|data| load_face(data)); let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, 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 quad = Quad::from_box([DVec2::ZERO, far]);
let multiplied = document.metadata().transform_to_viewport(layer) * quad; let multiplied = document.metadata().transform_to_viewport(layer) * quad;
overlay_context.quad(multiplied, None); overlay_context.quad(multiplied, None);
@ -392,7 +433,9 @@ impl Fsm for TextToolFsmState {
tool_data.editing_text = Some(EditingText { tool_data.editing_text = Some(EditingText {
text: String::new(), text: String::new(),
transform: DAffine2::from_translation(input.mouse.position), 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()), font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
color: tool_options.fill.active_color(), color: tool_options.fill.active_color(),
}); });

View file

@ -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 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(); let buffer = UnicodeBuffer::new();
(scale, line_height, buffer) (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<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_pos: f64) -> bool { fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64) -> bool {
if let Some(line_width) = line_width { if let Some(line_width) = line_width {
let word_length: i32 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance).sum(); 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 as f64 * scale; let scaled_word_length = word_length * font_size;
if scaled_word_length + x_pos > line_width { if scaled_word_length + x_pos > line_width {
return true; return true;
@ -80,14 +80,14 @@ fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_
false false
} }
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Vec<Subpath<PointId>> { pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option<f64>) -> Vec<Subpath<PointId>> {
let buzz_face = match buzz_face { let buzz_face = match buzz_face {
Some(face) => face, Some(face) => face,
// Show blank layer if font has not loaded // Show blank layer if font has not loaded
None => return vec![], None => return vec![],
}; };
let (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 { let mut builder = Builder {
current_subpath: Subpath::new(Vec::new(), false), current_subpath: Subpath::new(Vec::new(), false),
@ -105,13 +105,13 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, li
push_str(&mut buffer, word, index != length - 1); push_str(&mut buffer, word, index != length - 1);
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
if wrap_word(line_width, &glyph_buffer, scale, 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); builder.pos = DVec2::new(0., builder.pos.y + line_height);
} }
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) { for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
if let Some(line_width) = line_width { 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); builder.pos = DVec2::new(0., builder.pos.y + line_height);
} }
} }
@ -121,7 +121,7 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, li
builder.other_subpaths.push(core::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false))); builder.other_subpaths.push(core::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false)));
} }
builder.pos += DVec2::new(glyph_position.x_advance as f64, 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(); buffer = glyph_buffer.clear();
@ -131,14 +131,14 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, li
builder.other_subpaths builder.other_subpaths
} }
pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> DVec2 { pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_height_ratio: f64, character_spacing: f64, line_width: Option<f64>) -> DVec2 {
let buzz_face = match buzz_face { let buzz_face = match buzz_face {
Some(face) => face, Some(face) => face,
// Show blank layer if font has not loaded // Show blank layer if font has not loaded
None => return DVec2::ZERO, None => return DVec2::ZERO,
}; };
let (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 pos = DVec2::ZERO;
let mut bounds = DVec2::ZERO; let mut bounds = DVec2::ZERO;
@ -150,17 +150,17 @@ pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f6
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); 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); pos = DVec2::new(0., pos.y + line_height);
} }
for glyph_position in glyph_buffer.glyph_positions() { for glyph_position in glyph_buffer.glyph_positions() {
if let Some(line_width) = line_width { 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(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)); bounds = bounds.max(pos + DVec2::new(0., line_height));

View file

@ -3,7 +3,15 @@ use graph_craft::wasm_application_io::WasmEditorApi;
pub use graphene_core::text::{bounding_box, load_face, to_path, Font, FontCache}; pub use graphene_core::text::{bounding_box, load_face, to_path, Font, FontCache};
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
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)); 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)
} }