Skip to content

Parley + Fontique (Rust, Linebender) ​

Linebender's rich-text layout stack: fontique enumerates system fonts and answers fallback queries, parley turns styled text into lines, runs and positioned glyphs via HarfRust (shaping) and Skrifa (metrics), and leaves rasterization to whoever paints the Layout.

FieldValue
LanguageRust (edition 2024, MSRV 1.88); fontique is no_std + alloc
LicenseApache-2.0 OR MIT (Cargo.toml)
Repositorylinebender/parley
DocumentationCrate docs in parley/src/lib.rs; stack overview in README.md; docs.rs
Categorylayout engine · font database/discovery
Layer(s) covereddiscover · match/fallback · shape · layout (parse via read-fonts/skrifa; raster delegated)
Version at pinworkspace 0.11.0
Pinned revisiondd7e0b04966c2fda89356668e5b5dc46904d45c5 (2026-10-02)

Size at the pin (find <dir> -name '*.rs' | xargs wc -l):

CrateRole.rs lines
fontiqueenumeration, matching, fallback, synthesis6 119
parley_engineanalysis, itemization, HarfRust shaping, shaped text6 660
parleystyles, builders, line breaking, alignment, editing14 006
parlanceshared vocabulary (Script, GenericFamily, FontVariation)2 862

Overview ​

What it solves ​

GUI toolkits (Xilem, Masonry, Blitz's WPT-tested HTML engine) need CSS-grade paragraph layout — bidi, line breaking, per-span font properties, inline boxes, font fallback — without a C dependency. Parley composes four Rust crates rather than one monolith, and the README states the division of labour literally.

Design philosophy ​

text
Parley has four key dependencies: Fontique, HarfRust, Skrifa, and ICU4X. These
crates cover different pieces of the text-rendering process.
...
Fontique provides font enumeration and fallback.
...
The library is responsible for loading fonts into memory; it will use
memory-mapped IO to load portions into memory lazily and share them between
processes on the system.

— README.md, lines 19–29

The second governing idea is the context split, from the crate docs:

rust
//! - [`FontContext`] and [`LayoutContext`] are resources which should be shared globally (or at coarse-grained boundaries).
//!   - [`FontContext`] is database of fonts.
//!   - [`LayoutContext`] is scratch space that allows for reuse of allocations between layouts.

— parley/src/lib.rs, lines 7–9

Shaping at this pin is HarfRust, not swash: parley_engine depends on harfrust and skrifa only (parley_engine/src/shape/shaper.rs line 8); swash survives as a workspace dependency of the examples/swash_render rasterization demo (main.rs).

How it works ​

FontContext is two public fields, collection: fontique::Collection and source_cache: fontique::SourceCache (parley/src/font.rs). LayoutContext<B> holds the resolver, style tables, an ICU-backed Analyzer and Analysis, and a Shaper (context.rs, lines 21–45). A RangedBuilder (flat push(StyleProperty, range)), TreeBuilder (nested spans) or StyleRunBuilder resolves styles, then build(&text) runs analysis (bidi, script, segmentation, emoji), itemizes, selects a font per character cluster, shapes each single-font run, and returns Layout<B>. Line breaking (break_all_lines(max_width)) and align(...) can be repeated on the same Layout; a text or style change requires a new builder. Iteration is layout.lines() → line.items() → PositionedLayoutItem::GlyphRun or InlineBox → glyphs, each Glyph { id: u32, x, y, advance: f32 } (parley_engine/src/glyph.rs).

Analysis spine ​

1. Layering and ownership ​

LayerType(s)Owner / lifetime
Font bytesBlob<u8> (linebender_resource_handle, Arc + u64 id)refcounted; mmap'd by SourceCache
Font managerfontique::Collection, SourceCacheClone; optional Arc<Mutex<…>> shared store
Face referenceFontInfo (metadata), QueryFont, FontInstance{font, synthesis}value types holding a Blob + collection index
Shaper stateShaper (three 16-entry LRU caches of HarfRust objects)inside LayoutContext, &mut
OutputLayout<B> → Line → Run → Cluster → Glyphowned by caller, generic over brush B

There is no sized-font object: size is ShapeOptions::font_size, a field of each shaping call. The parsed-face caches are keyed by blob id + index, not by pointer: ShapeDataKey { font_blob_id: u64, font_index: u32 }, and ShapeInstanceId adds Synthesis and the variation list (cache.rs, lines 8–47). Thread-safety is by &mut: every Collection accessor, even family_names, takes &mut self because it calls sync_shared() first. With CollectionOptions { shared: true }, clones share an Arc<Shared> holding Mutex data and an atomic version counter, and each clone re-syncs lazily (collection/mod.rs, lines 37–58, 251–280); the doc comment calls sharing "pure overhead" for single-threaded use. Errors are Option throughout; harfrust::FontRef::from_index(...).unwrap() with a // TODO: How do we want to handle errors like this? is the shaping path's error model (shaper.rs, lines 275–279).

2. Face loading and table access ​

Two sources: SourceKind::Path (mmap via memmap2, std only) and SourceKind::Memory(Blob). FontInfo::from_source(source, index) parses with read-fonts' FontRef::from_index and extracts width/style/weight, fvar axes and a CharmapIndex (fontique/src/font.rs, lines 31–48, 224–254); scan.rs walks directories and enumerates every face of a .ttc (scan.rs, lines 148–195). Raw tables are not exposed by fontique or parley — a consumer re-opens Blob + index with skrifa/read-fonts, which is the intended inspector path. CharmapIndex remembers the chosen cmap subtable and maps only formats 4 and 12 (charmap.rs, lines 80–86).

3. Shaping ​

Shaper::shape_text takes the analysed text, a FontSelector callback and ShapeOptions { font_size, language, features: &[FontFeature], variations: &[FontVariation] } (shaper.rs, lines 25–37). Per run it fetches three cached HarfRust objects — ShaperData (per face), ShaperInstance (per face × variations), ShapePlan (per instance × script × language × direction × features):

rust
pub struct Shaper {
    shape_data_cache: LruCache<cache::ShapeDataKey, harfrust::ShaperData>,
    shape_instance_cache: LruCache<cache::ShapeInstanceId, harfrust::ShaperInstance>,
    shape_plan_cache: LruCache<cache::ShapePlanId, harfrust::ShapePlan>,
    unicode_buffer: Option<harfrust::UnicodeBuffer>,
    features: Vec<harfrust::Feature>,
    char_cluster: CharCluster,
}

— shaper.rs, lines 87–94 (MAX_ENTRIES = 16, line 98)

Features are OpenType tag/value pairs applied to the whole item (later duplicates win). The buffer uses BufferClusterLevel::MonotoneCharacters, so glyph clusters map back to UTF-8 byte ranges; Cluster exposes text range, visual order and ligature components. Output is pixels at font_size, with optional quantization (quantize: bool on the builders).

4. Variation and instances ​

Coordinates enter from two places and are concatenated in a fixed order: fontique::Synthesis::variation_settings() first, then the style's StyleProperty::FontVariations (accepts a CSS font-variation-settings string or a &[FontVariation] list, style/font.rs):

rust
fn variations_iter<'a>(
    synthesis: &'a fontique::Synthesis,
    item: &'a [FontVariation],
) -> impl Iterator<Item = harfrust::Variation> + 'a {
    synthesis
        .variation_settings()
        .iter()
        .map(|(tag, value)| harfrust::Variation { tag: *tag, value: *value })
        .chain(item.iter().map(|variation| harfrust::Variation {
            tag: harfrust::Tag::new(&variation.tag.to_bytes()),
            value: variation.value,
        }))
}

— shaper.rs, lines 433–450 (lightly reflowed)

User-space values go into harfrust::ShaperInstance::from_variations; the normalized coordinates it computes (avar applied) are stored on the run (harf_shaper.coords(), line 372) and surface as Run::normalized_coords() -> &[NormalizedCoord] (run.rs, line 111). Metrics recompute the location independently through skrifa's font_ref.axes().location(...) (shaped_text.rs, lines 82–99). The rasterizer receives the run's normalized coords — the swash example passes .normalized_coords(normalized_coords.iter().map(|c| c.to_bits())) to its Scaler (main.rs, lines 250–261). So the contract is: user coords in, F2Dot14 normalized coords out per run, handed to any rasterizer. FontInfo::axes() exposes fvar min/default/max; named instances and STAT are not read.

Synthesis is the novel piece: when a requested attribute differs from the face, fontique prefers a real axis over faking. A requested weight sets wght whenever the axis default differs (even 400 vs a wght default of 100); without an axis and more than 200 units short, it sets embolden. Italic tries ital=1, then slnt=14, then skew=14° (font.rs, lines 90–150).

5. Rasterization and outlines ​

Absent by design — Parley ends at positioned glyph ids plus Run::synthesis() (embolden/skew hints) and normalized_coords(). Outlines, hinting, AA and color glyphs belong to the painter: the repo ships swash_render, tiny_skia_render and vello_cpu_render examples, and vello consumes runs via skrifa. CoreText fallback carries a telling hack: "if we don't have a usable PingFangUI due to our inability to render hvgl outlines then try another font" (coretext.rs, lines 79–85) — fallback is constrained by what the downstream rasterizer can draw.

6. Metrics and measurement ​

FontMetrics { ascent, descent, leading, underline_*, strikethrough_*, cap_height, x_height } is built from skrifa::metrics::Metrics::new(font, Size::new(font_size), &location) — i.e. variation-aware and in pixels, with skrifa deciding hhea vs OS/2 typo (USE_TYPO_METRICS) (shaped_text.rs, lines 32–52, 95–125). Missing underline/strikeout fall back to HarfBuzz's defaults (units_per_em / 18), and a source TODO admits these stay in design units while the other fields are scaled. Layout-level metrics are LineMetrics { line_height, baseline, offset, advance, hanging_advance, … } (line.rs, lines 159–185) and SpanMetrics { ascent, descent, x_height }.

7. Discovery, matching and fallback ​

Discovery is per-platform behind one system module selected by cfg (backend/mod.rs): fontconfig on Linux/FreeBSD (linked or fontconfig-dlopen), CoreText enumeration plus a Library/Fonts scan on Apple, DirectWrite on Windows, and $ANDROID_ROOT/etc/fonts.xml parsed with roxmltree on Android. Families get a FamilyId; GenericFamily is the CSS set plus UiRounded, Emoji, Math (generic_family.rs).

Matching is the CSS Fonts 4 algorithm over a family's FontInfo list (match_font, matching.rs, 483 lines). A Query takes a family list, Attributes { width, style, weight } and a FallbackKey { script, locale }; matches_with(|font| QueryStatus) yields candidates from the requested families then the fallback families (query.rs, lines 96–170). Every query also appends the Hani fallback to catch CJK punctuation in Common (issue 597 hack, lines 105–112). Fallback families are resolved per script + locale, each backend in its own way:

BackendFallback mechanism
fontconfigFcPattern with FC_LANG + an FcCharSet of the script's sample chars → FcFontMatch
CoreTextCTFontCreateForString (…WithLanguage when a locale is set) on the sample string
DirectWriteIDWriteFontFallback::MapCharacters over the sample
Androidlocale table, then script table, then first sans-serif

The per-script sample strings live in SCRIPT_SAMPLES (script.rs). Parley then chooses per character cluster: in select_font it scores each query candidate by cluster.calculate_coverage(charmap.map(ch) != 0) and keeps the best; emoji clusters append GenericFamily::Emoji; a last-resort "any font" is cached when nothing has a charmap (parley/src/shape/mod.rs, lines 318–420). Classification is OS/2 usWidthClass, fsSelection italic/oblique, post.italicAngle, and weight — no PANOSE.

What it teaches sparkles:font ​

  • Two contexts, not one: a long-lived font database (FontContext) and a reusable scratch arena (LayoutContext) passed by ref to builders. In D, that is FontCollection + a UniqueBuffer-backed LayoutScratch.
  • Key caches by blob id + face index, not by pointer, and split shaping state into per-face / per-instance / per-plan caches with an LRU bound — the HarfBuzz hb_face_t/hb_font_t/hb_shape_plan_t split, made explicit.
  • Synthesis as data: matching returns "set wght=600" or "embolden" / "skew 14°" as a value the shaper and rasterizer both consume. hue's bold/italic face selection should return this, not a boolean.
  • Fallback = script sample string → platform matcher → family id, then per-cluster charmap coverage scoring. Probe with a sample, cache by (script, locale), decide per cluster.
  • Hand the rasterizer normalized coords per run, never re-derive them; the layout output must carry normalizedCoords alongside glyph ids.

Strengths ​

  • Clean crate seams: discovery, shaping engine, layout and vocabulary are separately usable; fontique is no_std.
  • Real system fallback on all four target platforms, including Android fonts.xml.
  • Variation-aware end to end: synthesis, shaping, metrics and the per-run coordinate hand-off agree.
  • CSS-conformant matching; tested against WPT via Blitz.
  • Lazy mmap loading and Blob sharing across clones.

Weaknesses ​

  • No raw table, feature-list or name-table API: an inspector must bypass it.
  • Collection read accessors require &mut; sharing needs Mutex + resync.
  • unwrap() on malformed faces in the shaping path.
  • Fallback is per script, not per codepoint, until cluster coverage scoring; hacks (Hani always appended, PingFang hvgl) show the seams.
  • No named instances or STAT; underline metrics left in design units.

Key design decisions and trade-offs ​

DecisionRationaleTrade-off
Separate fontique (discovery) from parley (layout)Reuse discovery without layout; no_std coreTwo contexts to thread through every call
HarfRust for shaping, Skrifa for metricsPure-Rust HarfBuzz parity; one fontations parserTwo parses of the same blob (harfrust + skrifa)
Size as a call parameter, caches keyed by blob idNo sized-font lifetime; cheap clones16-entry LRU can thrash with many faces × instances
Synthesis returned by matchingPrefer real axes over fake bold/italicRenderer must honour embolden/skew itself
Script-sample fallback via platform matchersReuses OS policy (fontconfig rules, CoreText cascade)Platform-dependent results; locale-only on Android
No rasterizationPainters (vello, swash, tiny-skia) differEvery consumer re-solves glyph caching
Layout rebreakable but not restylableCheap resize reflowAny edit rebuilds the layout

Sources ​

All paths verified at the pinned revision with git cat-file -e.