Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ vello_encoding = "0.7"
resvg = "0.47"
usvg = "0.47"
parley = "0.6"
hyphenation = { version = "0.8.3", features = ["embed_all"] }
skrifa = "0.40"
polycool = "0.4"
# Linebender ecosystem (END)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
Some(NodeInput::value(TaggedValue::Bool(false), false)), // separate_glyph_elements
Some(NodeInput::value(TaggedValue::Bool(typesetting.hyphenate), false)),
]);
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
let stroke = resolve_proto_node_type(graphene_std::vector_nodes::stroke::IDENTIFIER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ pub fn text_width(text: &str, font_size: f64) -> f64 {
max_height: None,
tilt: 0.0,
align: TextAlign::Left,
hyphenate: false,
};

// Load Source Sans Pro font data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@ impl OverlayContextInternal {
max_height: None,
tilt: 0.,
align: TextAlign::Left, // We'll handle alignment manually via pivot
hyphenate: false,
};

// Load Source Sans Pro font data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,9 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text::AlignInput::INDEX].as_value() else {
return None;
};
let Some(&TaggedValue::Bool(hyphenate)) = inputs[graphene_std::text::text::HyphenateInput::INDEX].as_value() else {
return None;
};
let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[graphene_std::text::text::SeparateGlyphElementsInput::INDEX].as_value() else {
return None;
};
Expand All @@ -434,6 +437,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
character_spacing,
tilt,
align,
hyphenate,
};
Some((text, font, typesetting, per_glyph_instances))
}
Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/tool/tool_messages/text_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ impl Fsm for TextToolFsmState {
max_height: constraint_size.map(|size| size.y),
tilt: tool_options.tilt,
align: tool_options.align,
hyphenate: false,
},
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
color: tool_options.fill.active_color(),
Expand Down
2 changes: 2 additions & 0 deletions node-graph/libraries/core-types/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub struct TypesettingConfig {
pub max_height: Option<f64>,
pub tilt: f64,
pub align: TextAlign,
pub hyphenate: bool,
}

impl Default for TypesettingConfig {
Expand All @@ -55,6 +56,7 @@ impl Default for TypesettingConfig {
max_height: None,
tilt: 0.,
align: TextAlign::default(),
hyphenate: false,
}
}
}
2 changes: 2 additions & 0 deletions node-graph/nodes/gstd/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fn text<'i: 'n>(
align: TextAlign,
/// Whether to split every letterform into its own vector path element. Otherwise, a single compound path is produced.
separate_glyph_elements: bool,
hyphenate: bool,
) -> Table<Vector> {
let typesetting = TypesettingConfig {
font_size: size,
Expand All @@ -70,6 +71,7 @@ fn text<'i: 'n>(
max_height: has_max_height.then_some(max_height),
tilt,
align,
hyphenate,
};

to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyph_elements)
Expand Down
1 change: 1 addition & 0 deletions node-graph/nodes/text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ node-macro = { workspace = true }
dyn-any = { workspace = true }
glam = { workspace = true }
parley = { workspace = true }
hyphenation = { workspace = true }
skrifa = { workspace = true }
log = { workspace = true }

Expand Down
2 changes: 2 additions & 0 deletions node-graph/nodes/text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct TypesettingConfig {
pub max_height: Option<f64>,
pub tilt: f64,
pub align: TextAlign,
pub hyphenate: bool,
}

impl Default for TypesettingConfig {
Expand All @@ -59,6 +60,7 @@ impl Default for TypesettingConfig {
max_height: None,
tilt: 0.,
align: TextAlign::default(),
hyphenate: false,
}
}
}
74 changes: 70 additions & 4 deletions node-graph/nodes/text/src/text_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use super::{Font, FontCache, TypesettingConfig};
use core::cell::RefCell;
use core_types::table::Table;
use glam::DVec2;
use hyphenation::{Hyphenator, Language, Load, Standard};
use parley::fontique::{Blob, FamilyId, FontInfo};
use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, OverflowWrap, PositionedLayoutItem, StyleProperty};
use std::collections::HashMap;
use std::sync::OnceLock;
use vector_types::Vector;

use super::path_builder::PathBuilder;
Expand All @@ -13,6 +15,24 @@ thread_local! {
static THREAD_TEXT: RefCell<TextContext> = RefCell::new(TextContext::default());
}

static EN_US_DICT: OnceLock<Option<Standard>> = OnceLock::new();

fn get_dict_for_language(lang: Language) -> Option<&'static Standard> {
match lang {
Language::EnglishUS | Language::EnglishGB => EN_US_DICT
.get_or_init(|| {
Standard::from_embedded(Language::EnglishUS)
.map_err(|err| {
log::error!("Failed to load hyphenation dictionary: {err}");
err
})
.ok()
})
.as_ref(),
_ => None,
}
}

/// Unified thread-local text processing context that combines font and layout management
/// for efficient text rendering operations.
#[derive(Default)]
Expand Down Expand Up @@ -61,14 +81,25 @@ impl TextContext {
}

/// Create a text layout using the specified font and typesetting configuration
// TODO: Cache layout builder to avoid re-shaping text continuously.
fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option<Layout<()>> {
// Note that the actual_font may not be the desired font if that font is not yet loaded.
// It is important not to cache the default font under the name of another font.
let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?;
let (font_family, font_info) = self.get_font_info(actual_font, &font_data)?;

let injected: String;
let layout_text = if typesetting.hyphenate && typesetting.max_width.is_some() {
// TODO: pull language from typesetting config
injected = apply_hyphenation(text, Language::EnglishUS);

&injected
} else {
text
};

const DISPLAY_SCALE: f32 = 1.;
let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false);
let mut builder = self.layout_context.ranged_builder(&mut self.font_context, layout_text, DISPLAY_SCALE, false);

builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
Expand All @@ -77,8 +108,8 @@ impl TextContext {
builder.push_default(StyleProperty::FontStyle(font_info.style()));
builder.push_default(StyleProperty::FontWidth(font_info.width()));
builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32));

let mut layout: Layout<()> = builder.build(text);
builder.push_default(StyleProperty::OverflowWrap(OverflowWrap::BreakWord));
let mut layout = builder.build(layout_text);

layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default());
Expand Down Expand Up @@ -133,3 +164,38 @@ impl TextContext {
max_height < bounds.y
}
}

/// Apply hyphenation to the given text.
fn apply_hyphenation(text: &str, lang: Language) -> String {
let Some(dict) = get_dict_for_language(lang) else {
return text.to_string();
};
let mut out = String::with_capacity(text.len() + text.len() / 8);
let mut word_start: Option<usize> = None;

fn push_hyphenated(out: &mut String, dict: &hyphenation::Standard, word: &str) {
const SOFT_HYPHEN: char = '\u{00AD}';
let mut segs = dict.hyphenate(word).into_iter().segments().peekable();
while let Some(seg) = segs.next() {
out.push_str(seg);
if segs.peek().is_some() {
out.push(SOFT_HYPHEN);
}
}
}

for (i, c) in text.char_indices() {
if c.is_alphabetic() {
word_start.get_or_insert(i);
} else {
if let Some(start) = word_start.take() {
push_hyphenated(&mut out, dict, &text[start..i]);
}
out.push(c);
}
}
if let Some(start) = word_start {
push_hyphenated(&mut out, dict, &text[start..]);
}
out
}
Loading