HTVG uses Parley , a modern text layout library from the Linebender project, to handle text shaping, line breaking, and glyph positioning.
Overview
Text rendering in HTVG is a two-phase process:
Measurement Phase
Parley measures text dimensions for layout computation
Layout Phase
Parley performs full text shaping with glyph positions
Rendering Phase
SVG <text> elements are generated from glyph positions
Why Parley?
Parley is a state-of-the-art text layout engine designed for modern UI frameworks.
Unicode Correct Proper handling of complex scripts (Arabic, Thai, etc.)
Bidirectional Text Supports RTL and mixed-direction text
Advanced Typography Ligatures, kerning, and OpenType features
Line Breaking Intelligent word wrapping with Unicode line break algorithm
Text Layout Engine
The text engine is implemented in crates/htvg-core/src/text.rs:
pub struct TextLayoutEngine {
font_cx : FontContext ,
layout_cx : LayoutContext <[ u8 ; 4 ]>,
}
impl TextLayoutEngine {
pub fn new () -> Self {
Self {
font_cx : FontContext :: new (),
layout_cx : LayoutContext :: new (),
}
}
}
From crates/htvg-core/src/text.rs:18-30.
Font Context
The FontContext manages font loading and selection. It maintains a font collection that can be populated with system fonts or custom fonts.
Layout Context
The LayoutContext is reusable across multiple layout operations, improving performance by caching internal state.
Font Registration
Fonts can be registered from binary data:
pub fn register_font ( & mut self , data : Vec < u8 >) -> Vec < String > {
let families = self . font_cx . collection . register_fonts ( data . into (), None );
families
. iter ()
. map ( | ( id , _info ) | {
self . font_cx
. collection
. family_name ( * id )
. unwrap_or ( "unknown" )
. to_string ()
})
. collect ()
}
From crates/htvg-core/src/text.rs:32-45.
For self-contained SVG output that doesn’t depend on system fonts, register custom fonts using this method.
Text Measurement
During layout computation, Taffy calls the measurement function to determine text dimensions:
pub fn measure ( & mut self , text : & str , font_size : f32 , max_width : Option < f32 >) -> Size < f32 > {
if text . is_empty () {
return Size {
width : 0.0 ,
height : font_size * 1.2 ,
};
}
let mut builder = self
. layout_cx
. ranged_builder ( & mut self . font_cx, text , 1.0 , false );
builder . push_default ( StyleProperty :: FontSize ( font_size ));
builder . push_default ( StyleProperty :: LineHeight ( LineHeight :: FontSizeRelative ( 1.2 )));
let mut layout : Layout <[ u8 ; 4 ]> = builder . build ( text );
layout . break_all_lines ( max_width );
let width = layout . width ();
let height = layout . height ();
// Fallback: if Parley returns zero dimensions (no font available),
// use approximate character-width estimation.
if width == 0.0 || height == 0.0 {
return fallback_measure ( text , font_size , 1.2 , max_width );
}
Size { width , height }
}
From crates/htvg-core/src/text.rs:47-76.
Measurement Process
Create builder - Initialize ranged style builder with font context
Apply styles - Set font size and line height
Build layout - Generate the layout tree
Break lines - Apply line breaking with max width constraint
Return dimensions - Extract width and height
If no fonts are available (e.g., in WASM without font registration), a fallback estimation is used based on average character width.
Full Text Layout
After Taffy computes positions, the full layout is performed:
pub fn layout (
& mut self ,
text : & str ,
font_family : & str ,
font_size : f32 ,
font_weight : u16 ,
line_height : f32 ,
text_align : TextAlign ,
max_width : f32 ,
) -> TextLayoutResult {
if text . is_empty () {
return TextLayoutResult {
width : 0.0 ,
height : font_size * line_height ,
lines : vec! [],
};
}
let mut builder = self
. layout_cx
. ranged_builder ( & mut self . font_cx, text , 1.0 , false );
builder . push_default ( StyleProperty :: FontSize ( font_size ));
builder . push_default ( StyleProperty :: FontWeight ( FontWeight :: new ( font_weight as f32 )));
builder . push_default ( StyleProperty :: LineHeight ( LineHeight :: FontSizeRelative (
line_height ,
)));
builder . push_default ( StyleProperty :: FontStack ( FontStack :: Source ( Cow :: Owned (
font_family . to_string (),
))));
let mut layout : Layout <[ u8 ; 4 ]> = builder . build ( text );
layout . break_all_lines ( Some ( max_width ));
let alignment = match text_align {
TextAlign :: Left => Alignment :: Start ,
TextAlign :: Center => Alignment :: Center ,
TextAlign :: Right => Alignment :: End ,
TextAlign :: Justify => Alignment :: Justify ,
};
layout . align ( Some ( max_width ), alignment , AlignmentOptions :: default ());
// Extract lines and glyphs...
}
From crates/htvg-core/src/text.rs:78-120.
Layout Steps
Create builder with all style properties
Apply font family using FontStack
Build layout with full typography
Break lines with max width
Align text according to textAlign property
Extract glyph positions for rendering
Line and Glyph Extraction
Parley provides line and glyph information:
for line in layout . lines () {
let metrics = line . metrics ();
let mut line_glyphs = Vec :: new ();
let mut line_start : Option < usize > = None ;
let mut line_end : usize = 0 ;
for item in line . items () {
if let PositionedLayoutItem :: GlyphRun ( positioned_run ) = item {
let run_x = positioned_run . offset ();
let run = positioned_run . run ();
let range = run . text_range ();
if line_start . is_none () || range . start < line_start . unwrap () {
line_start = Some ( range . start);
}
if range . end > line_end {
line_end = range . end;
}
for glyph in positioned_run . glyphs () {
line_glyphs . push ( PositionedGlyph {
glyph_id : glyph . id,
x : run_x + glyph . x,
y : glyph . y,
advance : glyph . advance,
});
}
}
}
let line_text = if let Some ( start ) = line_start {
text [ start .. line_end ] . trim_end () . to_string ()
} else {
String :: new ()
};
lines . push ( TextLine {
text : line_text ,
baseline : metrics . baseline,
ascent : metrics . ascent,
descent : metrics . descent,
glyphs : line_glyphs ,
});
}
From crates/htvg-core/src/text.rs:124-166.
Text Layout Result
The layout result contains all information needed for rendering:
pub struct TextLayoutResult {
pub width : f32 ,
pub height : f32 ,
pub lines : Vec < TextLine >,
}
pub struct TextLine {
pub text : String ,
pub baseline : f32 ,
pub ascent : f32 ,
pub descent : f32 ,
pub glyphs : Vec < PositionedGlyph >,
}
pub struct PositionedGlyph {
pub glyph_id : u32 ,
pub x : f32 ,
pub y : f32 ,
pub advance : f32 ,
}
From crates/htvg-core/src/text.rs:190-215.
TextLayoutResult
TextLine
PositionedGlyph
Contains overall dimensions and array of lines
One line of text with metrics (baseline, ascent, descent) and positioned glyphs
Individual glyph with ID and x/y position for rendering
Line Metrics
Each line provides typographic metrics:
baseline - Y position of the text baseline
ascent - Height above baseline (positive)
descent - Depth below baseline (positive)
┌─── ascent ───┐
│ │
─────── baseline ────────── ← y position
│ │
└── descent ───┘
These metrics are essential for correctly positioning text within SVG.
Text Alignment
Parley supports all standard text alignment modes:
Left (Start)
Center
Right (End)
Justify
Text aligned to the left edge (or right for RTL). Text centered horizontally. Text aligned to the right edge (or left for RTL). Text stretched to fill the full width.
Alignment is applied after line breaking:
let alignment = match text_align {
TextAlign :: Left => Alignment :: Start ,
TextAlign :: Center => Alignment :: Center ,
TextAlign :: Right => Alignment :: End ,
TextAlign :: Justify => Alignment :: Justify ,
};
layout . align ( Some ( max_width ), alignment , AlignmentOptions :: default ());
Line Breaking Algorithm
Parley uses the Unicode Line Breaking Algorithm (UAX #14) to determine valid line break opportunities.
Break Points
Between words - Standard space characters
After hyphens - Hyphen-minus and soft hyphens
Zero-width spaces - Explicit break opportunities
Language-specific rules - Complex scripts have special rules
Parley correctly handles languages like Thai (no spaces between words) and Arabic (contextual letter forms).
Fallback Rendering
When fonts are unavailable (e.g., WASM without font registration), HTVG uses fallback estimation:
const CHAR_WIDTH_RATIO : f32 = 0.55 ;
const ASCENT_RATIO : f32 = 0.8 ;
const DESCENT_RATIO : f32 = 0.2 ;
fn estimate_text_width ( text : & str , font_size : f32 ) -> f32 {
text . chars () . count () as f32 * font_size * CHAR_WIDTH_RATIO
}
fn word_wrap ( text : & str , font_size : f32 , max_width : Option < f32 >) -> Vec < String > {
// Simple word-based wrapping using estimated character width
// ...
}
From crates/htvg-core/src/text.rs:221-264.
Fallback rendering is approximate and doesn’t handle complex scripts correctly. For production use, always register fonts.
SVG Text Rendering
The SVG renderer uses Parley’s output to generate <text> elements:
< text
x = "20"
y = "50"
font-family = "sans-serif"
font-size = "16"
font-weight = "400"
fill = "#000000"
text-anchor = "start"
>
Line of text here
</ text >
Each line becomes a separate <text> element positioned at the correct baseline.
Text Rendering Modes
HTVG supports two text rendering modes (configured via textRendering style property):
Text Mode (Default)
Vector Mode (Planned)
{
"type" : "text" ,
"content" : "Hello" ,
"style" : {
"textRendering" : "text"
}
}
Renders as SVG <text> elements. Requires the font to be available on the viewer’s system. {
"type" : "text" ,
"content" : "Hello" ,
"style" : {
"textRendering" : "vector"
}
}
Would render text as vector <path> elements. Fully self-contained, no font dependency. Not yet implemented.
Vector rendering (converting glyphs to paths) is planned for future versions. This would make SVG output truly self-contained.
Typography Features
Parley supports advanced OpenType features:
Ligatures - Automatic substitution (fi → fi)
Kerning - Spacing adjustments between letter pairs
Contextual alternates - Letter forms that change based on context
Small caps, old-style numerals , and more
These features are automatically applied when the font supports them.
Measurement Caching
The LayoutContext is reused across multiple layout operations to cache internal state:
pub struct TextLayoutEngine {
font_cx : FontContext ,
layout_cx : LayoutContext <[ u8 ; 4 ]>, // Reused for better performance
}
Two-Phase Approach
HTVG uses a two-phase approach:
Fast measurement - Quick dimension calculation for layout
Full layout - Complete glyph positioning only when needed
This avoids redundant work during the iterative layout process.
For documents with many text elements, text measurement is typically the performance bottleneck. Using a consistent font reduces font loading overhead.
Example: Text Layout Flow
Debugging Text Layout
When debugging text rendering issues:
Check font availability - Ensure fonts are registered or available on the system
Verify max width - Text needs a width constraint to wrap
Inspect line metrics - Check that baseline/ascent/descent are reasonable
Test with simple ASCII - Rule out complex script issues
Check fallback mode - If dimensions seem estimated, fonts may not be loaded
Common issue: Text not wrapping. Ensure the parent container has a defined width.
Element Types Learn about the Text element type
Style Properties Typography style properties reference
Layout Engine How text measurement integrates with layout
External Resources