gui_toolkit

Text backends (font package)

The font package gets its shaping and font reading from a backend, either DirectWrite or a Rust library, and finds installed fonts through the platform. Everything else in the package is the same code whichever backend is used.

BackendWhat it isBuild
dwriteDirectWrite: IDWriteTextAnalyzer (GetGlyphs / GetGlyphPlacements, AnalyzeScript, AnalyzeBidi, AnalyzeLineBreakpoints), IDWriteFontFacedefault, Windows only
nativeFontations (read-fonts font model: faces, cmap, metrics, outlines, variations, COLR / CPAL), HarfRust (shaping), ICU4X (icu_segmenter line breaks with dictionaries, icu_properties data for unicode-bidi) in a static Rust library native/text-define:FONT_BACKEND=native

-define:FONT_BACKEND=native links build/text/gt_text.lib (built by tools/tk build_text) and makes native the default; Settings.backend still picks either one for a State (read by init only), which is how the text_backends test and tools/text_bench compare them in one process. Without the define, asking for native gives dwrite, and on Linux no text is shaped at all: its builds always pass the define. font.backend(s) tells which one a State uses; font.backend_available(b) whether this build has it.

The interface (src/font/backend.odin)

Text_Backend is a table of procedures, all callable from any thread at once:

ProcedureDoes
start, stopper-State setup (the script item of ASCII text, a version check)
face_openthe backend's side of a face the platform made (native: opens the same file or bytes in Fontations; it closes with the face, release_font)
has_glyphcoverage of one character (the package caches the BMP answers per face)
face_metricsdesign metrics (Face_Metrics: upem, ascent, descent, line gap, x / cap height, underline, strike)
glyph_boxadvance and ink box of a glyph (design units)
scriptsscript runs of a word (Script_Item: UTF-16 range, ISO 15924 tag, the backend's own data)
shapeone run (one face, one script, one direction, OpenType features) to glyphs (visual order) and clusters (logical order)
bidiUAX #9 levels per byte of a paragraph
line_breaksUAX #14 opportunities (byte offsets, mandatory or not)
outlineglyph outlines for Slug (Outline_Provider)

System_Fonts is the platform's: fallback (faces for characters no cascade entry has) and families (installed families, font.system_families). On Windows it is DirectWrite whatever the backend (IDWriteFontFallback, the system collection); on Linux fontconfig, on macOS CoreText, elsewhere nothing (system_other.odin).

Faces are always opened by the platform first: DirectWrite's face is what the atlas rasterizes (hinted coverage) and Direct2D draws in color. The native backend then opens the same bytes (native_source_windows.odin: the memory of a web font, or the path of a system font file through IDWriteLocalFontFileLoader, plus the face index and the axis values of a variable instance). With native, text is shaped, measured, broken and outlined (Slug) by the library; gray glyph bitmaps of the atlas and color glyphs still come from DirectWrite / Direct2D or the COLR interpreter.

The native library (native/text)

C ABI, one call per run (gt_shape), per paragraph (gt_bidi_levels, gt_line_breaks, gt_scripts) or per glyph (gt_glyph_outline, gt_glyph_box); a call whose buffer is too small returns the size it needs. Also gt_face_coverage (code point ranges), gt_face_table (raw table bytes: COLR / CPAL for the package's own COLR interpreter, the same on every platform), gt_color_glyph (COLR v0 / v1), gt_palette (CPAL colors). Every entry point catches panics (panic = "unwind"): a font the library cannot read is a face without glyphs, not a dead program. Faces are immutable and shared by threads; shape plans are cached per face (32), buffers per thread.

One Rust static library per program. Each Rust static library carries its own copy of Rust's standard library: two in one program collide at link time (std::panicking::EMPTY_PANIC multiply defined). A program with Rust code of its own builds native/text into its library as a dependency (the gt_* functions stay exported) and builds with -define:FONT_NATIVE_LINK=false: the font package then links nothing itself. An application that links its own Rust library adds the crate to it this way.

The recipe, for an application with a crate of its own (opencode native does it for Linux):

toml
# your crate's Cargo.toml
[dependencies]
gt_text = { path = "../../ext/gui_toolkit/native/text" }
Rust
// its lib.rs: the gt_* functions are exported from your library
pub extern crate gt_text;

and build the program with -define:FONT_BACKEND=native -define:FONT_NATIVE_LINK=false.

WebGPU's wgpu-native is a Rust static library too, prebuilt: it cannot join your workspace. Its few unmangled symbols of Rust's standard library (rust_eh_personality) collide with yours. On Linux they are the same code: link with -extra-linker-flags:"-Wl,--allow-multiple-definition" (tk build, opencode native's occtools). On Windows the linker refuses the duplicate: use Direct3D 11 there (the default backend), or build wgpu-native from source in your workspace.

Built like Wren's native/ workspace: exact versions and Cargo.lock (--locked), fat LTO, one codegen unit, the library's rust_eh_personality renamed so it links next to other Rust static libraries. The toolkit builds it itself (it already builds Rust: naga for the shaders), so any application of the toolkit gets the backend.

Platforms

odin check src/font -no-entry-point passes for Windows, Linux and macOS, with either define. What only Windows has is in *_windows.odin (DirectWrite faces and analyzers, Direct2D color glyphs, the color glyph thread, gray glyph bitmaps). font_other.odin is the platform part elsewhere: faces from files and memory opened by the native backend, the system's fonts from the platform's font service, no bitmap rasterizer (text is drawn by Slug from the backend's outlines), no COLR color glyphs yet.

PlatformFont serviceload_systemfallbacksystem_families
WindowsDirectWritesystem collectionIDWriteFontFallbackcollection
Linuxfontconfig (font_system_linux.odin)FcFontMatch after the configuration's substitutions (the family must match)best match whose charset has the characterFcFontList
macOSCoreText (font_system_darwin.odin)CTFontCreateWithName + symbolic traits (bold, italic)CTFontCreateForStringCTFontManagerCopyAvailableFontFamilyNames
othersnone (system_other.odin)failsnoneempty

The Linux service runs in the CI and on real desktops. The macOS one is written against the documented C API and has never run on a Mac. A face of a macOS collection (.ttc) is found by its PostScript name (gt_face_postscript_name, gt_file_face_count). On Linux and macOS cargo build --release --locked --lib --target-dir ../../build/text/target in native/text, then a copy of libgt_text.a to build/text/ (tk build_text does it, on Windows and Linux).

Comparing the backends

tools/text_bench (Markdown report, -v glyph by glyph, -perf timings) and tests/text_backends (logic only). On the corpus of tools/text_corpus (19 samples) both backends give the same widths everywhere, the same glyph ids, clusters and advances in 17 samples, the same bidi levels and the same design metrics. Known differences:

  • Marks: HarfBuzz composes a base and a mark the font has precomposed (a + U+0308 + U+0323: ä + dot below, 2 glyphs), DirectWrite keeps the decomposed glyphs (3). Same width.
  • Emoji with a skin tone: HarfBuzz makes 👍🏽 one cluster (one grapheme, one caret stop), DirectWrite two.
  • Hebrew (Segoe UI): two glyphs sit 20 and 50 font units apart, same ids and advances. Segoe UI kerns right-to-left pairs as OpenType wants them written: the first glyph gets XPlacement -k and XAdvance -k (ל before ם: -50; ז before ה: -20), so that the glyph stays where it is and the gap after it closes. HarfBuzz applies both (native/text/examples/ shape_dump: gid 2932 adv 1078 xoff -50; with -kern: adv 1128 xoff 0); DirectWrite's GetGlyphPlacements applies the advance but returns no offset for these glyphs (its advanceOffset is 0), so the glyph moves 50 units toward the letter before it and the pair it should kern does not close. The native backend is the right one here.
  • Line breaks in Thai, Lao, Khmer, Myanmar: ICU4X's dictionaries and Windows' differ (one Thai word boundary); Windows finds no break at all inside Lao, Khmer and Myanmar text, ICU4X does.
  • DirectWrite simulates bold and oblique (with_simulations); Fontations does not: such faces shape with the regular face's advances under native.
  • Faces DirectWrite opens through a loader other than files and this package's memory loader cannot be read natively (none today).

The comparison found a bug of the dwrite backend, fixed: an explicit feature list (letter-spacing, any font-feature-settings) replaced DirectWrite's default features, so kerning was lost.

Not done yet

  • sbix and SVG color glyphs off Windows (COLR and CBDT are drawn).

Edit this page on GitHub